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