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 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 #[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 self.annotation_data.stroke = Stroke::new(
135 theme.annotation_rectangle.width,
136 theme.annotation_rectangle.color,
137 );
138 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
341fn 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 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}