1use derive_more::Display;
3use egui::{Context, Painter, PointerButton, Response, RichText, Sense, Window};
4use emath::{Align2, Pos2, Rect, RectTransform, Vec2};
5use epaint::{FontId, Stroke};
6use num::BigInt;
7use serde::Deserialize;
8
9use crate::arrow::{ArrowHeadMode, WavePoint};
10use crate::config::{SurferConfig, SurferTheme};
11use crate::graphics::{Anchor, GraphicsY};
12use crate::time::TimeFormatter;
13use crate::view::DrawingContext;
14use crate::{Message, SystemState, wave_data::WaveData};
15
16const TAN_22_5_DEGREES: f32 = 0.41421357;
18
19fn create_gesture_stroke(config: &SurferConfig, is_measure: bool) -> Stroke {
21 let line_style = if is_measure {
22 &config.theme.measure
23 } else {
24 &config.theme.gesture
25 };
26 Stroke::from(line_style)
27}
28
29#[derive(Clone, PartialEq, Copy, Display, Debug, Deserialize)]
31enum GestureKind {
32 #[display("Zoom to fit")]
33 ZoomToFit,
34 #[display("Zoom in")]
35 ZoomIn,
36 #[display("Zoom out")]
37 ZoomOut,
38 #[display("Go to end")]
39 GoToEnd,
40 #[display("Go to start")]
41 GoToStart,
42 Cancel,
43}
44
45#[derive(Clone, PartialEq, Copy, Debug, Deserialize)]
47pub struct GestureZones {
48 north: GestureKind,
49 northeast: GestureKind,
50 east: GestureKind,
51 southeast: GestureKind,
52 south: GestureKind,
53 southwest: GestureKind,
54 west: GestureKind,
55 northwest: GestureKind,
56}
57
58#[derive(Clone, PartialEq, Copy, Display, Debug, Deserialize)]
60pub enum AnnotationKind {
61 Rectangle,
62 ArrowSingleHead,
63 ArrowDoubleHead,
64}
65
66impl SystemState {
67 #[allow(clippy::too_many_arguments)]
69 fn clamp_y(
70 &self,
71 pos: Pos2,
72 max_y: f32,
73 snap_y: bool,
74 waves: &WaveData,
75 ctx: &mut DrawingContext<'_>,
76 anchor: Anchor,
77 y_offset: f32,
78 ) -> Pos2 {
79 let mut y = pos.y.clamp(waves.get_content_start(ctx), max_y);
80 if snap_y {
81 let local_y = y - y_offset;
82
83 if let Some(snapped_y) = waves
84 .get_item_at_y(local_y)
85 .and_then(|vidx| waves.items_tree.get_visible(vidx))
86 .and_then(|node| {
87 let gy = GraphicsY {
88 item: node.item_ref,
89 anchor,
90 };
91
92 waves.get_item_y(&gy)
93 })
94 {
95 y = snapped_y + y_offset;
96 }
97 }
98
99 Pos2 {
100 x: pos.x,
101 y: y.min(max_y),
102 }
103 }
104
105 #[allow(clippy::too_many_arguments)]
107 pub(crate) fn draw_mouse_gesture_widget(
108 &self,
109 egui_ctx: &Context,
110 waves: &WaveData,
111 pointer_pos_canvas: Option<Pos2>,
112 response: &Response,
113 msgs: &mut Vec<Message>,
114 ctx: &mut DrawingContext,
115 viewport_idx: usize,
116 y_offset: f32,
117 ) {
118 if let Some(mut start_location) = self.gesture_start_location {
119 if self.annotation_kind == Some(AnnotationKind::Rectangle)
120 && start_location.y
121 > (waves.get_content_height(ctx) + self.user.config.layout.waveforms_gap)
122 {
123 return;
124 }
125 if let Some(time) = &self.gesture_start_time {
127 let x_pixel = waves.viewports[viewport_idx].pixel_from_time(
128 time,
129 ctx.cfg.canvas_size.x,
130 &waves.safe_num_timestamps(),
131 );
132 start_location.x = x_pixel;
133 }
134 let modifiers = egui_ctx.input(|i| i.modifiers);
135 if response.dragged_by(PointerButton::Middle)
136 || modifiers.command && response.dragged_by(PointerButton::Primary)
137 || self.annotation_kind.is_some() && response.dragged_by(PointerButton::Primary)
138 {
139 self.start_dragging(
140 pointer_pos_canvas,
141 start_location,
142 ctx,
143 egui_ctx,
144 response,
145 waves,
146 viewport_idx,
147 y_offset,
148 );
149 }
150
151 if response.drag_stopped_by(PointerButton::Middle)
152 || modifiers.command && response.drag_stopped_by(PointerButton::Primary)
153 || self.annotation_kind.is_some()
154 && response.drag_stopped_by(PointerButton::Primary)
155 {
156 let frame_width = response.rect.width();
157 self.stop_dragging(
158 pointer_pos_canvas,
159 start_location,
160 msgs,
161 viewport_idx,
162 waves,
163 frame_width,
164 ctx,
165 egui_ctx,
166 y_offset,
167 );
168 }
169 }
170 }
171
172 #[allow(clippy::too_many_arguments)]
173 fn stop_dragging(
174 &self,
175 pointer_pos_canvas: Option<Pos2>,
176 start_location: Pos2,
177 msgs: &mut Vec<Message>,
178 viewport_idx: usize,
179 waves: &WaveData,
180 frame_width: f32,
181 ctx: &mut DrawingContext<'_>,
182 ui: &Context,
183 y_offset: f32,
184 ) {
185 let num_timestamps = waves.safe_num_timestamps();
186 let Some(end_location) = pointer_pos_canvas else {
187 return;
188 };
189 let distance = end_location - start_location;
190 if distance.length_sq() >= self.user.config.gesture.deadzone {
191 match self.annotation_kind {
192 Some(AnnotationKind::Rectangle) => {
193 self.create_rectangle(
194 end_location,
195 start_location,
196 msgs,
197 viewport_idx,
198 waves,
199 &num_timestamps,
200 frame_width,
201 ctx,
202 ui,
203 y_offset,
204 );
205 }
206 Some(AnnotationKind::ArrowSingleHead | AnnotationKind::ArrowDoubleHead) => {
207 self.create_arrow(
208 end_location,
209 start_location,
210 msgs,
211 viewport_idx,
212 waves,
213 &num_timestamps,
214 frame_width,
215 ctx,
216 y_offset,
217 );
218 }
219 _ => {
220 match gesture_type(self.user.config.gesture.mapping, distance) {
221 GestureKind::ZoomToFit => {
222 msgs.push(Message::ZoomToFit { viewport_idx });
223 }
224 GestureKind::ZoomIn => {
225 let (min_x, max_x) = if end_location.x < start_location.x {
226 (end_location.x, start_location.x)
227 } else {
228 (start_location.x, end_location.x)
229 };
230 msgs.push(Message::ZoomToRange {
231 start: waves.viewports[viewport_idx].as_time_bigint(
233 min_x,
234 frame_width,
235 &num_timestamps,
236 ),
237 end: waves.viewports[viewport_idx].as_time_bigint(
238 max_x,
239 frame_width,
240 &num_timestamps,
241 ),
242 viewport_idx,
243 });
244 }
245 GestureKind::GoToStart => {
246 msgs.push(Message::GoToStart { viewport_idx });
247 }
248 GestureKind::GoToEnd => {
249 msgs.push(Message::GoToEnd { viewport_idx });
250 }
251 GestureKind::ZoomOut => {
252 msgs.push(Message::CanvasZoom {
253 mouse_ptr: None,
254 delta: 2.0,
255 viewport_idx,
256 });
257 }
258 GestureKind::Cancel => {}
259 }
260 }
261 }
262 }
263 msgs.push(Message::SetMouseGestureDragStart(None, None));
264 msgs.push(Message::SetMouseGestureAnnotation(None));
265 }
266
267 #[allow(clippy::too_many_arguments)]
268 fn start_dragging(
269 &self,
270 pointer_pos_canvas: Option<Pos2>,
271 start_location: Pos2,
272 ctx: &mut DrawingContext<'_>,
273 ui: &Context,
274 response: &Response,
275 waves: &WaveData,
276 viewport_idx: usize,
277 y_offset: f32,
278 ) {
279 let Some(current_location) = pointer_pos_canvas else {
280 return;
281 };
282 let distance = current_location - start_location;
283 if distance.length_sq() >= self.user.config.gesture.deadzone {
284 match self.annotation_kind {
285 Some(AnnotationKind::Rectangle) => {
286 self.draw_gesture_rectangle(
287 start_location,
288 waves,
289 ui,
290 current_location,
291 ctx,
292 y_offset,
293 );
294 }
295 Some(AnnotationKind::ArrowSingleHead | AnnotationKind::ArrowDoubleHead) => {
296 self.draw_arrow_line(start_location, current_location, "Add arrow", true, ctx);
297 }
298 _ => match gesture_type(self.user.config.gesture.mapping, distance) {
299 GestureKind::ZoomToFit => self.draw_gesture_line(
300 start_location,
301 current_location,
302 "Zoom to fit",
303 true,
304 ctx,
305 ),
306 GestureKind::ZoomIn => self.draw_zoom_in_gesture(
307 start_location,
308 current_location,
309 response,
310 ctx,
311 waves,
312 viewport_idx,
313 false,
314 ),
315
316 GestureKind::GoToStart => self.draw_gesture_line(
317 start_location,
318 current_location,
319 "Go to start",
320 true,
321 ctx,
322 ),
323 GestureKind::GoToEnd => {
324 self.draw_gesture_line(
325 start_location,
326 current_location,
327 "Go to end",
328 true,
329 ctx,
330 );
331 }
332 GestureKind::ZoomOut => {
333 self.draw_gesture_line(
334 start_location,
335 current_location,
336 "Zoom out",
337 true,
338 ctx,
339 );
340 }
341 GestureKind::Cancel => {
342 self.draw_gesture_line(
343 start_location,
344 current_location,
345 "Cancel",
346 false,
347 ctx,
348 );
349 }
350 },
351 }
352 } else if self.annotation_kind.is_none() {
353 draw_gesture_help(
354 &self.user.config,
355 response,
356 ctx.painter,
357 Some(start_location),
358 true,
359 );
360 }
361 }
362
363 fn draw_gesture_rectangle(
364 &self,
365 start_location: Pos2,
366 waves: &WaveData,
367 ui: &Context,
368 current_location: Pos2,
369 ctx: &mut DrawingContext,
370 y_offset: f32,
371 ) {
372 let modifiers = ui.input(|i| i.modifiers);
373 let max_y = waves.get_content_height(ctx);
374 let current_anchor = {
375 if current_location.y > start_location.y {
376 Anchor::Bottom
377 } else {
378 Anchor::Top
379 }
380 };
381 let start_anchor = {
382 if start_location.y < current_location.y {
383 Anchor::Top
384 } else {
385 Anchor::Bottom
386 }
387 };
388 let end = self.clamp_y(
389 current_location,
390 max_y,
391 !modifiers.shift,
392 waves,
393 ctx,
394 current_anchor,
395 y_offset,
396 );
397 let start = self.clamp_y(
398 start_location,
399 max_y,
400 !modifiers.shift,
401 waves,
402 ctx,
403 start_anchor,
404 y_offset,
405 );
406 let color = self.user.config.theme.annotation_rectangle.color;
407 let stroke = Stroke {
408 color,
409 width: self.user.config.theme.annotation_rectangle.width,
410 };
411
412 let start_pos = (ctx.to_screen)(start.x, start.y);
413 let end_pos = (ctx.to_screen)(end.x, end.y);
414
415 let temp_rect = emath::Rect::from_two_pos(start_pos, end_pos);
416
417 ctx.painter
418 .rect_stroke(temp_rect, 0.0, stroke, egui::StrokeKind::Middle);
419 }
420
421 #[allow(clippy::too_many_arguments)]
422 fn create_rectangle(
423 &self,
424 end_location: Pos2,
425 start_location: Pos2,
426 msgs: &mut Vec<Message>,
427 viewport_idx: usize,
428 waves: &WaveData,
429 num_timestamps: &BigInt,
430 frame_width: f32,
431 ctx: &mut DrawingContext<'_>,
432 ui: &Context,
433 y_offset: f32,
434 ) {
435 let modifiers = ui.input(|i| i.modifiers);
436 let max_y = waves.get_content_height(ctx);
437
438 let end_anchor = if end_location.y > start_location.y {
439 Anchor::Bottom
440 } else {
441 Anchor::Top
442 };
443
444 let start_anchor = if start_location.y < end_location.y {
445 Anchor::Top
446 } else {
447 Anchor::Bottom
448 };
449
450 let end = self.clamp_y(
451 end_location,
452 max_y,
453 !modifiers.shift,
454 waves,
455 ctx,
456 end_anchor,
457 y_offset,
458 );
459
460 let start = self.clamp_y(
461 start_location,
462 max_y,
463 !modifiers.shift,
464 waves,
465 ctx,
466 start_anchor,
467 y_offset,
468 );
469
470 let rect = emath::Rect::from_two_pos(start, end);
471
472 let viewport = &waves.viewports[viewport_idx];
473
474 let t1 = viewport.as_time_bigint(start_location.x, frame_width, num_timestamps);
475 let t2 = viewport.as_time_bigint(end_location.x, frame_width, num_timestamps);
476
477 let (time_start, time_end) = (t1.clone().min(t2.clone()), t1.max(t2));
478
479 let get_anchored_y = |y: f32, anchor: Anchor| {
480 waves
481 .get_item_at_y(y)
482 .and_then(|vidx| waves.items_tree.get_visible(vidx))
483 .map(|node| GraphicsY {
484 item: node.item_ref,
485 anchor,
486 })
487 };
488
489 let get_percentual_y = |lookup_y: f32, scale_y: f32| {
490 waves
491 .get_item_at_y(lookup_y)
492 .and_then(|vidx| waves.items_tree.get_visible(vidx))
493 .map(|node| {
494 let item = node.item_ref;
495 let p = waves.get_item_y_scale(item, scale_y);
496
497 GraphicsY {
498 item,
499 anchor: Anchor::Percentual(p.unwrap_or(0.)),
500 }
501 })
502 };
503
504 let (wave_from, wave_to) = if modifiers.shift {
505 let from =
506 get_percentual_y(start.y.min(end.y) - y_offset, start.y.min(end.y) - y_offset);
507
508 let to = get_percentual_y(
509 end.y.max(start.y) - y_offset - self.user.config.layout.waveforms_gap * 2.,
510 end.y.max(start.y) - y_offset,
511 );
512
513 (from, to)
514 } else {
515 let y_from = start.y.min(end.y);
516 let y_to = start.y.max(end.y);
517
518 let from = get_anchored_y(y_from - y_offset, Anchor::Top);
519
520 let mut adjusted_y = y_to - y_offset;
521 if y_to > waves.get_content_start(ctx) {
522 adjusted_y -= self.user.config.layout.waveforms_gap * 2.0;
523 }
524
525 let to = get_anchored_y(adjusted_y, Anchor::Bottom);
526
527 (from, to)
528 };
529
530 msgs.push(Message::RectangleAdded {
531 time_at_start: time_start,
532 time_at_end: time_end,
533 wave_from,
534 wave_to,
535 rect,
536 });
537 }
538
539 #[allow(clippy::too_many_arguments)]
540 fn create_arrow(
541 &self,
542 end_location: Pos2,
543 start_location: Pos2,
544 msgs: &mut Vec<Message>,
545 viewport_idx: usize,
546 waves: &WaveData,
547 num_timestamps: &BigInt,
548 frame_width: f32,
549 ctx: &mut DrawingContext<'_>,
550 offset: f32,
551 ) {
552 let start_pos = (ctx.to_screen)(start_location.x, start_location.y);
553 let end_pos = (ctx.to_screen)(end_location.x, end_location.y);
554
555 let time_from: BigInt = waves.viewports[viewport_idx].as_time_bigint(
556 start_location.x,
557 frame_width,
558 num_timestamps,
559 );
560
561 let snap_pos = Some(Pos2::new(end_location.x, end_location.y - offset));
562
563 let time_to: BigInt = self
564 .snap_to_edge(snap_pos, waves, frame_width, viewport_idx)
565 .unwrap_or_else(|| {
566 waves.viewports[viewport_idx].as_time_bigint(
567 end_location.x,
568 frame_width,
569 num_timestamps,
570 )
571 });
572
573 let attached_item_to = waves.item_ref_at_canvas_y(end_location.y - offset);
574 let attached_item_from = waves.item_ref_at_canvas_y(start_location.y - offset);
575
576 let mut head_mode = ArrowHeadMode::End;
577
578 if self.annotation_kind == Some(AnnotationKind::ArrowDoubleHead) {
579 head_mode = ArrowHeadMode::Double;
580 }
581
582 let wave_point_from = WavePoint {
583 time: time_from.clone(),
584 attached_item: attached_item_from,
585 screen_pos: start_pos,
586 };
587
588 let wave_point_to = WavePoint {
589 time: time_to.clone(),
590 attached_item: attached_item_to,
591 screen_pos: end_pos,
592 };
593
594 if attached_item_to.is_some() {
595 msgs.push(Message::ArrowAdded {
596 wave_point_from,
597 wave_point_to,
598 head_mode,
599 });
600 }
601 }
602
603 fn draw_gesture_line(
605 &self,
606 start: Pos2,
607 end: Pos2,
608 text: &str,
609 active: bool,
610 ctx: &mut DrawingContext,
611 ) {
612 let color = if active {
613 self.user.config.theme.gesture.color
614 } else {
615 self.user.config.theme.gesture.color.gamma_multiply(0.3)
616 };
617 let stroke = Stroke {
618 color,
619 width: self.user.config.theme.gesture.width,
620 };
621 ctx.painter.line_segment(
622 [
623 (ctx.to_screen)(end.x, end.y),
624 (ctx.to_screen)(start.x, start.y),
625 ],
626 stroke,
627 );
628 draw_gesture_text(
629 ctx,
630 (ctx.to_screen)(end.x, end.y),
631 text.to_string(),
632 &self.user.config.theme,
633 );
634 }
635
636 fn draw_arrow_line(
637 &self,
638 start: Pos2,
639 end: Pos2,
640 text: &str,
641 active: bool,
642 ctx: &mut DrawingContext,
643 ) {
644 let color = if active {
645 self.user.config.theme.annotation_arrow.color
646 } else {
647 self.user.config.theme.gesture.color.gamma_multiply(0.3)
648 };
649 let stroke = Stroke {
650 color,
651 width: self.user.config.theme.gesture.width,
652 };
653 ctx.painter.line_segment(
654 [
655 (ctx.to_screen)(end.x, end.y),
656 (ctx.to_screen)(start.x, start.y),
657 ],
658 stroke,
659 );
660 draw_gesture_text(
661 ctx,
662 (ctx.to_screen)(end.x, end.y),
663 text.to_string(),
664 &self.user.config.theme,
665 );
666 }
667
668 #[allow(clippy::too_many_arguments)]
670 fn draw_zoom_in_gesture(
671 &self,
672 start_location: Pos2,
673 current_location: Pos2,
674 response: &Response,
675 ctx: &mut DrawingContext<'_>,
676 waves: &WaveData,
677 viewport_idx: usize,
678 measure: bool,
679 ) {
680 let stroke = create_gesture_stroke(&self.user.config, measure);
681 let height = response.rect.height();
682 let width = response.rect.width();
683 let segments = [
684 ((start_location.x, 0.0), (start_location.x, height)),
685 ((current_location.x, 0.0), (current_location.x, height)),
686 (
687 (start_location.x, start_location.y),
688 (current_location.x, start_location.y),
689 ),
690 ];
691 for (start, end) in segments {
692 ctx.painter.line_segment(
693 [
694 (ctx.to_screen)(start.0, start.1),
695 (ctx.to_screen)(end.0, end.1),
696 ],
697 stroke,
698 );
699 }
700 let (minx, maxx) = if measure || current_location.x > start_location.x {
701 (start_location.x, current_location.x)
702 } else {
703 (current_location.x, start_location.x)
704 };
705 let num_timestamps = waves.safe_num_timestamps();
706 let start_time = waves.viewports[viewport_idx].as_time_bigint(minx, width, &num_timestamps);
707 let end_time = waves.viewports[viewport_idx].as_time_bigint(maxx, width, &num_timestamps);
708 let diff_time = &end_time - &start_time;
709 let time_formatter = TimeFormatter::new(
710 &waves.inner.metadata().timescale,
711 &self.user.wanted_timeunit,
712 &self.get_time_format(),
713 );
714 let start_time_str = time_formatter.format(&start_time);
715 let end_time_str = time_formatter.format(&end_time);
716 let diff_time_str = time_formatter.format(&diff_time);
717 draw_gesture_text(
718 ctx,
719 (ctx.to_screen)(current_location.x, current_location.y),
720 if measure {
721 format!("{start_time_str} to {end_time_str}\nΔ = {diff_time_str}")
722 } else {
723 format!("Zoom in: {diff_time_str}\n{start_time_str} to {end_time_str}")
724 },
725 &self.user.config.theme,
726 );
727 }
728
729 pub(crate) fn mouse_gesture_help(&self, ctx: &Context, msgs: &mut Vec<Message>) {
731 let mut open = true;
732 Window::new("Mouse gestures")
733 .open(&mut open)
734 .collapsible(false)
735 .resizable(true)
736 .show(ctx, |ui| {
737 ui.vertical_centered(|ui| {
738 ui.label(RichText::new(
739 "Press middle mouse button (or ctrl+primary mouse button) and drag",
740 ));
741 ui.add_space(20.);
742 let (response, painter) = ui.allocate_painter(
743 Vec2 {
744 x: self.user.config.gesture.size,
745 y: self.user.config.gesture.size,
746 },
747 Sense::empty(),
748 );
749 draw_gesture_help(&self.user.config, &response, &painter, None, false);
750 ui.add_space(10.);
751 ui.separator();
752 if ui.button("Close").clicked() {
753 msgs.push(Message::SetGestureHelpVisible(false));
754 }
755 });
756 });
757 if !open {
758 msgs.push(Message::SetGestureHelpVisible(false));
759 }
760 }
761
762 #[allow(clippy::too_many_arguments)]
763 pub(crate) fn draw_measure_widget(
764 &self,
765 egui_ctx: &Context,
766 waves: &WaveData,
767 pointer_pos_canvas: Option<Pos2>,
768 response: &Response,
769 msgs: &mut Vec<Message>,
770 ctx: &mut DrawingContext,
771 viewport_idx: usize,
772 ) {
773 if let Some(start_location) = self.measure_start_location {
774 let modifiers = egui_ctx.input(|i| i.modifiers);
775 if !modifiers.command
776 && response.dragged_by(PointerButton::Primary)
777 && self.do_measure(&modifiers)
778 && let Some(current_location) = pointer_pos_canvas
779 {
780 self.draw_zoom_in_gesture(
781 start_location,
782 current_location,
783 response,
784 ctx,
785 waves,
786 viewport_idx,
787 true,
788 );
789 }
790 if response.drag_stopped_by(PointerButton::Primary) {
791 msgs.push(Message::SetMeasureDragStart(None));
792 }
793 }
794 }
795}
796
797fn draw_gesture_help(
799 config: &SurferConfig,
800 response: &Response,
801 painter: &Painter,
802 midpoint: Option<Pos2>,
803 draw_bg: bool,
804) {
805 let frame_size = response.rect.size();
806 let (midx, midy, deltax, deltay) = if let Some(midpoint) = midpoint {
808 let halfsize = config.gesture.size * 0.5;
809 (midpoint.x, midpoint.y, halfsize, halfsize)
810 } else {
811 let halfwidth = frame_size.x * 0.5;
812 let halfheight = frame_size.y * 0.5;
813 (halfwidth, halfheight, halfwidth, halfheight)
814 };
815
816 let container_rect = Rect::from_min_size(Pos2::ZERO, frame_size);
817 let to_screen = &|x, y| {
818 RectTransform::from_to(container_rect, response.rect).transform_pos(Pos2::new(x, y))
819 };
820 let stroke = Stroke::from(&config.theme.gesture);
821 let tan225deltax = TAN_22_5_DEGREES * deltax;
822 let tan225deltay = TAN_22_5_DEGREES * deltay;
823 let left = midx - deltax;
824 let right = midx + deltax;
825 let top = midy - deltay;
826 let bottom = midy + deltay;
827 if draw_bg {
829 let bg_radius = config.gesture.background_radius * deltax;
830 painter.circle_filled(
831 to_screen(midx, midy),
832 bg_radius,
833 config
834 .theme
835 .canvas_colors
836 .background
837 .gamma_multiply(config.gesture.background_gamma),
838 );
839 }
840 let segments = [
842 ((left, midy + tan225deltax), (right, midy - tan225deltax)),
843 ((left, midy - tan225deltax), (right, midy + tan225deltax)),
844 ((midx + tan225deltay, top), (midx - tan225deltay, bottom)),
845 ((midx - tan225deltay, top), (midx + tan225deltay, bottom)),
846 ];
847 for (start, end) in segments {
848 painter.line_segment(
849 [to_screen(start.0, start.1), to_screen(end.0, end.1)],
850 stroke,
851 );
852 }
853
854 let halfwaytexty_upper = top + (deltay - tan225deltax) * 0.5;
855 let halfwaytexty_lower = bottom - (deltay - tan225deltax) * 0.5;
856
857 let directions = [
859 (left, midy, Align2::LEFT_CENTER, config.gesture.mapping.west),
860 (
861 right,
862 midy,
863 Align2::RIGHT_CENTER,
864 config.gesture.mapping.east,
865 ),
866 (
867 left,
868 halfwaytexty_upper,
869 Align2::LEFT_CENTER,
870 config.gesture.mapping.northwest,
871 ),
872 (
873 right,
874 halfwaytexty_upper,
875 Align2::RIGHT_CENTER,
876 config.gesture.mapping.northeast,
877 ),
878 (midx, top, Align2::CENTER_TOP, config.gesture.mapping.north),
879 (
880 left,
881 halfwaytexty_lower,
882 Align2::LEFT_CENTER,
883 config.gesture.mapping.southwest,
884 ),
885 (
886 right,
887 halfwaytexty_lower,
888 Align2::RIGHT_CENTER,
889 config.gesture.mapping.southeast,
890 ),
891 (
892 midx,
893 bottom,
894 Align2::CENTER_BOTTOM,
895 config.gesture.mapping.south,
896 ),
897 ];
898
899 for (x, y, align, text) in directions {
900 painter.text(
901 to_screen(x, y),
902 align,
903 text,
904 FontId::default(),
905 config.theme.foreground,
906 );
907 }
908}
909
910fn gesture_type(zones: GestureZones, delta: Vec2) -> GestureKind {
912 let tan225x = TAN_22_5_DEGREES * delta.x;
913 let tan225y = TAN_22_5_DEGREES * delta.y;
914 if delta.x < 0.0 {
915 if delta.y.abs() < -tan225x {
916 zones.west
918 } else if delta.y < 0.0 && delta.x < tan225y {
919 zones.northwest
921 } else if delta.y > 0.0 && delta.x < -tan225y {
922 zones.southwest
924 } else if delta.y < 0.0 {
925 zones.north
927 } else {
928 zones.south
930 }
931 } else if tan225x > delta.y.abs() {
932 zones.east
934 } else if delta.y < 0.0 && delta.x > -tan225y {
935 zones.northeast
937 } else if delta.y > 0.0 && delta.x > tan225y {
938 zones.southeast
940 } else if delta.y < 0.0 {
941 zones.north
943 } else {
944 zones.south
946 }
947}
948
949fn draw_gesture_text(
950 ctx: &mut DrawingContext,
951 pos: Pos2,
952 text: impl ToString,
953 theme: &SurferTheme,
954) {
955 let pos = pos + Vec2::new(10.0, -10.0);
957
958 let galley = ctx
959 .painter
960 .layout_no_wrap(text.to_string(), FontId::default(), theme.foreground);
961
962 ctx.painter.rect(
963 galley.rect.translate(pos.to_vec2()).expand(3.0),
964 2.0,
965 theme.primary_ui_color.background,
966 Stroke::default(),
967 epaint::StrokeKind::Inside,
968 );
969
970 ctx.painter
971 .galley(pos, galley, theme.primary_ui_color.foreground);
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977
978 fn default_zones() -> GestureZones {
979 GestureZones {
980 north: GestureKind::ZoomToFit,
981 northeast: GestureKind::ZoomIn,
982 east: GestureKind::GoToEnd,
983 southeast: GestureKind::ZoomOut,
984 south: GestureKind::Cancel,
985 southwest: GestureKind::ZoomOut,
986 west: GestureKind::GoToStart,
987 northwest: GestureKind::ZoomIn,
988 }
989 }
990
991 #[test]
992 fn gesture_type_cardinal_directions() {
993 let zones = default_zones();
994
995 assert_eq!(
997 gesture_type(zones, Vec2::new(100.0, 0.0)),
998 GestureKind::GoToEnd
999 ); assert_eq!(
1001 gesture_type(zones, Vec2::new(-100.0, 0.0)),
1002 GestureKind::GoToStart
1003 ); assert_eq!(
1005 gesture_type(zones, Vec2::new(0.0, -100.0)),
1006 GestureKind::ZoomToFit
1007 ); assert_eq!(
1009 gesture_type(zones, Vec2::new(0.0, 100.0)),
1010 GestureKind::Cancel
1011 ); }
1013
1014 #[test]
1015 fn gesture_type_diagonal_directions() {
1016 let zones = default_zones();
1017
1018 assert_eq!(
1020 gesture_type(zones, Vec2::new(100.0, -100.0)),
1021 GestureKind::ZoomIn
1022 ); assert_eq!(
1024 gesture_type(zones, Vec2::new(100.0, 100.0)),
1025 GestureKind::ZoomOut
1026 ); assert_eq!(
1028 gesture_type(zones, Vec2::new(-100.0, 100.0)),
1029 GestureKind::ZoomOut
1030 ); assert_eq!(
1032 gesture_type(zones, Vec2::new(-100.0, -100.0)),
1033 GestureKind::ZoomIn
1034 ); }
1036
1037 #[test]
1038 fn gesture_type_boundary_zones() {
1039 let zones = default_zones();
1040
1041 assert_eq!(
1044 gesture_type(zones, Vec2::new(100.0, 40.0)),
1045 GestureKind::GoToEnd
1046 ); assert_eq!(
1048 gesture_type(zones, Vec2::new(100.0, -40.0)),
1049 GestureKind::GoToEnd
1050 ); assert_eq!(
1054 gesture_type(zones, Vec2::new(100.0, 50.0)),
1055 GestureKind::ZoomOut
1056 ); assert_eq!(
1058 gesture_type(zones, Vec2::new(100.0, -50.0)),
1059 GestureKind::ZoomIn
1060 ); }
1062
1063 #[test]
1064 fn gesture_type_west_boundary_zones() {
1065 let zones = default_zones();
1066
1067 assert_eq!(
1069 gesture_type(zones, Vec2::new(-100.0, 40.0)),
1070 GestureKind::GoToStart
1071 ); assert_eq!(
1073 gesture_type(zones, Vec2::new(-100.0, -40.0)),
1074 GestureKind::GoToStart
1075 ); assert_eq!(
1079 gesture_type(zones, Vec2::new(-100.0, 50.0)),
1080 GestureKind::ZoomOut
1081 ); assert_eq!(
1083 gesture_type(zones, Vec2::new(-100.0, -50.0)),
1084 GestureKind::ZoomIn
1085 ); }
1087}