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//! # New input pipeline (recommended)
8//!
9//! The new pipeline works like this:
10//!
11//! 1. Translate native events to [`ViewportEvent`]
12//! 2. Feed into [`OrbitCameraController`](crate::camera::controllers::orbit::OrbitCameraController) (or lower-level [`ViewportInput`])
13//! 3. Call [`OrbitCameraController::apply_to_camera`](crate::camera::controllers::orbit::OrbitCameraController::apply_to_camera) each frame
14//!
15//! # Legacy input system (compatibility)
16//!
17//! The older [`InputSystem`] / [`FrameInput`] query model remains available.
18
19/// Semantic action enum.
20pub mod action;
21/// Binding, trigger, and modifier types.
22pub mod binding;
23/// Default key/mouse bindings for the viewport.
24pub mod defaults;
25/// Input mode enum (Normal, FlyMode, Manipulating).
26pub mod mode;
27/// Per-frame input snapshot and action-state query evaluation.
28pub mod query;
29
30// New input pipeline modules
31/// Per-frame resolved action output.
32pub mod action_frame;
33/// Per-frame viewport context.
34pub mod context;
35/// Framework-agnostic viewport events.
36pub mod event;
37/// Named control presets.
38pub mod preset;
39/// Viewport gesture and binding types.
40pub mod viewport_binding;
41/// Stateful viewport input accumulator and resolver.
42pub mod viewport_input;
43
44// Legacy re-exports (compatibility)
45pub use action::Action;
46pub use binding::{ActivationMode, Binding, KeyCode, Modifiers, MouseButton, Trigger, TriggerKind};
47pub use defaults::default_bindings;
48pub use mode::{InputMode, NavigationMode};
49pub use query::{ActionState, FrameInput};
50
51// New pipeline re-exports
52pub use action_frame::{ActionFrame, NavigationActions, ResolvedActionState};
53pub use context::ViewportContext;
54pub use event::{ButtonState, ScrollUnits, ViewportEvent};
55pub use preset::{BindingPreset, viewport_all_bindings};
56pub use viewport_binding::{ModifiersMatch, ViewportBinding, ViewportGesture};
57pub use viewport_input::ViewportInput;
58
59/// Central input system that evaluates action queries against the current
60/// binding table and input mode.
61pub struct InputSystem {
62    bindings: Vec<Binding>,
63    mode: InputMode,
64}
65
66impl InputSystem {
67    /// Create a new input system with default bindings in Normal mode.
68    pub fn new() -> Self {
69        Self {
70            bindings: default_bindings(),
71            mode: InputMode::Normal,
72        }
73    }
74
75    /// Current input mode.
76    pub fn mode(&self) -> InputMode {
77        self.mode
78    }
79
80    /// Set the input mode.
81    pub fn set_mode(&mut self, mode: InputMode) {
82        self.mode = mode;
83    }
84
85    /// Query whether an action is active this frame.
86    ///
87    /// Iterates bindings matching the action and current mode, evaluates
88    /// each trigger against the frame input. First match wins.
89    pub fn query(&self, action: Action, input: &FrameInput) -> ActionState {
90        for binding in &self.bindings {
91            if binding.action != action {
92                continue;
93            }
94            // Check mode filter.
95            if !binding.active_modes.is_empty() && !binding.active_modes.contains(&self.mode) {
96                continue;
97            }
98            let state = query::evaluate_trigger(
99                &binding.trigger.kind,
100                &binding.trigger.activation,
101                &binding.trigger.modifiers,
102                binding.trigger.ignore_modifiers,
103                input,
104            );
105            if !matches!(state, ActionState::Inactive) {
106                return state;
107            }
108        }
109        ActionState::Inactive
110    }
111
112    /// Access the current binding table.
113    pub fn bindings(&self) -> &[Binding] {
114        &self.bindings
115    }
116
117    /// Replace the binding table.
118    pub fn set_bindings(&mut self, bindings: Vec<Binding>) {
119        self.bindings = bindings;
120    }
121}
122
123impl Default for InputSystem {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use binding::{KeyCode, Modifiers, MouseButton};
133    use query::FrameInput;
134
135    fn input_with_left_drag() -> FrameInput {
136        let mut input = FrameInput::default();
137        input.dragging.insert(MouseButton::Left);
138        input.drag_delta = glam::Vec2::new(10.0, 5.0);
139        input.hovered = true;
140        input
141    }
142
143    #[test]
144    fn test_query_orbit_active() {
145        let sys = InputSystem::new();
146        let mut input = input_with_left_drag();
147        input.modifiers = Modifiers::ALT;
148        let state = sys.query(Action::Orbit, &input);
149        assert!(
150            state.is_active(),
151            "orbit should be active on alt+left-drag in Normal mode"
152        );
153    }
154
155    #[test]
156    fn test_query_orbit_inactive_without_alt() {
157        let sys = InputSystem::new();
158        let input = input_with_left_drag();
159        let state = sys.query(Action::Orbit, &input);
160        assert!(
161            !state.is_active(),
162            "orbit should be inactive on plain left-drag in Normal mode"
163        );
164    }
165
166    #[test]
167    fn test_mode_filtering() {
168        let mut sys = InputSystem::new();
169        sys.set_mode(InputMode::FlyMode);
170        let input = input_with_left_drag();
171        // Orbit is bound to Normal mode only, should be inactive in FlyMode.
172        let state = sys.query(Action::Orbit, &input);
173        assert!(!state.is_active(), "orbit should be inactive in FlyMode");
174    }
175
176    #[test]
177    fn test_modifier_matching() {
178        let sys = InputSystem::new();
179        // Pan requires Shift + left drag.
180        let mut input = FrameInput::default();
181        input.dragging.insert(MouseButton::Left);
182        input.drag_delta = glam::Vec2::new(10.0, 5.0);
183        input.modifiers = Modifiers::SHIFT;
184        let state = sys.query(Action::Pan, &input);
185        assert!(
186            state.is_active(),
187            "pan should be active with shift+left drag"
188        );
189
190        // Without shift, pan should be inactive (orbit takes it instead).
191        let mut input2 = FrameInput::default();
192        input2.dragging.insert(MouseButton::Left);
193        input2.drag_delta = glam::Vec2::new(10.0, 5.0);
194        input2.modifiers = Modifiers::CTRL;
195        let state2 = sys.query(Action::Pan, &input2);
196        assert!(
197            !state2.is_active(),
198            "pan should be inactive with ctrl modifier"
199        );
200    }
201
202    #[test]
203    fn test_ignore_modifiers() {
204        let mut sys = InputSystem::new();
205        sys.set_mode(InputMode::FlyMode);
206        // FlyForward (W) uses ignore_modifiers, so it should fire even with Shift held.
207        let mut input = FrameInput::default();
208        input.keys_held.insert(KeyCode::W);
209        input.modifiers = Modifiers::SHIFT;
210        let state = sys.query(Action::FlyForward, &input);
211        assert!(
212            state.is_active(),
213            "fly forward should be active with shift held (ignore_modifiers)"
214        );
215    }
216
217    #[test]
218    fn test_empty_input_inactive() {
219        let sys = InputSystem::new();
220        let input = FrameInput::default();
221        assert!(!sys.query(Action::Orbit, &input).is_active());
222        assert!(!sys.query(Action::Pan, &input).is_active());
223        assert!(!sys.query(Action::Zoom, &input).is_active());
224        assert!(!sys.query(Action::FocusObject, &input).is_active());
225    }
226}