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