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((
218                255,
219                cursor,
220                WidgetText::RichText(RichText::new("Primary").into()),
221            ));
222        }
223
224        let mut numbered_markers = waves
225            .items_tree
226            .iter()
227            .map(|node| waves.displayed_items.get(&node.item_ref))
228            .filter_map(|displayed_item| match displayed_item {
229                Some(DisplayedItem::Marker(marker)) => {
230                    let text_color = self.get_item_text_color(displayed_item.unwrap());
231                    Some((
232                        marker.idx,
233                        waves.numbered_marker_time(marker.idx),
234                        marker.marker_text(text_color),
235                    ))
236                }
237                _ => None,
238            })
239            .sorted_by(|a, b| Ord::cmp(&a.0, &b.0))
240            .collect_vec();
241
242        markers.append(&mut numbered_markers);
243        Window::new("Markers")
244            .collapsible(true)
245            .resizable(true)
246            .open(&mut open)
247            .show(ctx, |ui| {
248                ui.vertical_centered(|ui| {
249                    Grid::new("markers")
250                        .striped(true)
251                        .num_columns(markers.len() + 1)
252                        .spacing([5., 5.])
253                        .show(ui, |ui| {
254                            ui.label("");
255                            for (marker_idx, _, widget_text) in &markers {
256                                if *marker_idx < 255 {
257                                    ui.selectable_label(false, widget_text.clone())
258                                        .clicked()
259                                        .then(|| {
260                                            msgs.push(Message::GoToMarkerPosition(*marker_idx, 0));
261                                        });
262                                } else {
263                                    ui.selectable_label(false, widget_text.clone())
264                                        .clicked()
265                                        .then(|| {
266                                            msgs.push(Message::GoToTime(waves.cursor.clone(), 0));
267                                        });
268                                }
269                            }
270                            ui.end_row();
271                            for (marker_idx, row_marker_time, row_widget_text) in &markers {
272                                if *marker_idx < 255 {
273                                    ui.selectable_label(false, row_widget_text.clone())
274                                        .clicked()
275                                        .then(|| {
276                                            msgs.push(Message::GoToMarkerPosition(*marker_idx, 0));
277                                        });
278                                } else {
279                                    ui.selectable_label(false, row_widget_text.clone())
280                                        .clicked()
281                                        .then(|| {
282                                            msgs.push(Message::GoToTime(waves.cursor.clone(), 0));
283                                        });
284                                }
285                                for (_, col_marker_time, _) in &markers {
286                                    ui.label(time_string(
287                                        &(*row_marker_time - *col_marker_time),
288                                        &waves.inner.metadata().timescale,
289                                        &self.user.wanted_timeunit,
290                                        &self.get_time_format(),
291                                    ));
292                                }
293                                ui.end_row();
294                            }
295                        });
296                    ui.add_space(15.);
297                    if ui.button("Close").clicked() {
298                        msgs.push(Message::SetCursorWindowVisible(false));
299                    }
300                });
301            });
302        if !open {
303            msgs.push(Message::SetCursorWindowVisible(false));
304        }
305    }
306
307    pub fn draw_marker_boxes(
308        &self,
309        waves: &WaveData,
310        ctx: &mut DrawingContext,
311        view_width: f32,
312        gap: f32,
313        viewport: &Viewport,
314        y_zero: f32,
315    ) {
316        let text_size = ctx.cfg.text_size;
317
318        for drawing_info in waves.drawing_infos.iter().filter_map(|item| match item {
319            ItemDrawingInfo::Marker(marker) => Some(marker),
320            _ => None,
321        }) {
322            let Some(item) = waves
323                .items_tree
324                .get_visible(drawing_info.item_list_idx)
325                .and_then(|node| waves.displayed_items.get(&node.item_ref))
326            else {
327                return;
328            };
329
330            // We draw in absolute coords, but the variable offset in the y
331            // direction is also in absolute coordinates, so we need to
332            // compensate for that
333            let y_offset = drawing_info.top - y_zero;
334            let y_bottom = drawing_info.bottom - y_zero;
335
336            let background_color = item
337                .color()
338                .and_then(|color| self.user.config.theme.get_color(color))
339                .unwrap_or(&self.user.config.theme.cursor.color);
340
341            let x = waves.numbered_marker_location(drawing_info.idx, viewport, view_width);
342
343            // Time string
344            let time = time_string(
345                waves
346                    .markers
347                    .get(&drawing_info.idx)
348                    .unwrap_or(&BigInt::from(0)),
349                &waves.inner.metadata().timescale,
350                &self.user.wanted_timeunit,
351                &self.get_time_format(),
352            );
353
354            let text_color = *self.user.config.theme.get_best_text_color(background_color);
355
356            // Create galley
357            let galley =
358                ctx.painter
359                    .layout_no_wrap(time, FontId::proportional(text_size), text_color);
360            let offset_width = galley.rect.width() * 0.5 + 2. * gap;
361
362            // Background rectangle
363            let min = (ctx.to_screen)(x - offset_width, y_offset - gap);
364            let max = (ctx.to_screen)(x + offset_width, y_bottom + gap);
365
366            ctx.painter.rect_filled(
367                Rect { min, max },
368                CornerRadius::default(),
369                *background_color,
370            );
371
372            // Draw actual text on top of rectangle
373            ctx.painter.galley(
374                (ctx.to_screen)(
375                    x - galley.rect.width() * 0.5,
376                    (y_offset + y_bottom - galley.rect.height()) * 0.5,
377                ),
378                galley,
379                text_color,
380            );
381        }
382    }
383}