Skip to main content

libsurfer/
rectangle.rs

1use crate::{
2    annotation::{Annotatable, AnnotationData},
3    annotation_list::DEFAULT_GROUP_NAME,
4    comment::Comment,
5    config::SurferTheme,
6    displayed_item::DisplayedItemRef,
7    graphics::{Anchor, GraphicsY},
8    message::Message,
9    time::TimeFormatter,
10    view::DrawingContext,
11    viewport::Viewport,
12    wave_data::WaveData,
13};
14use egui::{Id, Pos2, Rect, Response, Sense, Stroke, Ui, Widget};
15use emath::RectTransform;
16use num::BigInt;
17
18const DEFAULT_TYPE: &str = "Rectangle";
19const SELECTED_GAMMA_FACTOR: f32 = 1.1;
20const SELECTED_WIDTH_FACTOR: f32 = 1.3;
21const HITBOX_SIZE_FACTOR: f32 = 3.;
22
23#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]
24pub struct AnchorPoint {
25    pub wave: Option<GraphicsY>,
26    pub time: BigInt,
27}
28
29#[derive(Clone, serde::Serialize, serde::Deserialize)]
30pub struct RectAnnotation {
31    pub annotation_data: AnnotationData,
32    pub from: AnchorPoint,
33    pub to: AnchorPoint,
34    pub rect: Rect,
35}
36
37impl RectAnnotation {
38    pub(crate) fn new(
39        id: Id,
40        time_at_start: BigInt,
41        time_at_end: BigInt,
42        wave_from: Option<GraphicsY>,
43        wave_to: Option<GraphicsY>,
44        rect: Rect,
45        num: i32,
46    ) -> Self {
47        let name = format!("{DEFAULT_TYPE} {num}");
48        let annotation_data = AnnotationData::new(id, name, num);
49        Self {
50            annotation_data,
51            from: AnchorPoint {
52                wave: wave_from,
53                time: time_at_start,
54            },
55            to: AnchorPoint {
56                wave: wave_to,
57                time: time_at_end,
58            },
59            rect,
60        }
61    }
62    #[must_use]
63    pub fn get_id(&self) -> Id {
64        self.annotation_data.id
65    }
66
67    #[must_use]
68    pub fn get_pos(
69        &self,
70        waves: &WaveData,
71        viewport: &Viewport,
72        ctx: &DrawingContext,
73        y_offset: f32,
74    ) -> Option<Pos2> {
75        let range = waves.time_range();
76
77        let x = viewport.pixel_from_time(&self.from.time, ctx.cfg.canvas_size.x, range);
78
79        let from_y = self.from.wave.as_ref().and_then(|f| waves.get_item_y(f))?;
80        let to_y = self.to.wave.as_ref().and_then(|to| waves.get_item_y(to))?;
81
82        let min_y = (from_y + y_offset).min(to_y + y_offset);
83
84        Some((ctx.to_screen)(x, min_y))
85    }
86
87    //Find the correct y_positions for the rectangle. If the "p" value is none, it means we have a snapped value
88    //and make sure to anchor them correctly.
89    fn resolve_y_positions(&mut self, waves: &WaveData) -> (Option<f32>, Option<f32>) {
90        let mut from_y = calculate_y(self.from.wave.as_ref(), waves);
91        let mut to_y = calculate_y(self.to.wave.as_ref(), waves);
92
93        if from_y >= to_y {
94            if let Some(wave_from) = self.from.wave.as_mut()
95                && matches!(wave_from.anchor, Anchor::Top)
96            {
97                wave_from.anchor = Anchor::Bottom;
98                from_y = calculate_y(Some(wave_from), waves);
99            }
100
101            if let Some(wave_to) = self.to.wave.as_mut()
102                && matches!(wave_to.anchor, Anchor::Bottom)
103            {
104                wave_to.anchor = Anchor::Top;
105                to_y = calculate_y(Some(wave_to), waves);
106            }
107        }
108        (from_y, to_y)
109    }
110
111    /// Calculate the correct position of the rectangle on to the canvas.
112    #[allow(clippy::too_many_arguments)]
113    fn compute_rect(
114        &mut self,
115        from_y: f32,
116        to_y: f32,
117        waves: &WaveData,
118        ctx: &DrawingContext,
119        viewport_idx: usize,
120        theme: &SurferTheme,
121        y_offset: f32,
122    ) {
123        let viewport = waves.viewports[viewport_idx];
124        let range = waves.time_range();
125
126        //Update size and coloring from theme and whether it selected or not
127        self.annotation_data.stroke = Stroke::new(
128            theme.annotation_rectangle.width,
129            theme.annotation_rectangle.color,
130        );
131        // y_offset adjusts positioning whether the default timeline is shown or not.
132        let min_y = from_y.min(to_y) + y_offset;
133        let max_y = from_y.max(to_y) + y_offset;
134
135        let min_x = viewport.pixel_from_time(&self.from.time, ctx.cfg.canvas_size.x, range);
136        let max_x = viewport.pixel_from_time(&self.to.time, ctx.cfg.canvas_size.x, range);
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: &str) {
159        self.annotation_data.name = name.to_string();
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        if let (Some(from_y), Some(to_y)) = rectangle_annotation.resolve_y_positions(waves) {
256            rectangle_annotation.compute_rect(
257                from_y,
258                to_y,
259                waves,
260                ctx,
261                viewport_idx,
262                theme,
263                y_offset,
264            );
265
266            if waves.selected_annotation == Some(self.get_id()) {
267                rectangle_annotation.is_selected();
268            }
269
270            let hover_start_time = time_formatter.format(&self.from.time);
271            let hover_end_time = time_formatter.format(&self.to.time);
272
273            let group_name = waves
274                .annotation_groups
275                .iter()
276                .find(|group| group.annotations.contains(&self.get_id()))
277                .map_or(DEFAULT_GROUP_NAME, |group| &group.name);
278            let res = ui.add(rectangle_annotation).on_hover_ui(|ui| {
279                self.draw_hover_info(group_name, ui, (&hover_start_time, &hover_end_time));
280            });
281
282            if res.clicked_by(egui::PointerButton::Primary) {
283                msgs.push(Message::SetActiveViewport(viewport_idx));
284                msgs.push(Message::AnnotationClicked(
285                    Some(self.annotation_data.id),
286                    res.interact_pointer_pos(),
287                    Some(viewport_idx),
288                    Some(to_screen),
289                    Some(ctx.cfg.canvas_size.x),
290                ));
291                msgs.push(Message::ClickHandled());
292            }
293        }
294    }
295
296    fn get_comment_position(
297        &self,
298        viewport: &Viewport,
299        ctx: &DrawingContext,
300        waves: &WaveData,
301        offset: f32,
302    ) -> Option<Pos2> {
303        let range = waves.time_range();
304        let x = viewport.pixel_from_time(&self.to.time, ctx.cfg.canvas_size.x, range);
305        let y = calculate_y(self.to.wave.as_ref(), waves)? + offset;
306        Some((ctx.to_screen)(x, y))
307    }
308
309    fn get_time_info(&self, time_formatter: &TimeFormatter) -> String {
310        format!(
311            "from: {}, to: {}",
312            time_formatter.format(&self.from.time),
313            time_formatter.format(&self.to.time)
314        )
315    }
316}
317
318/// Creates an outer and inner rectangle, used to identify whether the annotation was clicked on.
319fn point_on_rect_border(p: emath::Pos2, rect: Rect, width: f32) -> (bool, Rect) {
320    let half_width: f32 = width * HITBOX_SIZE_FACTOR;
321    let outer_rect = Rect {
322        min: emath::Pos2 {
323            x: rect.min.x - half_width,
324            y: rect.min.y - half_width,
325        },
326        max: emath::Pos2 {
327            x: rect.max.x + half_width,
328            y: rect.max.y + half_width,
329        },
330    };
331    let inner_rect = Rect {
332        min: emath::Pos2 {
333            x: rect.min.x + half_width,
334            y: rect.min.y + half_width,
335        },
336        max: emath::Pos2 {
337            x: rect.max.x - half_width,
338            y: rect.max.y - half_width,
339        },
340    };
341    (
342        outer_rect.contains(p) && !inner_rect.contains(p),
343        outer_rect,
344    )
345}
346
347impl Widget for RectAnnotation {
348    fn ui(self, ui: &mut Ui) -> Response {
349        if self.is_visible() {
350            ui.painter().rect_stroke(
351                self.rect,
352                0.0,
353                self.annotation_data.stroke,
354                egui::StrokeKind::Middle,
355            );
356            // Always draw the rectangle but if we are on border we should also register clicks.
357            // This allows the click to be transferred unto the underlying panel so the rectangle is hollow
358            let (on_border, hitbox) = ui
359                .ctx()
360                .pointer_hover_pos()
361                .map_or((false, Rect::ZERO), |p| {
362                    point_on_rect_border(p, self.rect, self.annotation_data.stroke.width)
363                });
364
365            if on_border {
366                ui.interact(hitbox, self.annotation_data.id, Sense::click_and_drag())
367            } else {
368                ui.allocate_response(egui::Vec2::ZERO, Sense::empty())
369            }
370        } else {
371            let rect = self.hide_annotation(ui, self.annotation_data.stroke, self.rect.min);
372            ui.interact(rect, self.annotation_data.id, Sense::click_and_drag())
373        }
374    }
375}