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