viewport_lib/interaction/input/
viewport_input.rs1use std::collections::HashSet;
7
8use super::action::Action;
9use super::action_frame::{ActionFrame, NavigationActions, ResolvedActionState};
10use super::binding::{KeyCode, Modifiers, MouseButton};
11use super::context::ViewportContext;
12use super::event::{ButtonState, ScrollUnits, ViewportEvent};
13use super::preset::{BindingPreset, viewport_all_bindings, viewport_primitives_bindings};
14use super::viewport_binding::{ViewportBinding, ViewportGesture};
15
16const PIXELS_PER_LINE: f32 = 28.0;
18
19pub struct ViewportInput {
38 bindings: Vec<ViewportBinding>,
39
40 drag_delta: glam::Vec2,
42 wheel_delta: glam::Vec2, rotate_gesture: f32, keys_pressed: HashSet<KeyCode>,
47 typed_chars: Vec<char>,
49
50 pointer_pos: Option<glam::Vec2>,
52 button_held: [bool; 3], button_press_pos: [Option<glam::Vec2>; 3],
56 modifiers: Modifiers,
57 keys_held: HashSet<KeyCode>,
59
60 ctx: ViewportContext,
61}
62
63fn button_index(b: MouseButton) -> usize {
64 match b {
65 MouseButton::Left => 0,
66 MouseButton::Right => 1,
67 MouseButton::Middle => 2,
68 }
69}
70
71impl ViewportInput {
72 pub fn new(bindings: Vec<ViewportBinding>) -> Self {
74 Self {
75 bindings,
76 drag_delta: glam::Vec2::ZERO,
77 wheel_delta: glam::Vec2::ZERO,
78 rotate_gesture: 0.0,
79 keys_pressed: HashSet::new(),
80 typed_chars: Vec::new(),
81 pointer_pos: None,
82 button_held: [false; 3],
83 button_press_pos: [None, None, None],
84 modifiers: Modifiers::NONE,
85 keys_held: HashSet::new(),
86 ctx: ViewportContext::default(),
87 }
88 }
89
90 pub fn from_preset(preset: BindingPreset) -> Self {
92 let bindings = match preset {
93 BindingPreset::ViewportPrimitives => viewport_primitives_bindings(),
94 BindingPreset::ViewportAll => viewport_all_bindings(),
95 };
96 Self::new(bindings)
97 }
98
99 pub fn begin_frame(&mut self, ctx: ViewportContext) {
105 self.ctx = ctx;
106 self.drag_delta = glam::Vec2::ZERO;
107 self.wheel_delta = glam::Vec2::ZERO;
108 self.rotate_gesture = 0.0;
109 self.keys_pressed.clear();
110 self.typed_chars.clear();
111 }
113
114 pub fn push_event(&mut self, event: ViewportEvent) {
116 match event {
117 ViewportEvent::PointerMoved { position } => {
118 if let Some(prev) = self.pointer_pos {
119 if self.button_held.iter().any(|&h| h) {
121 self.drag_delta += position - prev;
122 }
123 }
124 self.pointer_pos = Some(position);
125 }
126 ViewportEvent::MouseButton { button, state } => {
127 let idx = button_index(button);
128 match state {
129 ButtonState::Pressed => {
130 self.button_held[idx] = true;
131 self.button_press_pos[idx] = self.pointer_pos;
132 }
133 ButtonState::Released => {
134 self.button_held[idx] = false;
135 self.button_press_pos[idx] = None;
136 }
137 }
138 }
139 ViewportEvent::Wheel { delta, units } => {
140 let scale = match units {
141 ScrollUnits::Lines => PIXELS_PER_LINE,
142 ScrollUnits::Pixels => 1.0,
143 };
144 if self.ctx.hovered {
146 self.wheel_delta += delta * scale;
147 }
148 }
149 ViewportEvent::ModifiersChanged(mods) => {
150 self.modifiers = mods;
151 }
152 ViewportEvent::Key { key, state, repeat } => {
153 if !self.ctx.focused {
155 return;
156 }
157 match state {
158 ButtonState::Pressed => {
159 if !repeat {
160 self.keys_pressed.insert(key);
161 }
162 self.keys_held.insert(key);
163 }
164 ButtonState::Released => {
165 self.keys_held.remove(&key);
166 }
167 }
168 }
169 ViewportEvent::Character(c) => {
170 if c.is_ascii_digit() || c == '.' || c == '-' {
174 self.typed_chars.push(c);
175 }
176 }
177 ViewportEvent::PointerLeft => {
178 self.pointer_pos = None;
179 for held in &mut self.button_held {
181 *held = false;
182 }
183 for pos in &mut self.button_press_pos {
184 *pos = None;
185 }
186 }
187 ViewportEvent::FocusLost => {
188 for held in &mut self.button_held {
190 *held = false;
191 }
192 for pos in &mut self.button_press_pos {
193 *pos = None;
194 }
195 self.keys_held.clear();
196 self.keys_pressed.clear();
197 }
198 ViewportEvent::TrackpadRotate(angle) => {
199 if self.ctx.hovered {
200 self.rotate_gesture += angle;
201 }
202 }
203 }
204 }
205
206 pub fn resolve(&self) -> ActionFrame {
210 let mut orbit = glam::Vec2::ZERO;
211 let mut pan = glam::Vec2::ZERO;
212 let mut zoom = 0.0f32;
213 let mut actions = std::collections::HashMap::new();
214
215 let any_held_with_press = self
218 .button_held
219 .iter()
220 .enumerate()
221 .any(|(i, &held)| held && self.button_press_pos[i].is_some());
222 let pointer_active = self.ctx.hovered || any_held_with_press;
223
224 for binding in &self.bindings {
225 match &binding.gesture {
226 ViewportGesture::Drag { button, modifiers } => {
227 if !pointer_active {
228 continue;
229 }
230 let idx = button_index(*button);
231 let held = self.button_held[idx];
232 let press_started = self.button_press_pos[idx].is_some();
233 if held && press_started && modifiers.matches(self.modifiers) {
234 let delta = self.drag_delta;
235 match binding.action {
236 Action::Orbit => {
237 if orbit == glam::Vec2::ZERO {
238 orbit += delta;
239 actions
240 .entry(binding.action)
241 .or_insert(ResolvedActionState::Delta(delta));
242 }
243 }
244 Action::Pan => {
245 if pan == glam::Vec2::ZERO {
246 pan += delta;
247 actions
248 .entry(binding.action)
249 .or_insert(ResolvedActionState::Delta(delta));
250 }
251 }
252 Action::Zoom => {
253 if zoom == 0.0 {
254 zoom += delta.y;
255 actions
256 .entry(binding.action)
257 .or_insert(ResolvedActionState::Delta(delta));
258 }
259 }
260 _ => {
261 actions
262 .entry(binding.action)
263 .or_insert(ResolvedActionState::Delta(delta));
264 }
265 }
266 }
267 }
268 ViewportGesture::WheelY { modifiers } => {
269 if !pointer_active {
270 continue;
271 }
272 if modifiers.matches(self.modifiers) && self.wheel_delta.y != 0.0 {
273 let y = self.wheel_delta.y;
274 match binding.action {
275 Action::Zoom => zoom += y,
276 Action::Orbit => orbit.y += y,
277 Action::Pan => pan.y += y,
278 _ => {}
279 }
280 actions
281 .entry(binding.action)
282 .or_insert(ResolvedActionState::Delta(glam::Vec2::new(0.0, y)));
283 }
284 }
285 ViewportGesture::WheelXY { modifiers } => {
286 if !pointer_active {
287 continue;
288 }
289 if modifiers.matches(self.modifiers) && self.wheel_delta != glam::Vec2::ZERO {
290 let delta = self.wheel_delta;
291 match binding.action {
292 Action::Orbit => orbit += delta,
293 Action::Pan => pan += delta,
294 Action::Zoom => zoom += delta.y,
295 _ => {}
296 }
297 actions
298 .entry(binding.action)
299 .or_insert(ResolvedActionState::Delta(delta));
300 }
301 }
302 ViewportGesture::KeyPress { key, modifiers } => {
303 if self.keys_pressed.contains(key) && modifiers.matches(self.modifiers) {
304 actions
305 .entry(binding.action)
306 .or_insert(ResolvedActionState::Pressed);
307 }
308 }
309 ViewportGesture::KeyHold { key, modifiers } => {
310 if self.keys_held.contains(key) && modifiers.matches(self.modifiers) {
311 actions
312 .entry(binding.action)
313 .or_insert(ResolvedActionState::Held);
314 }
315 }
316 }
317 }
318
319 ActionFrame {
320 navigation: NavigationActions {
321 orbit,
322 pan,
323 zoom,
324 twist: self.rotate_gesture,
325 },
326 actions,
327 typed_chars: self.typed_chars.clone(),
328 }
329 }
330
331 pub fn modifiers(&self) -> Modifiers {
333 self.modifiers
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::interaction::input::event::ButtonState;
341 use crate::interaction::input::preset::viewport_all_bindings;
342
343 fn focused_ctx() -> ViewportContext {
344 ViewportContext {
345 hovered: true,
346 focused: true,
347 viewport_size: [800.0, 600.0],
348 }
349 }
350
351 #[test]
352 fn key_press_fires_once_then_clears() {
353 let mut input = ViewportInput::new(viewport_all_bindings());
354 input.begin_frame(focused_ctx());
355 input.push_event(ViewportEvent::Key {
356 key: KeyCode::F,
357 state: ButtonState::Pressed,
358 repeat: false,
359 });
360 let frame = input.resolve();
361 assert!(
362 frame.is_active(Action::FocusObject),
363 "FocusObject should be active on first frame"
364 );
365
366 input.begin_frame(focused_ctx());
368 let frame2 = input.resolve();
369 assert!(
370 !frame2.is_active(Action::FocusObject),
371 "FocusObject should not be active on second frame"
372 );
373 }
374
375 #[test]
376 fn key_ignored_when_not_focused() {
377 let mut input = ViewportInput::new(viewport_all_bindings());
378 input.begin_frame(ViewportContext {
379 hovered: true,
380 focused: false,
381 viewport_size: [800.0, 600.0],
382 });
383 input.push_event(ViewportEvent::Key {
384 key: KeyCode::F,
385 state: ButtonState::Pressed,
386 repeat: false,
387 });
388 let frame = input.resolve();
389 assert!(
390 !frame.is_active(Action::FocusObject),
391 "key should be ignored without focus"
392 );
393 }
394
395 #[test]
396 fn resolve_no_events_is_zero() {
397 let mut input = ViewportInput::new(viewport_all_bindings());
398 input.begin_frame(focused_ctx());
399 let frame = input.resolve();
400 assert_eq!(frame.navigation.orbit, glam::Vec2::ZERO);
401 assert_eq!(frame.navigation.pan, glam::Vec2::ZERO);
402 assert_eq!(frame.navigation.zoom, 0.0);
403 assert_eq!(frame.navigation.twist, 0.0);
404 assert!(frame.actions.is_empty());
405 }
406
407 #[test]
408 fn scroll_produces_zoom() {
409 let mut input = ViewportInput::new(viewport_all_bindings());
410 input.begin_frame(focused_ctx());
411 input.push_event(ViewportEvent::Wheel {
412 delta: glam::Vec2::new(0.0, 3.0),
413 units: ScrollUnits::Lines,
414 });
415 let frame = input.resolve();
416 assert!((frame.navigation.zoom - 84.0).abs() < 1e-3);
418 }
419
420 #[test]
421 fn scroll_pixel_units_no_scaling() {
422 let mut input = ViewportInput::new(viewport_all_bindings());
423 input.begin_frame(focused_ctx());
424 input.push_event(ViewportEvent::Wheel {
425 delta: glam::Vec2::new(0.0, 10.0),
426 units: ScrollUnits::Pixels,
427 });
428 let frame = input.resolve();
429 assert!((frame.navigation.zoom - 10.0).abs() < 1e-3);
430 }
431
432 #[test]
433 fn scroll_ignored_when_not_hovered() {
434 let mut input = ViewportInput::new(viewport_all_bindings());
435 input.begin_frame(ViewportContext {
436 hovered: false,
437 focused: true,
438 viewport_size: [800.0, 600.0],
439 });
440 input.push_event(ViewportEvent::Wheel {
441 delta: glam::Vec2::new(0.0, 5.0),
442 units: ScrollUnits::Lines,
443 });
444 let frame = input.resolve();
445 assert_eq!(frame.navigation.zoom, 0.0);
446 }
447
448 #[test]
449 fn right_drag_produces_pan() {
450 let mut input = ViewportInput::new(viewport_all_bindings());
451 input.begin_frame(focused_ctx());
452 input.push_event(ViewportEvent::PointerMoved {
454 position: glam::Vec2::new(100.0, 100.0),
455 });
456 input.push_event(ViewportEvent::MouseButton {
457 button: MouseButton::Right,
458 state: ButtonState::Pressed,
459 });
460 input.push_event(ViewportEvent::PointerMoved {
461 position: glam::Vec2::new(110.0, 105.0),
462 });
463 let frame = input.resolve();
464 assert!((frame.navigation.pan.x - 10.0).abs() < 1e-3);
465 assert!((frame.navigation.pan.y - 5.0).abs() < 1e-3);
466 }
467
468 #[test]
469 fn pointer_move_without_button_no_drag() {
470 let mut input = ViewportInput::new(viewport_all_bindings());
471 input.begin_frame(focused_ctx());
472 input.push_event(ViewportEvent::PointerMoved {
473 position: glam::Vec2::new(100.0, 100.0),
474 });
475 input.push_event(ViewportEvent::PointerMoved {
476 position: glam::Vec2::new(200.0, 200.0),
477 });
478 let frame = input.resolve();
479 assert_eq!(frame.navigation.orbit, glam::Vec2::ZERO);
480 assert_eq!(frame.navigation.pan, glam::Vec2::ZERO);
481 }
482
483 #[test]
484 fn begin_frame_resets_accumulators() {
485 let mut input = ViewportInput::new(viewport_all_bindings());
486 input.begin_frame(focused_ctx());
487 input.push_event(ViewportEvent::Wheel {
488 delta: glam::Vec2::new(0.0, 5.0),
489 units: ScrollUnits::Pixels,
490 });
491 let frame1 = input.resolve();
493 assert!(frame1.navigation.zoom != 0.0);
494 input.begin_frame(focused_ctx());
496 let frame2 = input.resolve();
497 assert_eq!(frame2.navigation.zoom, 0.0);
498 }
499
500 #[test]
501 fn pointer_left_releases_buttons() {
502 let mut input = ViewportInput::new(viewport_all_bindings());
503 input.begin_frame(focused_ctx());
504 input.push_event(ViewportEvent::PointerMoved {
505 position: glam::Vec2::new(100.0, 100.0),
506 });
507 input.push_event(ViewportEvent::MouseButton {
508 button: MouseButton::Right,
509 state: ButtonState::Pressed,
510 });
511 input.push_event(ViewportEvent::PointerLeft);
512 input.push_event(ViewportEvent::PointerMoved {
514 position: glam::Vec2::new(200.0, 200.0),
515 });
516 let frame = input.resolve();
517 assert_eq!(frame.navigation.pan, glam::Vec2::ZERO);
518 }
519
520 #[test]
521 fn focus_lost_clears_keys() {
522 let mut input = ViewportInput::new(viewport_all_bindings());
523 input.begin_frame(focused_ctx());
524 input.push_event(ViewportEvent::Key {
525 key: KeyCode::W,
526 state: ButtonState::Pressed,
527 repeat: false,
528 });
529 input.push_event(ViewportEvent::FocusLost);
530 let frame = input.resolve();
531 assert!(
533 !frame.is_active(Action::FlyForward),
534 "FlyForward should not be active after focus lost"
535 );
536 }
537
538 #[test]
539 fn character_event_populates_typed_chars() {
540 let mut input = ViewportInput::new(viewport_all_bindings());
541 input.begin_frame(focused_ctx());
542 input.push_event(ViewportEvent::Character('3'));
543 input.push_event(ViewportEvent::Character('.'));
544 input.push_event(ViewportEvent::Character('5'));
545 input.push_event(ViewportEvent::Character('a')); let frame = input.resolve();
547 assert_eq!(frame.typed_chars, vec!['3', '.', '5']);
548 }
549
550 #[test]
551 fn trackpad_rotate_accumulates_twist() {
552 let mut input = ViewportInput::new(viewport_all_bindings());
553 input.begin_frame(focused_ctx());
554 input.push_event(ViewportEvent::TrackpadRotate(0.1));
555 input.push_event(ViewportEvent::TrackpadRotate(0.2));
556 let frame = input.resolve();
557 assert!((frame.navigation.twist - 0.3).abs() < 1e-5);
558 }
559
560 #[test]
561 fn key_hold_active_every_frame() {
562 let mut input = ViewportInput::new(viewport_all_bindings());
563 input.begin_frame(focused_ctx());
564 input.push_event(ViewportEvent::Key {
565 key: KeyCode::W,
566 state: ButtonState::Pressed,
567 repeat: false,
568 });
569 let frame1 = input.resolve();
570 assert!(frame1.is_active(Action::FlyForward));
571 input.begin_frame(focused_ctx());
573 let frame2 = input.resolve();
574 assert!(
575 frame2.is_active(Action::FlyForward),
576 "FlyForward should persist while key is held"
577 );
578 }
579
580 #[test]
581 fn key_release_stops_hold() {
582 let mut input = ViewportInput::new(viewport_all_bindings());
583 input.begin_frame(focused_ctx());
584 input.push_event(ViewportEvent::Key {
585 key: KeyCode::W,
586 state: ButtonState::Pressed,
587 repeat: false,
588 });
589 let frame1 = input.resolve();
590 assert!(frame1.is_active(Action::FlyForward));
591 input.begin_frame(focused_ctx());
592 input.push_event(ViewportEvent::Key {
593 key: KeyCode::W,
594 state: ButtonState::Released,
595 repeat: false,
596 });
597 let frame2 = input.resolve();
598 assert!(
599 !frame2.is_active(Action::FlyForward),
600 "FlyForward should stop after key release"
601 );
602 }
603
604 #[test]
605 fn modifiers_changed_affects_bindings() {
606 let mut input = ViewportInput::new(viewport_all_bindings());
607 input.begin_frame(focused_ctx());
608 input.push_event(ViewportEvent::ModifiersChanged(Modifiers::SHIFT));
610 input.push_event(ViewportEvent::Key {
611 key: KeyCode::X,
612 state: ButtonState::Pressed,
613 repeat: false,
614 });
615 let frame = input.resolve();
616 assert!(
617 frame.is_active(Action::ExcludeX),
618 "Shift+X should fire ExcludeX"
619 );
620 }
621
622 #[test]
623 fn repeat_key_does_not_fire_press() {
624 let mut input = ViewportInput::new(viewport_all_bindings());
625 input.begin_frame(focused_ctx());
626 input.push_event(ViewportEvent::Key {
627 key: KeyCode::G,
628 state: ButtonState::Pressed,
629 repeat: true,
630 });
631 let frame = input.resolve();
632 assert!(
634 !frame.is_active(Action::BeginMove),
635 "repeat should not fire KeyPress bindings"
636 );
637 }
638
639 #[test]
640 fn middle_drag_shift_produces_pan() {
641 let mut input = ViewportInput::new(viewport_all_bindings());
642 input.begin_frame(focused_ctx());
643 input.push_event(ViewportEvent::PointerMoved {
644 position: glam::Vec2::new(50.0, 50.0),
645 });
646 input.push_event(ViewportEvent::ModifiersChanged(Modifiers::SHIFT));
647 input.push_event(ViewportEvent::MouseButton {
648 button: MouseButton::Middle,
649 state: ButtonState::Pressed,
650 });
651 input.push_event(ViewportEvent::PointerMoved {
652 position: glam::Vec2::new(60.0, 55.0),
653 });
654 let frame = input.resolve();
655 assert!((frame.navigation.pan.x - 10.0).abs() < 1e-3);
656 assert!((frame.navigation.pan.y - 5.0).abs() < 1e-3);
657 }
658}