Skip to main content

viewport_lib/interaction/input/
mod.rs

1//! Input system: action-based input mapping with mode-sensitive bindings.
2//!
3//! Decouples semantic actions (Orbit, Pan, Zoom, ...) from physical triggers
4//! (key/mouse combinations), enabling future key reconfiguration and
5//! context-sensitive controls (Normal / FlyMode / Manipulating).
6
7/// Semantic action enum.
8pub mod action;
9/// Binding, trigger, and modifier types.
10pub mod binding;
11/// Default key/mouse bindings for the viewport.
12pub mod defaults;
13/// Input mode enum (Normal, FlyMode, Manipulating).
14pub mod mode;
15/// Per-frame input snapshot and action-state query evaluation.
16pub mod query;
17
18pub use action::Action;
19pub use binding::{ActivationMode, Binding, KeyCode, Modifiers, MouseButton, Trigger, TriggerKind};
20pub use defaults::default_bindings;
21pub use mode::InputMode;
22pub use query::{ActionState, FrameInput};
23
24/// Central input system that evaluates action queries against the current
25/// binding table and input mode.
26pub struct InputSystem {
27    bindings: Vec<Binding>,
28    mode: InputMode,
29}
30
31impl InputSystem {
32    /// Create a new input system with default bindings in Normal mode.
33    pub fn new() -> Self {
34        Self {
35            bindings: default_bindings(),
36            mode: InputMode::Normal,
37        }
38    }
39
40    /// Current input mode.
41    pub fn mode(&self) -> InputMode {
42        self.mode
43    }
44
45    /// Set the input mode.
46    pub fn set_mode(&mut self, mode: InputMode) {
47        self.mode = mode;
48    }
49
50    /// Query whether an action is active this frame.
51    ///
52    /// Iterates bindings matching the action and current mode, evaluates
53    /// each trigger against the frame input. First match wins.
54    pub fn query(&self, action: Action, input: &FrameInput) -> ActionState {
55        for binding in &self.bindings {
56            if binding.action != action {
57                continue;
58            }
59            // Check mode filter.
60            if !binding.active_modes.is_empty() && !binding.active_modes.contains(&self.mode) {
61                continue;
62            }
63            let state = query::evaluate_trigger(
64                &binding.trigger.kind,
65                &binding.trigger.activation,
66                &binding.trigger.modifiers,
67                binding.trigger.ignore_modifiers,
68                input,
69            );
70            if !matches!(state, ActionState::Inactive) {
71                return state;
72            }
73        }
74        ActionState::Inactive
75    }
76
77    /// Access the current binding table.
78    pub fn bindings(&self) -> &[Binding] {
79        &self.bindings
80    }
81
82    /// Replace the binding table.
83    pub fn set_bindings(&mut self, bindings: Vec<Binding>) {
84        self.bindings = bindings;
85    }
86}
87
88impl Default for InputSystem {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use binding::{KeyCode, Modifiers, MouseButton};
98    use query::FrameInput;
99
100    fn input_with_left_drag() -> FrameInput {
101        let mut input = FrameInput::default();
102        input.dragging.insert(MouseButton::Left);
103        input.drag_delta = glam::Vec2::new(10.0, 5.0);
104        input.hovered = true;
105        input
106    }
107
108    #[test]
109    fn test_query_orbit_active() {
110        let sys = InputSystem::new();
111        let mut input = input_with_left_drag();
112        input.modifiers = Modifiers::ALT;
113        let state = sys.query(Action::Orbit, &input);
114        assert!(
115            state.is_active(),
116            "orbit should be active on alt+left-drag in Normal mode"
117        );
118    }
119
120    #[test]
121    fn test_query_orbit_inactive_without_alt() {
122        let sys = InputSystem::new();
123        let input = input_with_left_drag();
124        let state = sys.query(Action::Orbit, &input);
125        assert!(
126            !state.is_active(),
127            "orbit should be inactive on plain left-drag in Normal mode"
128        );
129    }
130
131    #[test]
132    fn test_mode_filtering() {
133        let mut sys = InputSystem::new();
134        sys.set_mode(InputMode::FlyMode);
135        let input = input_with_left_drag();
136        // Orbit is bound to Normal mode only, should be inactive in FlyMode.
137        let state = sys.query(Action::Orbit, &input);
138        assert!(!state.is_active(), "orbit should be inactive in FlyMode");
139    }
140
141    #[test]
142    fn test_modifier_matching() {
143        let sys = InputSystem::new();
144        // Pan requires Shift + left drag.
145        let mut input = FrameInput::default();
146        input.dragging.insert(MouseButton::Left);
147        input.drag_delta = glam::Vec2::new(10.0, 5.0);
148        input.modifiers = Modifiers::SHIFT;
149        let state = sys.query(Action::Pan, &input);
150        assert!(
151            state.is_active(),
152            "pan should be active with shift+left drag"
153        );
154
155        // Without shift, pan should be inactive (orbit takes it instead).
156        let mut input2 = FrameInput::default();
157        input2.dragging.insert(MouseButton::Left);
158        input2.drag_delta = glam::Vec2::new(10.0, 5.0);
159        input2.modifiers = Modifiers::CTRL;
160        let state2 = sys.query(Action::Pan, &input2);
161        assert!(
162            !state2.is_active(),
163            "pan should be inactive with ctrl modifier"
164        );
165    }
166
167    #[test]
168    fn test_ignore_modifiers() {
169        let mut sys = InputSystem::new();
170        sys.set_mode(InputMode::FlyMode);
171        // FlyForward (W) uses ignore_modifiers, so it should fire even with Shift held.
172        let mut input = FrameInput::default();
173        input.keys_held.insert(KeyCode::W);
174        input.modifiers = Modifiers::SHIFT;
175        let state = sys.query(Action::FlyForward, &input);
176        assert!(
177            state.is_active(),
178            "fly forward should be active with shift held (ignore_modifiers)"
179        );
180    }
181
182    #[test]
183    fn test_empty_input_inactive() {
184        let sys = InputSystem::new();
185        let input = FrameInput::default();
186        assert!(!sys.query(Action::Orbit, &input).is_active());
187        assert!(!sys.query(Action::Pan, &input).is_active());
188        assert!(!sys.query(Action::Zoom, &input).is_active());
189        assert!(!sys.query(Action::FocusObject, &input).is_active());
190    }
191}