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