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