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.shift && !modifiers.command && response.dragged_by(PointerButton::Primary)
368 {
369 let current_location = pointer_pos_canvas.unwrap();
370 self.draw_zoom_in_gesture(
371 start_location,
372 current_location,
373 response,
374 ctx,
375 waves,
376 viewport_idx,
377 true,
378 );
379 }
380 if response.drag_stopped_by(PointerButton::Primary) {
381 msgs.push(Message::SetMeasureDragStart(None));
382 }
383 }
384 }
385}
386
387fn draw_gesture_help(
389 config: &SurferConfig,
390 response: &Response,
391 painter: &Painter,
392 midpoint: Option<Pos2>,
393 draw_bg: bool,
394) {
395 let tan225 = 0.41421357;
397 let (midx, midy, deltax, deltay) = if let Some(midpoint) = midpoint {
398 let halfsize = config.gesture.size * 0.5;
399 (midpoint.x, midpoint.y, halfsize, halfsize)
400 } else {
401 let halfwidth = response.rect.width() * 0.5;
402 let halfheight = response.rect.height() * 0.5;
403 (halfwidth, halfheight, halfwidth, halfheight)
404 };
405
406 let container_rect = Rect::from_min_size(Pos2::ZERO, response.rect.size());
407 let to_screen = &|x, y| {
408 RectTransform::from_to(container_rect, response.rect)
409 .transform_pos(Pos2::new(x, y) + Vec2::new(0.5, 0.5))
410 };
411 let stroke = Stroke {
412 color: config.theme.gesture.color,
413 width: config.theme.gesture.width,
414 };
415 let tan225deltax = tan225 * deltax;
416 let tan225deltay = tan225 * deltay;
417 let left = midx - deltax;
418 let right = midx + deltax;
419 let top = midy - deltay;
420 let bottom = midy + deltay;
421 if draw_bg {
423 let bg_radius = config.gesture.background_radius * deltax;
424 painter.circle_filled(
425 to_screen(midx, midy),
426 bg_radius,
427 config
428 .theme
429 .canvas_colors
430 .background
431 .gamma_multiply(config.gesture.background_gamma),
432 );
433 }
434 painter.line_segment(
436 [
437 to_screen(left, midy + tan225deltax),
438 to_screen(right, midy - tan225deltax),
439 ],
440 stroke,
441 );
442 painter.line_segment(
443 [
444 to_screen(left, midy - tan225deltax),
445 to_screen(right, midy + tan225deltax),
446 ],
447 stroke,
448 );
449 painter.line_segment(
450 [
451 to_screen(midx + tan225deltay, top),
452 to_screen(midx - tan225deltay, bottom),
453 ],
454 stroke,
455 );
456 painter.line_segment(
457 [
458 to_screen(midx - tan225deltay, top),
459 to_screen(midx + tan225deltay, bottom),
460 ],
461 stroke,
462 );
463
464 let halfwaytexty_upper = top + (deltay - tan225deltax) * 0.5;
465 let halfwaytexty_lower = bottom - (deltay - tan225deltax) * 0.5;
466 painter.text(
469 to_screen(left, midy),
470 Align2::LEFT_CENTER,
471 config.gesture.mapping.west,
472 FontId::default(),
473 config.theme.foreground,
474 );
475 painter.text(
477 to_screen(right, midy),
478 Align2::RIGHT_CENTER,
479 config.gesture.mapping.east,
480 FontId::default(),
481 config.theme.foreground,
482 );
483 painter.text(
485 to_screen(left, halfwaytexty_upper),
486 Align2::LEFT_CENTER,
487 config.gesture.mapping.northwest,
488 FontId::default(),
489 config.theme.foreground,
490 );
491 painter.text(
493 to_screen(right, halfwaytexty_upper),
494 Align2::RIGHT_CENTER,
495 config.gesture.mapping.northeast,
496 FontId::default(),
497 config.theme.foreground,
498 );
499 painter.text(
501 to_screen(midx, top),
502 Align2::CENTER_TOP,
503 config.gesture.mapping.north,
504 FontId::default(),
505 config.theme.foreground,
506 );
507 painter.text(
509 to_screen(left, halfwaytexty_lower),
510 Align2::LEFT_CENTER,
511 config.gesture.mapping.southwest,
512 FontId::default(),
513 config.theme.foreground,
514 );
515 painter.text(
517 to_screen(right, halfwaytexty_lower),
518 Align2::RIGHT_CENTER,
519 config.gesture.mapping.southeast,
520 FontId::default(),
521 config.theme.foreground,
522 );
523 painter.text(
525 to_screen(midx, bottom),
526 Align2::CENTER_BOTTOM,
527 config.gesture.mapping.south,
528 FontId::default(),
529 config.theme.foreground,
530 );
531}
532
533fn gesture_type(zones: &GestureZones, delta: Vec2) -> GestureKind {
535 let tan225 = 0.41421357;
536 let tan225x = tan225 * delta.x;
537 let tan225y = tan225 * delta.y;
538 if delta.x < 0.0 {
539 if delta.y.abs() < -tan225x {
540 zones.west
542 } else if delta.y < 0.0 && delta.x < tan225y {
543 zones.northwest
545 } else if delta.y > 0.0 && delta.x < -tan225y {
546 zones.southwest
548 } else if delta.y < 0.0 {
549 zones.north
551 } else {
552 zones.south
554 }
555 } else if tan225x > delta.y.abs() {
556 zones.east
558 } else if delta.y < 0.0 && delta.x > -tan225y {
559 zones.northeast
561 } else if delta.y > 0.0 && delta.x > tan225y {
562 zones.southeast
564 } else if delta.y > 0.0 {
565 zones.north
567 } else {
568 zones.south
570 }
571}
572
573fn draw_gesture_text(
574 ctx: &mut DrawingContext,
575 pos: Pos2,
576 text: impl ToString,
577 theme: &SurferTheme,
578) {
579 let pos = pos + Vec2::new(10.0, -10.0);
581
582 let galley = ctx
583 .painter
584 .layout_no_wrap(text.to_string(), FontId::default(), theme.foreground);
585
586 ctx.painter.rect(
587 galley.rect.translate(pos.to_vec2()).expand(3.0),
588 2.0,
589 theme.primary_ui_color.background,
590 Stroke::default(),
591 egui::StrokeKind::Inside,
592 );
593
594 ctx.painter
595 .galley(pos, galley, theme.primary_ui_color.foreground);
596}