libsurfer/
marker.rs

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