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