Skip to main content

libsurfer/
annotation.rs

1use egui::{Color32, Frame, Id, Pos2, Rect, Stroke, Ui};
2use egui_remixicon::icons;
3use emath::RectTransform;
4use num::BigInt;
5use tracing::warn;
6
7use crate::{
8    SystemState,
9    arrow::ArrowAnnotation,
10    comment::{Comment, CommentMessage},
11    config::SurferTheme,
12    displayed_item::DisplayedItemRef,
13    graphics::GraphicsY,
14    message::Message,
15    rectangle::RectAnnotation,
16    time::TimeFormatter,
17    view::DrawingContext,
18    viewport::Viewport,
19    wave_data::WaveData,
20};
21
22const DEFAULT_HIDE_RADIUS: f32 = 5.0;
23
24#[derive(Clone, serde::Serialize, serde::Deserialize)]
25pub struct AnnotationData {
26    pub id: Id,
27    pub visible: bool,
28    pub name: String,
29    pub stroke: Stroke,
30    pub show_comments: bool,
31    pub comment_box: Comment,
32}
33
34impl AnnotationData {
35    pub(crate) fn new(id_source: impl egui::AsId, name: String, num: i32) -> Self {
36        let id = Id::new(id_source);
37        let c_id = Id::new(("comment_box", num));
38        AnnotationData {
39            id,
40            visible: true,
41            name,
42            stroke: Stroke::new(2.0, Color32::from_rgb(255, 255, 255)),
43            show_comments: false,
44            comment_box: Comment::new(c_id, id),
45        }
46    }
47}
48
49#[derive(Clone, serde::Serialize, serde::Deserialize)]
50pub enum Annotation {
51    Arrow(ArrowAnnotation),
52    Rect(RectAnnotation),
53}
54impl Annotatable for Annotation {
55    fn get_id(&self) -> Id {
56        match self {
57            Annotation::Arrow(a) => a.get_id(),
58            Annotation::Rect(r) => r.get_id(),
59        }
60    }
61
62    fn get_type(&self) -> &str {
63        match self {
64            Annotation::Arrow(a) => a.get_type(),
65            Annotation::Rect(r) => r.get_type(),
66        }
67    }
68
69    fn set_name(&mut self, name: &str) {
70        match self {
71            Annotation::Arrow(a) => a.set_name(name),
72            Annotation::Rect(r) => r.set_name(name),
73        }
74    }
75
76    fn get_name(&self) -> String {
77        match self {
78            Annotation::Arrow(a) => a.get_name(),
79            Annotation::Rect(r) => r.get_name(),
80        }
81    }
82
83    fn is_selected(&mut self) {
84        match self {
85            Annotation::Arrow(a) => a.is_selected(),
86            Annotation::Rect(r) => r.is_selected(),
87        }
88    }
89
90    fn set_visibility(&mut self, visible: bool) {
91        match self {
92            Annotation::Arrow(a) => a.set_visibility(visible),
93            Annotation::Rect(r) => r.set_visibility(visible),
94        }
95    }
96
97    fn show_comments(&self) -> bool {
98        match self {
99            Annotation::Arrow(a) => a.show_comments(),
100            Annotation::Rect(r) => r.show_comments(),
101        }
102    }
103    fn show_comment_box(&self) -> bool {
104        match self {
105            Annotation::Arrow(a) => a.show_comment_box(),
106            Annotation::Rect(r) => r.show_comment_box(),
107        }
108    }
109
110    fn set_show_comments(&mut self, show: bool) {
111        match self {
112            Annotation::Arrow(a) => a.set_show_comments(show),
113            Annotation::Rect(r) => r.set_show_comments(show),
114        }
115    }
116
117    fn get_comment_box(&self) -> Comment {
118        match self {
119            Annotation::Arrow(a) => a.get_comment_box(),
120            Annotation::Rect(r) => r.get_comment_box(),
121        }
122    }
123
124    fn get_comment_box_mut(&mut self) -> &mut Comment {
125        match self {
126            Annotation::Arrow(a) => a.get_comment_box_mut(),
127            Annotation::Rect(r) => r.get_comment_box_mut(),
128        }
129    }
130
131    fn is_visible(&self) -> bool {
132        match self {
133            Annotation::Arrow(a) => a.is_visible(),
134            Annotation::Rect(r) => r.is_visible(),
135        }
136    }
137
138    fn get_center_time(&self) -> BigInt {
139        match self {
140            Annotation::Arrow(a) => a.get_center_time(),
141            Annotation::Rect(r) => r.get_center_time(),
142        }
143    }
144
145    fn get_start_time(&self) -> BigInt {
146        match self {
147            Annotation::Arrow(a) => a.get_start_time(),
148            Annotation::Rect(r) => r.get_start_time(),
149        }
150    }
151
152    fn get_end_time(&self) -> BigInt {
153        match self {
154            Annotation::Arrow(a) => a.get_end_time(),
155            Annotation::Rect(r) => r.get_end_time(),
156        }
157    }
158
159    fn is_attached(&self, removed_ref: &DisplayedItemRef) -> bool {
160        match self {
161            Annotation::Arrow(a) => a.is_attached(removed_ref),
162            Annotation::Rect(r) => r.is_attached(removed_ref),
163        }
164    }
165
166    fn get_from_wave(&self) -> Option<GraphicsY> {
167        match self {
168            Annotation::Arrow(a) => a.get_from_wave(),
169            Annotation::Rect(r) => r.get_from_wave(),
170        }
171    }
172
173    fn get_to_wave(&self) -> Option<GraphicsY> {
174        match self {
175            Annotation::Arrow(a) => a.get_to_wave(),
176            Annotation::Rect(r) => r.get_to_wave(),
177        }
178    }
179
180    #[allow(clippy::too_many_arguments)]
181    fn draw(
182        &self,
183        ui: &mut Ui,
184        waves: &WaveData,
185        viewport_idx: usize,
186        ctx: &mut DrawingContext,
187        theme: &SurferTheme,
188        msgs: &mut Vec<Message>,
189        y_offset: f32,
190        to_screen: RectTransform,
191        time_formatter: &TimeFormatter,
192    ) {
193        match self {
194            Annotation::Arrow(a) => a.draw(
195                ui,
196                waves,
197                viewport_idx,
198                ctx,
199                theme,
200                msgs,
201                y_offset,
202                to_screen,
203                time_formatter,
204            ),
205            Annotation::Rect(r) => r.draw(
206                ui,
207                waves,
208                viewport_idx,
209                ctx,
210                theme,
211                msgs,
212                y_offset,
213                to_screen,
214                time_formatter,
215            ),
216        }
217    }
218
219    fn get_comment_position(
220        &self,
221        viewport: &Viewport,
222        ctx: &DrawingContext,
223        waves: &WaveData,
224        offset: f32,
225    ) -> Pos2 {
226        match self {
227            Annotation::Arrow(a) => a.get_comment_position(viewport, ctx, waves, offset),
228            Annotation::Rect(r) => r.get_comment_position(viewport, ctx, waves, offset),
229        }
230    }
231
232    fn get_time_info(&self, time_formatter: &TimeFormatter) -> String {
233        match self {
234            Annotation::Arrow(a) => a.get_time_info(time_formatter),
235            Annotation::Rect(r) => r.get_time_info(time_formatter),
236        }
237    }
238
239    fn get_messages(&self) -> Vec<CommentMessage> {
240        match self {
241            Annotation::Arrow(a) => a.get_messages(),
242            Annotation::Rect(r) => r.get_messages(),
243        }
244    }
245}
246
247pub trait Annotatable {
248    fn get_id(&self) -> Id;
249    fn get_type(&self) -> &str;
250    fn set_name(&mut self, name: &str);
251    fn get_name(&self) -> String;
252    fn is_selected(&mut self);
253    fn set_visibility(&mut self, visible: bool);
254    fn show_comments(&self) -> bool;
255    fn show_comment_box(&self) -> bool;
256    fn set_show_comments(&mut self, show: bool);
257    fn get_comment_box(&self) -> Comment;
258    fn get_comment_box_mut(&mut self) -> &mut Comment;
259    fn get_messages(&self) -> Vec<CommentMessage>;
260    fn is_visible(&self) -> bool;
261    fn get_center_time(&self) -> BigInt;
262    fn get_start_time(&self) -> BigInt;
263    fn get_end_time(&self) -> BigInt;
264    /// Checks whether the annotation is attached to the given Item.
265    fn is_attached(&self, removed_ref: &DisplayedItemRef) -> bool;
266    fn get_time_info(&self, time_formatter: &TimeFormatter) -> String;
267    fn get_from_wave(&self) -> Option<GraphicsY>;
268    fn get_to_wave(&self) -> Option<GraphicsY>;
269    #[allow(clippy::too_many_arguments)]
270    fn draw(
271        &self,
272        ui: &mut Ui,
273        waves: &WaveData,
274        viewport_idx: usize,
275        ctx: &mut DrawingContext,
276        theme: &SurferTheme,
277        msgs: &mut Vec<Message>,
278        y_offset: f32,
279        to_screen: RectTransform,
280        time_formatter: &TimeFormatter,
281    );
282    fn draw_quick_menu(
283        &self,
284        ui: &mut egui::Ui,
285        msgs: &mut Vec<Message>,
286        waves: &WaveData,
287        viewport_rect: egui::Rect,
288        position: Pos2,
289    ) {
290        let id: Id = self.get_id();
291
292        let menu_rect = egui::Rect::from_min_size(position, egui::vec2(0.0, 0.0));
293
294        if !viewport_rect.intersects(menu_rect) {
295            return;
296        }
297
298        egui::Area::new(egui::Id::new(("annotation_quick_menu", id)))
299            .order(egui::Order::Foreground)
300            .fixed_pos(position)
301            .show(ui.ctx(), |ui| {
302                Frame::popup(ui.style())
303                    .fill(ui.visuals().extreme_bg_color)
304                    .stroke(Stroke::new(
305                        1.0,
306                        ui.visuals().widgets.noninteractive.bg_stroke.color,
307                    ))
308                    .corner_radius(8.0)
309                    .inner_margin(egui::Margin::same(4))
310                    .show(ui, |ui| {
311                        ui.spacing_mut().item_spacing.x = 2.0;
312                        ui.spacing_mut().button_padding = egui::vec2(4.0, 2.0);
313
314                        ui.horizontal(|ui| {
315                            if ui
316                                .button(icons::SEARCH_LINE)
317                                .on_hover_text("Go to annotation")
318                                .clicked()
319                            {
320                                msgs.push(Message::GoToAnnotationPosition(
321                                    id,
322                                    waves.last_active_viewport_idx,
323                                ));
324                            }
325
326                            let vis_icon = if self.is_visible() {
327                                icons::EYE_LINE
328                            } else {
329                                icons::EYE_OFF_LINE
330                            };
331
332                            if ui
333                                .button(vis_icon)
334                                .on_hover_text("Toggle visibility")
335                                .clicked()
336                            {
337                                msgs.push(Message::ToggleAnnotationVisiblility(id));
338                            }
339
340                            if ui
341                                .button(icons::DELETE_BIN_LINE)
342                                .on_hover_text("Delete annotation")
343                                .clicked()
344                            {
345                                msgs.push(Message::RemoveAnnotation(id));
346                            }
347
348                            if self.is_visible() {
349                                let comment = self.get_comment_box();
350
351                                let chat_icon = if comment.visible {
352                                    icons::CHAT_4_LINE
353                                } else {
354                                    icons::CHAT_OFF_LINE
355                                };
356
357                                if ui
358                                    .button(chat_icon)
359                                    .on_hover_text("Toggle comment visibility")
360                                    .clicked()
361                                {
362                                    msgs.push(Message::ToggleCommentVisibility(id));
363                                }
364                            }
365                        });
366                    });
367            });
368    }
369
370    fn draw_hover_info(
371        &self,
372        group_name: &str,
373        ui: &mut egui::Ui,
374        (time_start_str, time_end_str): (&str, &str),
375    ) {
376        ui.label(format!("Start time: {time_start_str} "));
377        ui.label(format!("End time:   {time_end_str} "));
378        ui.painter().add(egui::Shape::line_segment(
379            [ui.cursor().left_top(), ui.cursor().right_top()],
380            egui::Stroke::new(0.2, egui::Color32::LIGHT_GRAY),
381        ));
382        ui.label(format!("Name: {}", self.get_name()));
383        ui.label(format!("Group: {group_name}"));
384        ui.label(format!("Type: {}", self.get_type()));
385        ui.label(format!("ID: {:?}", self.get_id()));
386    }
387    fn hide_annotation(&self, ui: &mut egui::Ui, stroke: Stroke, center: Pos2) -> Rect {
388        ui.painter()
389            .circle_filled(center, DEFAULT_HIDE_RADIUS, stroke.color);
390
391        egui::Rect::from_center_size(
392            center,
393            egui::vec2(DEFAULT_HIDE_RADIUS * 2.0, DEFAULT_HIDE_RADIUS * 2.0),
394        )
395    }
396    fn get_comment_position(
397        &self,
398        viewport: &Viewport,
399        ctx: &DrawingContext,
400        waves: &WaveData,
401        offset: f32,
402    ) -> Pos2;
403
404    fn draw_comment_box(
405        &self,
406        ui: &mut egui::Ui,
407        viewport_idx: usize,
408        msgs: &mut Vec<Message>,
409        comment_position: Pos2,
410    ) -> (Id, Comment) {
411        let mut comment = self.get_comment_box();
412        comment.id = Id::new((comment.id, viewport_idx));
413
414        comment.name = self.get_name();
415
416        // X-coordinate
417        comment.rect.min.x = comment_position.x + comment.offset.x;
418        comment.rect.max.x = comment_position.x + comment.offset.x + comment.size.x;
419
420        // Y-coordinate
421        comment.rect.min.y = comment_position.y + comment.offset.y;
422        comment.rect.max.y = comment_position.y + comment.offset.y + comment.size.y;
423
424        comment.anchor = comment_position;
425        ui.add(&mut comment);
426        // Handle "Enter" key to submit new comment
427        if let Some(save_text) = &comment.save_text {
428            msgs.push(Message::AddCommentMessage(
429                comment.annotation_id,
430                save_text.clone(),
431                "user".to_string(),
432            ));
433        }
434
435        (comment.annotation_id, comment)
436    }
437
438    fn update_comment_box(&mut self, comment: Comment) {
439        let c = self.get_comment_box_mut();
440        c.name = comment.name;
441        c.new_text = comment.new_text;
442        c.offset = comment.offset;
443        c.size = comment.size;
444        c.rect = comment.rect;
445        c.visible = comment.visible;
446    }
447}
448
449impl WaveData {
450    pub fn delete_annotation(&mut self, id: egui::Id) {
451        self.annotations
452            .retain(|annotation| annotation.get_id() != id);
453    }
454
455    #[must_use]
456    pub fn get_annotation_by_id(&self, id: &egui::Id) -> Option<&Annotation> {
457        self.annotations.iter().find(|anno| anno.get_id() == *id)
458    }
459
460    #[allow(clippy::too_many_arguments)]
461    pub fn draw_annotations(
462        &self,
463        ui: &mut egui::Ui,
464        viewport: &Viewport,
465        viewport_idx: usize,
466        ctx: &mut DrawingContext,
467        theme: &SurferTheme,
468        msgs: &mut Vec<Message>,
469        y_offset: f32,
470        viewport_rect: egui::Rect,
471        to_screen: RectTransform,
472        time_formatter: &TimeFormatter,
473    ) {
474        let mut comment_changes = Vec::new();
475
476        for annotation in &self.annotations {
477            annotation.draw(
478                ui,
479                self,
480                viewport_idx,
481                ctx,
482                theme,
483                msgs,
484                y_offset,
485                to_screen,
486                time_formatter,
487            );
488
489            if self.selected_annotation == Some(annotation.get_id())
490                && viewport_idx == self.last_active_viewport_idx
491            {
492                let mut menu_position = self.annotation_menu_pos.unwrap();
493                let menu_time = self.annotation_menu_time.clone().unwrap();
494
495                menu_position.x = viewport.pixel_from_time(
496                    &menu_time,
497                    ctx.cfg.canvas_size.x,
498                    &self.safe_max_timestamp(),
499                    self.time_offset(),
500                );
501                let temp_y = menu_position.y;
502                menu_position = (ctx.to_screen)(menu_position.x, menu_position.y);
503                menu_position.y = temp_y;
504
505                annotation.draw_quick_menu(ui, msgs, self, viewport_rect, menu_position);
506            }
507        }
508        for annotation in &self.annotations {
509            if annotation.show_comment_box() && annotation.is_visible() {
510                let comment_position =
511                    annotation.get_comment_position(viewport, ctx, self, y_offset);
512                let (id, comment) =
513                    annotation.draw_comment_box(ui, viewport_idx, msgs, comment_position);
514                // Only update comment if change has been made or something is being written
515                if comment.change || annotation.get_comment_box().new_text != comment.new_text {
516                    comment_changes.push((id, comment));
517                }
518            }
519        }
520        if !comment_changes.is_empty() {
521            msgs.push(Message::UpdateCommentBox(comment_changes));
522        }
523    }
524}
525
526impl SystemState {
527    pub(crate) fn go_to_annotation_position(&mut self, anno_id: Id, viewport_idx: usize) {
528        if let Some(waves) = self.user.waves.as_mut() {
529            if let Some(max_timestamp) = waves.max_timestamp() {
530                if let Some(target) = waves.get_annotation_by_id(&anno_id) {
531                    let mut left = target.get_start_time();
532                    let mut right = target.get_end_time();
533                    let from_wave = target.get_from_wave();
534                    let to_wave = target.get_to_wave();
535
536                    let difference = (&right - &left) / 2;
537                    left -= &difference;
538                    right += difference;
539                    let time_offset = waves.time_offset().clone();
540                    waves.viewports[viewport_idx].zoom_to_range(
541                        &left,
542                        &right,
543                        &max_timestamp,
544                        &time_offset,
545                    );
546
547                    if let Some(from_wave) = from_wave
548                        && let Some(to_wave) = to_wave
549                    {
550                        if let Some(y_1) = waves.get_item_y(&from_wave)
551                            && let Some(y_2) = waves.get_item_y(&to_wave)
552                        {
553                            // let y_diff = (y_2 - y_1) * 0.5;
554                            // let center = y_1 + y_diff;
555                            if let Some(item) = waves.get_item_at_y(y_1.min(y_2)) {
556                                waves.scroll_to_item(item.0);
557                            }
558                        } else {
559                            warn!("GoToAnnotationPosition: got None from get_item_y");
560                        }
561                    } else {
562                        warn!("GoToAnnotationPosition: got None from to_wave");
563                    }
564                }
565
566                self.invalidate_draw_commands();
567            } else {
568                warn!(
569                    "Go to marker position: No timestamps count, even though waveforms should be loaded"
570                );
571            }
572        }
573    }
574
575    pub(crate) fn annotation_id(&mut self) -> Id {
576        let id = egui::Id::new(("annotation", self.annotation_id_source));
577        self.annotation_id_source += 1;
578        id
579    }
580}