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 serde::Deserialize;
7
8use crate::config::{SurferConfig, SurferTheme};
9use crate::time::time_string;
10use crate::view::DrawingContext;
11use crate::{wave_data::WaveData, Message, SystemState};
12
13#[derive(Clone, PartialEq, Copy, Display, Debug, Deserialize)]
15enum GestureKind {
16 #[display("Zoom to fit")]
17 ZoomToFit,
18 #[display("Zoom in")]
19 ZoomIn,
20 #[display("Zoom out")]
21 ZoomOut,
22 #[display("Go to end")]
23 GoToEnd,
24 #[display("Go to start")]
25 GoToStart,
26 Cancel,
27}
28
29#[derive(Clone, PartialEq, Copy, Debug, Deserialize)]
31pub struct GestureZones {
32 north: GestureKind,
33 northeast: GestureKind,
34 east: GestureKind,
35 southeast: GestureKind,
36 south: GestureKind,
37 southwest: GestureKind,
38 west: GestureKind,
39 northwest: GestureKind,
40}
41
42impl SystemState {
43 #[allow(clippy::too_many_arguments)]
45 pub fn draw_mouse_gesture_widget(
46 &self,
47 egui_ctx: &egui::Context,
48 waves: &WaveData,
49 pointer_pos_canvas: Option<Pos2>,
50 response: &Response,
51 msgs: &mut Vec<Message>,
52 ctx: &mut DrawingContext,
53 viewport_idx: usize,
54 ) {
55 if let Some(start_location) = self.gesture_start_location {
56 let modifiers = egui_ctx.input(|i| i.modifiers);
57 if response.dragged_by(PointerButton::Middle)
58 || modifiers.command && response.dragged_by(PointerButton::Primary)
59 {
60 self.start_dragging(
61 pointer_pos_canvas,
62 start_location,
63 ctx,
64 response,
65 waves,
66 viewport_idx,
67 );
68 }
69
70 if response.drag_stopped_by(PointerButton::Middle)
71 || modifiers.command && response.drag_stopped_by(PointerButton::Primary)
72 {
73 let frame_width = response.rect.width();
74 self.stop_dragging(
75 pointer_pos_canvas,
76 start_location,
77 msgs,
78 viewport_idx,
79 waves,
80 frame_width,
81 );
82 }
83 }
84 }
85
86 fn stop_dragging(
87 &self,
88 pointer_pos_canvas: Option<Pos2>,
89 start_location: Pos2,
90 msgs: &mut Vec<Message>,
91 viewport_idx: usize,
92 waves: &WaveData,
93 frame_width: f32,
94 ) {
95 let num_timestamps = waves.num_timestamps().unwrap_or(1.into());
96 let end_location = pointer_pos_canvas.unwrap();
97 let distance = end_location - start_location;
98 if distance.length_sq() >= self.user.config.gesture.deadzone {
99 match gesture_type(&self.user.config.gesture.mapping, distance) {
100 GestureKind::ZoomToFit => {
101 msgs.push(Message::ZoomToFit { viewport_idx });
102 }
103 GestureKind::ZoomIn => {
104 let (minx, maxx) = if end_location.x < start_location.x {
105 (end_location.x, start_location.x)
106 } else {
107 (start_location.x, end_location.x)
108 };
109 msgs.push(Message::ZoomToRange {
110 start: waves.viewports[viewport_idx].as_time_bigint(
112 minx,
113 frame_width,
114 &num_timestamps,
115 ),
116 end: waves.viewports[viewport_idx].as_time_bigint(
117 maxx,
118 frame_width,
119 &num_timestamps,
120 ),
121 viewport_idx,
122 });
123 }
124 GestureKind::GoToStart => {
125 msgs.push(Message::GoToStart { viewport_idx });
126 }
127 GestureKind::GoToEnd => {
128 msgs.push(Message::GoToEnd { viewport_idx });
129 }
130 GestureKind::ZoomOut => {
131 msgs.push(Message::CanvasZoom {
132 mouse_ptr: None,
133 delta: 2.0,
134 viewport_idx,
135 });
136 }
137 GestureKind::Cancel => {}
138 }
139 }
140 msgs.push(Message::SetMouseGestureDragStart(None));
141 }
142
143 fn start_dragging(
144 &self,
145 pointer_pos_canvas: Option<Pos2>,
146 start_location: Pos2,
147 ctx: &mut DrawingContext<'_>,
148 response: &Response,
149 waves: &WaveData,
150 viewport_idx: usize,
151 ) {
152 let current_location = pointer_pos_canvas.unwrap();
153 let distance = current_location - start_location;
154 if distance.length_sq() >= self.user.config.gesture.deadzone {
155 match gesture_type(&self.user.config.gesture.mapping, distance) {
156 GestureKind::ZoomToFit => self.draw_gesture_line(
157 start_location,
158 current_location,
159 "Zoom to fit",
160 true,
161 ctx,
162 ),
163 GestureKind::ZoomIn => self.draw_zoom_in_gesture(
164 start_location,
165 current_location,
166 response,
167 ctx,
168 waves,
169 viewport_idx,
170 false,
171 ),
172
173 GestureKind::GoToStart => self.draw_gesture_line(
174 start_location,
175 current_location,
176 "Go to start",
177 true,
178 ctx,
179 ),
180 GestureKind::GoToEnd => {
181 self.draw_gesture_line(start_location, current_location, "Go to end", true, ctx)
182 }
183 GestureKind::ZoomOut => {
184 self.draw_gesture_line(start_location, current_location, "Zoom out", true, ctx)
185 }
186 GestureKind::Cancel => {
187 self.draw_gesture_line(start_location, current_location, "Cancel", false, ctx)
188 }
189 }
190 } else {
191 draw_gesture_help(
192 &self.user.config,
193 response,
194 ctx.painter,
195 Some(start_location),
196 true,
197 );
198 }
199 }
200
201 fn draw_gesture_line(
203 &self,
204 start: Pos2,
205 end: Pos2,
206 text: &str,
207 active: bool,
208 ctx: &mut DrawingContext,
209 ) {
210 let stroke = Stroke {
211 color: if active {
212 self.user.config.theme.gesture.color
213 } else {
214 self.user.config.theme.gesture.color.gamma_multiply(0.3)
215 },
216 width: self.user.config.theme.gesture.width,
217 };
218 ctx.painter.line_segment(
219 [
220 (ctx.to_screen)(end.x, end.y),
221 (ctx.to_screen)(start.x, start.y),
222 ],
223 stroke,
224 );
225 draw_gesture_text(
226 ctx,
227 (ctx.to_screen)(end.x, end.y),
228 text.to_string(),
229 &self.user.config.theme,
230 );
231 }
232
233 #[allow(clippy::too_many_arguments)]
235 fn draw_zoom_in_gesture(
236 &self,
237 start_location: Pos2,
238 current_location: Pos2,
239 response: &Response,
240 ctx: &mut DrawingContext<'_>,
241 waves: &WaveData,
242 viewport_idx: usize,
243 measure: bool,
244 ) {
245 let stroke = if measure {
246 Stroke {
247 color: self.user.config.theme.measure.color,
248 width: self.user.config.theme.measure.width,
249 }
250 } else {
251 Stroke {
252 color: self.user.config.theme.gesture.color,
253 width: self.user.config.theme.gesture.width,
254 }
255 };
256 let height = response.rect.height();
257 let width = response.rect.width();
258 ctx.painter.line_segment(
259 [
260 (ctx.to_screen)(start_location.x, 0.0),
261 (ctx.to_screen)(start_location.x, height),
262 ],
263 stroke,
264 );
265 ctx.painter.line_segment(
266 [
267 (ctx.to_screen)(current_location.x, 0.0),
268 (ctx.to_screen)(current_location.x, height),
269 ],
270 stroke,
271 );
272 ctx.painter.line_segment(
273 [
274 (ctx.to_screen)(start_location.x, start_location.y),
275 (ctx.to_screen)(current_location.x, start_location.y),
276 ],
277 stroke,
278 );
279
280 let (minx, maxx) = if measure || current_location.x > start_location.x {
281 (start_location.x, current_location.x)
282 } else {
283 (current_location.x, start_location.x)
284 };
285 let num_timestamps = waves.num_timestamps().unwrap_or(1.into());
286 let start_time = waves.viewports[viewport_idx].as_time_bigint(minx, width, &num_timestamps);
287 let end_time = waves.viewports[viewport_idx].as_time_bigint(maxx, width, &num_timestamps);
288 let diff_time = &end_time - &start_time;
289 let timescale = &waves.inner.metadata().timescale;
290 let time_format = &self.get_time_format();
291 let start_time_str = time_string(
292 &start_time,
293 timescale,
294 &self.user.wanted_timeunit,
295 time_format,
296 );
297 let end_time_str = time_string(
298 &end_time,
299 timescale,
300 &self.user.wanted_timeunit,
301 time_format,
302 );
303 let diff_time_str = time_string(
304 &diff_time,
305 timescale,
306 &self.user.wanted_timeunit,
307 time_format,
308 );
309 draw_gesture_text(
310 ctx,
311 (ctx.to_screen)(current_location.x, current_location.y),
312 if measure {
313 format!("{start_time_str} to {end_time_str}\nΔ = {diff_time_str}")
314 } else {
315 format!("Zoom in: {diff_time_str}\n{start_time_str} to {end_time_str}")
316 },
317 &self.user.config.theme,
318 );
319 }
320
321 pub fn mouse_gesture_help(&self, ctx: &Context, msgs: &mut Vec<Message>) {
323 let mut open = true;
324 Window::new("Mouse gestures")
325 .open(&mut open)
326 .collapsible(false)
327 .resizable(true)
328 .show(ctx, |ui| {
329 ui.vertical_centered(|ui| {
330 ui.label(RichText::new(
331 "Press middle mouse button (or ctrl+primary mouse button) and drag",
332 ));
333 ui.add_space(20.);
334 let (response, painter) = ui.allocate_painter(
335 Vec2 {
336 x: self.user.config.gesture.size,
337 y: self.user.config.gesture.size,
338 },
339 Sense::empty(),
340 );
341 draw_gesture_help(&self.user.config, &response, &painter, None, false);
342 ui.add_space(10.);
343 ui.separator();
344 if ui.button("Close").clicked() {
345 msgs.push(Message::SetGestureHelpVisible(false));
346 }
347 });
348 });
349 if !open {
350 msgs.push(Message::SetGestureHelpVisible(false));
351 }
352 }
353
354 #[allow(clippy::too_many_arguments)]
355 pub fn draw_measure_widget(
356 &self,
357 egui_ctx: &egui::Context,
358 waves: &WaveData,
359 pointer_pos_canvas: Option<Pos2>,
360 response: &Response,
361 msgs: &mut Vec<Message>,
362 ctx: &mut DrawingContext,
363 viewport_idx: usize,
364 ) {
365 if let Some(start_location) = self.measure_start_location {
366 let modifiers = egui_ctx.input(|i| i.modifiers);
367 if !modifiers.command
368 && response.dragged_by(PointerButton::Primary)
369 && self.do_measure(&modifiers)
370 {
371 let current_location = pointer_pos_canvas.unwrap();
372 self.draw_zoom_in_gesture(
373 start_location,
374 current_location,
375 response,
376 ctx,
377 waves,
378 viewport_idx,
379 true,
380 );
381 }
382 if response.drag_stopped_by(PointerButton::Primary) {
383 msgs.push(Message::SetMeasureDragStart(None));
384 }
385 }
386 }
387}
388
389fn draw_gesture_help(
391 config: &SurferConfig,
392 response: &Response,
393 painter: &Painter,
394 midpoint: Option<Pos2>,
395 draw_bg: bool,
396) {
397 let tan225 = 0.41421357;
399 let (midx, midy, deltax, deltay) = if let Some(midpoint) = midpoint {
400 let halfsize = config.gesture.size * 0.5;
401 (midpoint.x, midpoint.y, halfsize, halfsize)
402 } else {
403 let halfwidth = response.rect.width() * 0.5;
404 let halfheight = response.rect.height() * 0.5;
405 (halfwidth, halfheight, halfwidth, halfheight)
406 };
407
408 let container_rect = Rect::from_min_size(Pos2::ZERO, response.rect.size());
409 let to_screen = &|x, y| {
410 RectTransform::from_to(container_rect, response.rect)
411 .transform_pos(Pos2::new(x, y) + Vec2::new(0.5, 0.5))
412 };
413 let stroke = Stroke {
414 color: config.theme.gesture.color,
415 width: config.theme.gesture.width,
416 };
417 let tan225deltax = tan225 * deltax;
418 let tan225deltay = tan225 * deltay;
419 let left = midx - deltax;
420 let right = midx + deltax;
421 let top = midy - deltay;
422 let bottom = midy + deltay;
423 if draw_bg {
425 let bg_radius = config.gesture.background_radius * deltax;
426 painter.circle_filled(
427 to_screen(midx, midy),
428 bg_radius,
429 config
430 .theme
431 .canvas_colors
432 .background
433 .gamma_multiply(config.gesture.background_gamma),
434 );
435 }
436 painter.line_segment(
438 [
439 to_screen(left, midy + tan225deltax),
440 to_screen(right, midy - tan225deltax),
441 ],
442 stroke,
443 );
444 painter.line_segment(
445 [
446 to_screen(left, midy - tan225deltax),
447 to_screen(right, midy + tan225deltax),
448 ],
449 stroke,
450 );
451 painter.line_segment(
452 [
453 to_screen(midx + tan225deltay, top),
454 to_screen(midx - tan225deltay, bottom),
455 ],
456 stroke,
457 );
458 painter.line_segment(
459 [
460 to_screen(midx - tan225deltay, top),
461 to_screen(midx + tan225deltay, bottom),
462 ],
463 stroke,
464 );
465
466 let halfwaytexty_upper = top + (deltay - tan225deltax) * 0.5;
467 let halfwaytexty_lower = bottom - (deltay - tan225deltax) * 0.5;
468 painter.text(
471 to_screen(left, midy),
472 Align2::LEFT_CENTER,
473 config.gesture.mapping.west,
474 FontId::default(),
475 config.theme.foreground,
476 );
477 painter.text(
479 to_screen(right, midy),
480 Align2::RIGHT_CENTER,
481 config.gesture.mapping.east,
482 FontId::default(),
483 config.theme.foreground,
484 );
485 painter.text(
487 to_screen(left, halfwaytexty_upper),
488 Align2::LEFT_CENTER,
489 config.gesture.mapping.northwest,
490 FontId::default(),
491 config.theme.foreground,
492 );
493 painter.text(
495 to_screen(right, halfwaytexty_upper),
496 Align2::RIGHT_CENTER,
497 config.gesture.mapping.northeast,
498 FontId::default(),
499 config.theme.foreground,
500 );
501 painter.text(
503 to_screen(midx, top),
504 Align2::CENTER_TOP,
505 config.gesture.mapping.north,
506 FontId::default(),
507 config.theme.foreground,
508 );
509 painter.text(
511 to_screen(left, halfwaytexty_lower),
512 Align2::LEFT_CENTER,
513 config.gesture.mapping.southwest,
514 FontId::default(),
515 config.theme.foreground,
516 );
517 painter.text(
519 to_screen(right, halfwaytexty_lower),
520 Align2::RIGHT_CENTER,
521 config.gesture.mapping.southeast,
522 FontId::default(),
523 config.theme.foreground,
524 );
525 painter.text(
527 to_screen(midx, bottom),
528 Align2::CENTER_BOTTOM,
529 config.gesture.mapping.south,
530 FontId::default(),
531 config.theme.foreground,
532 );
533}
534
535fn gesture_type(zones: &GestureZones, delta: Vec2) -> GestureKind {
537 let tan225 = 0.41421357;
538 let tan225x = tan225 * delta.x;
539 let tan225y = tan225 * delta.y;
540 if delta.x < 0.0 {
541 if delta.y.abs() < -tan225x {
542 zones.west
544 } else if delta.y < 0.0 && delta.x < tan225y {
545 zones.northwest
547 } else if delta.y > 0.0 && delta.x < -tan225y {
548 zones.southwest
550 } else if delta.y < 0.0 {
551 zones.north
553 } else {
554 zones.south
556 }
557 } else if tan225x > delta.y.abs() {
558 zones.east
560 } else if delta.y < 0.0 && delta.x > -tan225y {
561 zones.northeast
563 } else if delta.y > 0.0 && delta.x > tan225y {
564 zones.southeast
566 } else if delta.y > 0.0 {
567 zones.north
569 } else {
570 zones.south
572 }
573}
574
575fn draw_gesture_text(
576 ctx: &mut DrawingContext,
577 pos: Pos2,
578 text: impl ToString,
579 theme: &SurferTheme,
580) {
581 let pos = pos + Vec2::new(10.0, -10.0);
583
584 let galley = ctx
585 .painter
586 .layout_no_wrap(text.to_string(), FontId::default(), theme.foreground);
587
588 ctx.painter.rect(
589 galley.rect.translate(pos.to_vec2()).expand(3.0),
590 2.0,
591 theme.primary_ui_color.background,
592 Stroke::default(),
593 egui::StrokeKind::Inside,
594 );
595
596 ctx.painter
597 .galley(pos, galley, theme.primary_ui_color.foreground);
598}