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