Skip to main content

libsurfer/
rectangle.rs

1use crate::{
2    annotation::{Annotatable, AnnotationData},
3    comment::Comment,
4    config::SurferTheme,
5    displayed_item::DisplayedItemRef,
6    graphics::{Anchor, GraphicsY},
7    message::Message,
8    time::TimeFormatter,
9    view::DrawingContext,
10    viewport::Viewport,
11    wave_data::WaveData,
12};
13use egui::{Id, Pos2, Rect, Response, Sense, Stroke, Ui, Widget};
14use emath::RectTransform;
15use num::BigInt;
16
17const DEFAULT_TYPE: &str = "Rectangle";
18const SELECTED_GAMMA_FACTOR: f32 = 1.1;
19const SELECTED_WIDTH_FACTOR: f32 = 1.3;
20const HITBOX_SIZE_FACTOR: f32 = 3.;
21
22#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]
23pub struct AnchorPoint {
24    pub wave: Option<GraphicsY>,
25    pub time: BigInt,
26}
27
28#[derive(Clone, serde::Serialize, serde::Deserialize)]
29pub struct RectAnnotation {
30    pub annotation_data: AnnotationData,
31    pub from: AnchorPoint,
32    pub to: AnchorPoint,
33    pub rect: Rect,
34}
35
36impl RectAnnotation {
37    pub(crate) fn new(
38        id: Id,
39        time_at_start: BigInt,
40        time_at_end: BigInt,
41        wave_from: Option<GraphicsY>,
42        wave_to: Option<GraphicsY>,
43        rect: Rect,
44        num: i32,
45    ) -> Self {
46        let name = format!("{DEFAULT_TYPE} {num}");
47        let annotation_data = AnnotationData::new(id, name, num);
48        Self {
49            annotation_data,
50            from: AnchorPoint {
51                wave: wave_from,
52                time: time_at_start,
53            },
54            to: AnchorPoint {
55                wave: wave_to,
56                time: time_at_end,
57            },
58            rect,
59        }
60    }
61    #[must_use]
62    pub fn get_id(&self) -> Id {
63        self.annotation_data.id
64    }
65
66    #[must_use]
67    pub fn get_pos(
68        &self,
69        waves: &WaveData,
70        viewport: &Viewport,
71        ctx: &DrawingContext,
72        y_offset: f32,
73    ) -> Option<Pos2> {
74        let num_timestamps = waves.safe_num_timestamps();
75
76        let x = viewport.pixel_from_time(&self.from.time, ctx.cfg.canvas_size.x, &num_timestamps);
77
78        let from_y = self.from.wave.as_ref().and_then(|f| waves.get_item_y(f))?;
79        let to_y = self.to.wave.as_ref().and_then(|to| waves.get_item_y(to))?;
80
81        let min_y = (from_y + y_offset).min(to_y + y_offset);
82
83        Some((ctx.to_screen)(x, min_y))
84    }
85
86    //Find the correct y_positions for the rectangle. If the "p" value is none, it means we have a snapped value
87    //and make sure to anchor them correctly.
88    fn resolve_y_positions(&mut self, waves: &WaveData) -> (Option<f32>, Option<f32>) {
89        let mut from_y = calculate_y(self.from.wave.as_ref(), waves);
90        let mut to_y = calculate_y(self.to.wave.as_ref(), waves);
91
92        if from_y >= to_y {
93            if let Some(wave_from) = self.from.wave.as_mut()
94                && matches!(wave_from.anchor, Anchor::Top)
95            {
96                wave_from.anchor = Anchor::Bottom;
97                from_y = calculate_y(Some(wave_from), waves);
98            }
99
100            if let Some(wave_to) = self.to.wave.as_mut()
101                && matches!(wave_to.anchor, Anchor::Bottom)
102            {
103                wave_to.anchor = Anchor::Top;
104                to_y = calculate_y(Some(wave_to), waves);
105            }
106        }
107        (from_y, to_y)
108    }
109
110    /// Calculate the correct position of the rectangle on to the canvas.
111    #[allow(clippy::too_many_arguments)]
112    fn compute_rect(
113        &mut self,
114        from_y: f32,
115        to_y: f32,
116        waves: &WaveData,
117        ctx: &DrawingContext,
118        viewport_idx: usize,
119        theme: &SurferTheme,
120        y_offset: f32,
121    ) {
122        let viewport = waves.viewports[viewport_idx];
123        let num_timestamps = waves.safe_num_timestamps();
124
125        //Update size and coloring from theme and whether it selected or not
126        self.annotation_data.stroke = Stroke::new(
127            theme.annotation_rectangle.width,
128            theme.annotation_rectangle.color,
129        );
130        // y_offset adjusts positioning whether the default timeline is shown or not.
131        let min_y = from_y.min(to_y) + y_offset;
132        let max_y = from_y.max(to_y) + y_offset;
133
134        let min_x =
135            viewport.pixel_from_time(&self.from.time, ctx.cfg.canvas_size.x, &num_timestamps);
136        let max_x = viewport.pixel_from_time(&self.to.time, ctx.cfg.canvas_size.x, &num_timestamps);
137
138        self.rect = Rect {
139            min: (ctx.to_screen)(min_x, min_y),
140            max: (ctx.to_screen)(max_x, max_y),
141        }
142    }
143}
144
145pub(crate) fn calculate_y(wave: Option<&GraphicsY>, waves: &WaveData) -> Option<f32> {
146    wave.and_then(|from| waves.get_item_y(from))
147}
148
149impl Annotatable for RectAnnotation {
150    fn get_id(&self) -> Id {
151        self.annotation_data.id
152    }
153
154    fn get_type(&self) -> &str {
155        DEFAULT_TYPE
156    }
157
158    fn set_name(&mut self, name: String) {
159        self.annotation_data.name = name;
160    }
161
162    fn get_name(&self) -> String {
163        self.annotation_data.name.clone()
164    }
165
166    fn is_selected(&mut self) {
167        self.annotation_data.stroke.width *= SELECTED_WIDTH_FACTOR;
168        self.annotation_data
169            .stroke
170            .color
171            .gamma_multiply(SELECTED_GAMMA_FACTOR);
172    }
173
174    fn set_visibility(&mut self, visible: bool) {
175        self.annotation_data.visible = visible;
176    }
177
178    fn show_comments(&self) -> bool {
179        self.annotation_data.show_comments
180    }
181
182    fn show_comment_box(&self) -> bool {
183        self.annotation_data.comment_box.visible
184    }
185
186    fn set_show_comments(&mut self, show: bool) {
187        self.annotation_data.show_comments = show;
188    }
189
190    fn get_comment_box(&self) -> Comment {
191        self.annotation_data.comment_box.clone()
192    }
193
194    fn get_comment_box_mut(&mut self) -> &mut Comment {
195        &mut self.annotation_data.comment_box
196    }
197
198    fn get_messages(&self) -> Vec<crate::comment::CommentMessage> {
199        self.annotation_data.comment_box.message_chain.clone()
200    }
201
202    fn is_visible(&self) -> bool {
203        self.annotation_data.visible
204    }
205
206    fn get_center_time(&self) -> BigInt {
207        (&self.from.time + &self.to.time) / 2
208    }
209
210    fn get_start_time(&self) -> BigInt {
211        self.from.time.clone()
212    }
213
214    fn get_end_time(&self) -> BigInt {
215        self.to.time.clone()
216    }
217
218    fn is_attached(&self, removed_ref: &DisplayedItemRef) -> bool {
219        self.from
220            .wave
221            .as_ref()
222            .is_some_and(|wave| &wave.item == removed_ref)
223            || self
224                .to
225                .wave
226                .as_ref()
227                .is_some_and(|wave| &wave.item == removed_ref)
228    }
229
230    fn get_from_wave(&self) -> Option<GraphicsY> {
231        self.from.wave.clone()
232    }
233
234    fn get_to_wave(&self) -> Option<GraphicsY> {
235        self.to.wave.clone()
236    }
237
238    fn draw(
239        &self,
240        ui: &mut Ui,
241        waves: &WaveData,
242        viewport_idx: usize,
243        ctx: &mut DrawingContext,
244        theme: &SurferTheme,
245        msgs: &mut Vec<Message>,
246        y_offset: f32,
247        to_screen: RectTransform,
248        time_formatter: &TimeFormatter,
249    ) {
250        let mut rectangle_annotation = self.clone();
251
252        rectangle_annotation.annotation_data.id =
253            egui::Id::new(("rectangle", self.annotation_data.id, viewport_idx));
254
255        let (from_y, to_y) = rectangle_annotation.resolve_y_positions(waves);
256
257        if let Some(to_y) = to_y
258            && let Some(from_y) = from_y
259        {
260            rectangle_annotation.compute_rect(
261                from_y,
262                to_y,
263                waves,
264                ctx,
265                viewport_idx,
266                theme,
267                y_offset,
268            );
269
270            if waves.selected_annotation == Some(self.get_id()) {
271                rectangle_annotation.is_selected();
272            }
273
274            let hover_start_time = time_formatter.format(&self.from.time);
275            let hover_end_time = time_formatter.format(&self.to.time);
276
277            let group_name = waves
278                .annotation_groups
279                .iter()
280                .find(|group| group.annotations.contains(&self.get_id()))
281                .map(|group| group.name.clone())
282                .unwrap_or("Ungrouped".to_string());
283            let res = ui.add(rectangle_annotation).on_hover_ui(|ui| {
284                self.draw_hover_info(group_name, ui, (&hover_start_time, &hover_end_time));
285            });
286
287            if res.clicked_by(egui::PointerButton::Primary) {
288                msgs.push(Message::SetActiveViewport(viewport_idx));
289                msgs.push(Message::AnnotationClicked(
290                    Some(self.annotation_data.id),
291                    res.interact_pointer_pos(),
292                    Some(viewport_idx),
293                    Some(to_screen),
294                    Some(ctx.cfg.canvas_size.x),
295                ));
296                msgs.push(Message::ClickHandled());
297            }
298        }
299    }
300
301    fn get_comment_position(
302        &self,
303        viewport: &Viewport,
304        ctx: &DrawingContext,
305        waves: &WaveData,
306        offset: f32,
307    ) -> Pos2 {
308        let num_timestamps = waves.safe_num_timestamps();
309        let x = viewport.pixel_from_time(&self.to.time, ctx.cfg.canvas_size.x, &num_timestamps);
310        let y = calculate_y(self.to.wave.as_ref(), waves).unwrap() + offset;
311        (ctx.to_screen)(x, y)
312    }
313
314    fn get_time_info(&self, time_formatter: &TimeFormatter) -> String {
315        format!(
316            "from: {}, to: {}",
317            time_formatter.format(&self.from.time),
318            time_formatter.format(&self.to.time)
319        )
320    }
321}
322
323/// Creates an outer and inner rectangle, used to identify whether the annotation was clicked on.
324fn point_on_rect_border(p: emath::Pos2, rect: Rect, width: f32) -> (bool, Rect) {
325    let half_width: f32 = width * HITBOX_SIZE_FACTOR;
326    let outer_rect = Rect {
327        min: emath::Pos2 {
328            x: rect.min.x - half_width,
329            y: rect.min.y - half_width,
330        },
331        max: emath::Pos2 {
332            x: rect.max.x + half_width,
333            y: rect.max.y + half_width,
334        },
335    };
336    let inner_rect = Rect {
337        min: emath::Pos2 {
338            x: rect.min.x + half_width,
339            y: rect.min.y + half_width,
340        },
341        max: emath::Pos2 {
342            x: rect.max.x - half_width,
343            y: rect.max.y - half_width,
344        },
345    };
346    (
347        outer_rect.contains(p) && !inner_rect.contains(p),
348        outer_rect,
349    )
350}
351
352impl Widget for RectAnnotation {
353    fn ui(self, ui: &mut Ui) -> Response {
354        if self.is_visible() {
355            ui.painter().rect_stroke(
356                self.rect,
357                0.0,
358                self.annotation_data.stroke,
359                egui::StrokeKind::Middle,
360            );
361            // Always draw the rectangle but if we are on border we should also register clicks.
362            // This allows the click to be transferred unto the underlying panel so the rectangle is hollow
363            let (on_border, hitbox) = ui
364                .ctx()
365                .pointer_hover_pos()
366                .map_or((false, Rect::ZERO), |p| {
367                    point_on_rect_border(p, self.rect, self.annotation_data.stroke.width)
368                });
369
370            if on_border {
371                ui.interact(hitbox, self.annotation_data.id, Sense::click_and_drag())
372            } else {
373                ui.allocate_response(egui::Vec2::ZERO, egui::Sense::empty())
374            }
375        } else {
376            let rect = self.hide_annotation(ui, self.annotation_data.stroke, self.rect.min);
377            ui.interact(rect, self.annotation_data.id, egui::Sense::click_and_drag())
378        }
379    }
380}