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 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 #[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 self.annotation_data.stroke = Stroke::new(
127 theme.annotation_rectangle.width,
128 theme.annotation_rectangle.color,
129 );
130 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
323fn 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 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}