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