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 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 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 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 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 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}