Skip to main content

libsurfer/
marker.rs

1use ecolor::Color32;
2use egui::{Context, RichText, WidgetText, Window};
3use egui_extras::{Column, TableBuilder};
4use emath::{Align2, Pos2, Rect};
5use epaint::{CornerRadius, FontId, Stroke};
6use itertools::Itertools;
7use num::{BigInt, Zero};
8
9use crate::SystemState;
10use crate::drawing_canvas::draw_vertical_line;
11use crate::{
12    config::SurferTheme,
13    displayed_item::{DisplayedItem, DisplayedItemRef, DisplayedMarker},
14    message::Message,
15    time::TimeFormatter,
16    view::{DrawingContext, ItemDrawingInfo},
17    viewport::Viewport,
18    wave_data::WaveData,
19};
20
21pub const DEFAULT_MARKER_NAME: &str = "Marker";
22const MAX_MARKERS: usize = 255;
23const MAX_MARKER_INDEX: u8 = 254;
24const CURSOR_MARKER_IDX: u8 = 255;
25
26impl WaveData {
27    /// Get the color for a marker by its index, falling back to cursor color if not found
28    fn get_marker_color(&self, idx: u8, theme: &SurferTheme) -> Color32 {
29        self.items_tree
30            .iter()
31            .find_map(|node| {
32                if let Some(DisplayedItem::Marker(marker)) =
33                    self.displayed_items.get(&node.item_ref)
34                    && marker.idx == idx
35                {
36                    return marker
37                        .color
38                        .as_ref()
39                        .and_then(|color| theme.get_color(color));
40                }
41                None
42            })
43            .unwrap_or(theme.cursor.color)
44    }
45
46    pub fn draw_cursor(&self, theme: &SurferTheme, ctx: &mut DrawingContext, viewport: &Viewport) {
47        if let Some(marker) = &self.cursor {
48            let num_timestamps = self.safe_num_timestamps();
49            let x = viewport.pixel_from_time(marker, ctx.cfg.canvas_size.x, &num_timestamps);
50            draw_vertical_line(x, ctx, &theme.cursor);
51        }
52    }
53
54    pub fn draw_markers(&self, theme: &SurferTheme, ctx: &mut DrawingContext, viewport: &Viewport) {
55        let num_timestamps = self.safe_num_timestamps();
56        for (idx, marker) in &self.markers {
57            let color = self.get_marker_color(*idx, theme);
58            let stroke = Stroke {
59                color,
60                width: theme.cursor.width,
61            };
62            let x = viewport.pixel_from_time(marker, ctx.cfg.canvas_size.x, &num_timestamps);
63            draw_vertical_line(x, ctx, stroke);
64        }
65    }
66
67    #[must_use]
68    pub fn can_add_marker(&self) -> bool {
69        self.markers.len() < MAX_MARKERS
70    }
71
72    pub fn add_marker(
73        &mut self,
74        location: &BigInt,
75        name: Option<String>,
76        move_focus: bool,
77    ) -> Option<DisplayedItemRef> {
78        if !self.can_add_marker() {
79            return None;
80        }
81
82        let Some(idx) = (0..=MAX_MARKER_INDEX).find(|idx| !self.markers.contains_key(idx)) else {
83            // This shouldn't happen since can_add_marker() was already checked,
84            // but handle it gracefully
85            return None;
86        };
87
88        let item_ref = self.insert_item(
89            DisplayedItem::Marker(DisplayedMarker {
90                color: None,
91                background_color: None,
92                name,
93                idx,
94            }),
95            None,
96            move_focus,
97        );
98        self.markers.insert(idx, location.clone());
99
100        Some(item_ref)
101    }
102
103    pub fn remove_marker(&mut self, idx: u8) {
104        if let Some(&marker_item_ref) =
105            self.displayed_items
106                .iter()
107                .find_map(|(id, item)| match item {
108                    DisplayedItem::Marker(marker) if marker.idx == idx => Some(id),
109                    _ => None,
110                })
111        {
112            self.remove_displayed_item(marker_item_ref);
113        }
114    }
115
116    /// Set the marker with the specified id to the location. If the marker doesn't exist already,
117    /// it will be created
118    pub fn set_marker_position(&mut self, idx: u8, location: &BigInt) {
119        if !self.markers.contains_key(&idx) {
120            self.insert_item(
121                DisplayedItem::Marker(DisplayedMarker {
122                    color: None,
123                    background_color: None,
124                    name: None,
125                    idx,
126                }),
127                None,
128                true,
129            );
130        }
131        self.markers.insert(idx, location.clone());
132    }
133
134    pub fn move_marker_to_cursor(&mut self, idx: u8) {
135        if let Some(location) = self.cursor.clone() {
136            self.set_marker_position(idx, &location);
137        }
138    }
139
140    /// Draw text with background box at the specified position
141    /// Returns the text and its background rectangle info for reuse if needed
142    fn draw_text_with_background(
143        ctx: &mut DrawingContext,
144        x: f32,
145        text: &str,
146        background_color: Color32,
147        foreground_color: Color32,
148        padding: f32,
149    ) {
150        let y = ctx.cfg.canvas_size.y * 0.5;
151        let text_size = ctx.cfg.text_size;
152        // Measure text first
153        let rect = ctx.painter.text(
154            (ctx.to_screen)(x, y),
155            Align2::CENTER_CENTER,
156            text,
157            FontId::proportional(text_size),
158            foreground_color,
159        );
160
161        // Background rectangle with padding
162        let min = Pos2::new(rect.min.x - padding, rect.min.y - padding);
163        let max = Pos2::new(rect.max.x + padding, rect.max.y + padding);
164
165        ctx.painter
166            .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
167
168        // Draw text on top of background
169        ctx.painter.text(
170            (ctx.to_screen)(x, y),
171            Align2::CENTER_CENTER,
172            text,
173            FontId::proportional(text_size),
174            foreground_color,
175        );
176    }
177
178    pub fn draw_marker_number_boxes(
179        &self,
180        ctx: &mut DrawingContext,
181        theme: &SurferTheme,
182        viewport: &Viewport,
183    ) {
184        for displayed_item in self
185            .items_tree
186            .iter_visible()
187            .map(|node| self.displayed_items.get(&node.item_ref))
188            .filter_map(|item| match item {
189                Some(DisplayedItem::Marker(marker)) => Some(marker),
190                _ => None,
191            })
192        {
193            let item = DisplayedItem::Marker(displayed_item.clone());
194            let background_color = get_marker_background_color(&item, theme);
195
196            let x =
197                self.numbered_marker_location(displayed_item.idx, viewport, ctx.cfg.canvas_size.x);
198            let idx_string = displayed_item.idx.to_string();
199
200            Self::draw_text_with_background(
201                ctx,
202                x,
203                &idx_string,
204                background_color,
205                theme.foreground,
206                2.0,
207            );
208        }
209    }
210}
211
212impl SystemState {
213    pub fn draw_marker_window(&self, waves: &WaveData, ctx: &Context, msgs: &mut Vec<Message>) {
214        let mut open = true;
215
216        // Construct markers list: cursor first (if present), then numbered markers
217        let markers: Vec<(u8, &BigInt, WidgetText)> = waves
218            .cursor
219            .as_ref()
220            .into_iter()
221            .map(|cursor| {
222                (
223                    CURSOR_MARKER_IDX,
224                    cursor,
225                    WidgetText::RichText(RichText::new("Primary").into()),
226                )
227            })
228            .chain(
229                waves
230                    .items_tree
231                    .iter()
232                    .filter_map(|node| waves.displayed_items.get(&node.item_ref))
233                    .filter_map(|displayed_item| match displayed_item {
234                        DisplayedItem::Marker(marker) => {
235                            let text_color = self.get_item_text_color(displayed_item);
236                            Some((
237                                marker.idx,
238                                waves.numbered_marker_time(marker.idx),
239                                marker.marker_text(text_color),
240                            ))
241                        }
242                        _ => None,
243                    })
244                    .sorted_by(|a, b| Ord::cmp(&a.0, &b.0)),
245            )
246            .collect();
247
248        Window::new("Markers")
249            .collapsible(true)
250            .resizable(true)
251            .open(&mut open)
252            .show(ctx, |ui| {
253                ui.vertical_centered(|ui| {
254                    // Table of markers: header row then rows of time differences.
255                    let row_height = ui.text_style_height(&egui::TextStyle::Body);
256                    TableBuilder::new(ui)
257                        .striped(true)
258                        .cell_layout(egui::Layout::right_to_left(emath::Align::TOP))
259                        .columns(Column::auto().resizable(true), markers.len() + 1)
260                        .auto_shrink(emath::Vec2b::new(false, true))
261                        .header(row_height, |mut header| {
262                            header.col(|ui| {
263                                ui.label("");
264                            });
265                            for (marker_idx, _, widget_text) in &markers {
266                                header.col(|ui| {
267                                    if ui.label(widget_text.clone()).clicked() {
268                                        msgs.push(marker_click_message(
269                                            *marker_idx,
270                                            waves.cursor.as_ref(),
271                                        ));
272                                    }
273                                });
274                            }
275                        })
276                        .body(|mut body| {
277                            let time_formatter = TimeFormatter::new(
278                                &waves.inner.metadata().timescale,
279                                &self.user.wanted_timeunit,
280                                &self.get_time_format(),
281                            );
282                            for (marker_idx, row_marker_time, row_widget_text) in &markers {
283                                body.row(row_height, |mut row| {
284                                    row.col(|ui| {
285                                        if ui.label(row_widget_text.clone()).clicked() {
286                                            msgs.push(marker_click_message(
287                                                *marker_idx,
288                                                waves.cursor.as_ref(),
289                                            ));
290                                        }
291                                    });
292                                    for (_, col_marker_time, _) in &markers {
293                                        let diff = time_formatter
294                                            .format(&(*row_marker_time - *col_marker_time));
295                                        row.col(|ui| {
296                                            ui.label(diff);
297                                        });
298                                    }
299                                });
300                            }
301                        });
302                    ui.add_space(15.);
303                    if ui.button("Close").clicked() {
304                        msgs.push(Message::SetCursorWindowVisible(false));
305                    }
306                });
307            });
308        if !open {
309            msgs.push(Message::SetCursorWindowVisible(false));
310        }
311    }
312
313    pub fn draw_marker_boxes(
314        &self,
315        waves: &WaveData,
316        ctx: &mut DrawingContext,
317        viewport: &Viewport,
318        y_zero: f32,
319    ) {
320        let horizontal_padding = self.user.config.layout.waveforms_gap;
321
322        let time_formatter = TimeFormatter::new(
323            &waves.inner.metadata().timescale,
324            &self.user.wanted_timeunit,
325            &self.get_time_format(),
326        );
327        for drawing_info in waves.drawing_infos.iter().filter_map(|item| match item {
328            ItemDrawingInfo::Marker(marker) => Some(marker),
329            _ => None,
330        }) {
331            let Some(item) = waves
332                .items_tree
333                .get_visible(drawing_info.vidx)
334                .and_then(|node| waves.displayed_items.get(&node.item_ref))
335            else {
336                continue;
337            };
338
339            // We draw in absolute coords, but the variable offset in the y
340            // direction is also in absolute coordinates, so we need to
341            // compensate for that
342            let row_top = drawing_info.top - y_zero;
343            let row_bottom = drawing_info.bottom - y_zero;
344
345            let background_color = get_marker_background_color(item, &self.user.config.theme);
346
347            let x =
348                waves.numbered_marker_location(drawing_info.idx, viewport, ctx.cfg.canvas_size.x);
349
350            // Time string
351            let time = time_formatter.format(
352                waves
353                    .markers
354                    .get(&drawing_info.idx)
355                    .unwrap_or(&BigInt::zero()),
356            );
357
358            let text_color = self.user.config.theme.get_best_text_color(background_color);
359
360            // Create galley
361            let galley = ctx.painter.layout_no_wrap(
362                time,
363                FontId::proportional(ctx.cfg.text_size),
364                text_color,
365            );
366            let offset_width = galley.rect.width() * 0.5 + horizontal_padding;
367
368            // Background rectangle
369            let min = (ctx.to_screen)(x - offset_width, row_top);
370            let max = (ctx.to_screen)(x + offset_width, row_bottom);
371
372            ctx.painter
373                .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
374
375            // Draw actual text on top of rectangle
376            ctx.painter.galley(
377                (ctx.to_screen)(
378                    x - galley.rect.width() * 0.5,
379                    (row_top + row_bottom - galley.rect.height()) * 0.5,
380                ),
381                galley,
382                text_color,
383            );
384        }
385    }
386}
387
388/// Get the background color for a marker or cursor, with fallback to theme cursor color
389fn get_marker_background_color(item: &DisplayedItem, theme: &SurferTheme) -> Color32 {
390    item.color()
391        .and_then(|color| theme.get_color(color))
392        .unwrap_or(theme.cursor.color)
393}
394
395/// Generate the message for a marker click based on its index
396fn marker_click_message(marker_idx: u8, cursor: Option<&BigInt>) -> Message {
397    if marker_idx < CURSOR_MARKER_IDX {
398        Message::GoToMarkerPosition(marker_idx, 0)
399    } else {
400        Message::GoToTime(cursor.cloned(), 0)
401    }
402}