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