Skip to main content

viewport_lib/interaction/input/
viewport_input.rs

1//! Stateful viewport input accumulator and resolver.
2//!
3//! [`ViewportInput`] is the lower-level input resolver. Most consumers should
4//! use [`super::controller::OrbitCameraController`] which wraps it.
5
6use 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
16/// Pixels-per-line conversion for scroll delta normalisation.
17const PIXELS_PER_LINE: f32 = 28.0;
18
19/// Stateful viewport input accumulator.
20///
21/// Maintains pointer and button state across frames and resolves raw
22/// [`ViewportEvent`]s into semantic [`ActionFrame`] output.
23///
24/// # Frame lifecycle
25///
26/// ```text
27/// // --- AppState construction ---
28/// input.begin_frame(ctx);          // prime the accumulator
29///
30/// // --- Per winit window_event ---
31/// input.push_event(translated_event);
32///
33/// // --- RedrawRequested ---
34/// let actions = input.resolve();   // apply to camera / interactions
35/// input.begin_frame(ctx);          // reset for next frame's events
36/// ```
37pub struct ViewportInput {
38    bindings: Vec<ViewportBinding>,
39
40    // Per-frame accumulated deltas
41    drag_delta: glam::Vec2,
42    wheel_delta: glam::Vec2, // always in pixels
43
44    // Per-frame key accumulators (reset by begin_frame)
45    keys_pressed: HashSet<KeyCode>,
46
47    // Persistent state
48    pointer_pos: Option<glam::Vec2>,
49    /// Which buttons are currently held. Tracks three buttons.
50    button_held: [bool; 3], // [Left, Right, Middle]
51    /// Position at which each button was first pressed (to detect in-viewport press).
52    button_press_pos: [Option<glam::Vec2>; 3],
53    modifiers: Modifiers,
54    /// Keys currently held down (persistent across frames).
55    keys_held: HashSet<KeyCode>,
56
57    ctx: ViewportContext,
58}
59
60fn button_index(b: MouseButton) -> usize {
61    match b {
62        MouseButton::Left => 0,
63        MouseButton::Right => 1,
64        MouseButton::Middle => 2,
65    }
66}
67
68impl ViewportInput {
69    /// Create a new resolver with the given binding list.
70    pub fn new(bindings: Vec<ViewportBinding>) -> Self {
71        Self {
72            bindings,
73            drag_delta: glam::Vec2::ZERO,
74            wheel_delta: glam::Vec2::ZERO,
75            keys_pressed: HashSet::new(),
76            pointer_pos: None,
77            button_held: [false; 3],
78            button_press_pos: [None, None, None],
79            modifiers: Modifiers::NONE,
80            keys_held: HashSet::new(),
81            ctx: ViewportContext::default(),
82        }
83    }
84
85    /// Create a resolver for a named [`BindingPreset`].
86    pub fn from_preset(preset: BindingPreset) -> Self {
87        let bindings = match preset {
88            BindingPreset::ViewportPrimitives => viewport_primitives_bindings(),
89            BindingPreset::ViewportAll => viewport_all_bindings(),
90        };
91        Self::new(bindings)
92    }
93
94    /// Begin a new frame.
95    ///
96    /// Resets per-frame accumulators and records the current viewport context.
97    /// Call this at the END of each render so it's ready to accumulate the next
98    /// batch of events. Also call once during initialisation.
99    pub fn begin_frame(&mut self, ctx: ViewportContext) {
100        self.ctx = ctx;
101        self.drag_delta = glam::Vec2::ZERO;
102        self.wheel_delta = glam::Vec2::ZERO;
103        self.keys_pressed.clear();
104        // Note: persistent state (button_held, pointer_pos, modifiers, keys_held) is NOT reset.
105    }
106
107    /// Push a single viewport-scoped event into the accumulator.
108    pub fn push_event(&mut self, event: ViewportEvent) {
109        match event {
110            ViewportEvent::PointerMoved { position } => {
111                if let Some(prev) = self.pointer_pos {
112                    // Only accumulate drag delta when at least one button is held
113                    if self.button_held.iter().any(|&h| h) {
114                        self.drag_delta += position - prev;
115                    }
116                }
117                self.pointer_pos = Some(position);
118            }
119            ViewportEvent::MouseButton { button, state } => {
120                let idx = button_index(button);
121                match state {
122                    ButtonState::Pressed => {
123                        self.button_held[idx] = true;
124                        self.button_press_pos[idx] = self.pointer_pos;
125                    }
126                    ButtonState::Released => {
127                        self.button_held[idx] = false;
128                        self.button_press_pos[idx] = None;
129                    }
130                }
131            }
132            ViewportEvent::Wheel { delta, units } => {
133                let scale = match units {
134                    ScrollUnits::Lines => PIXELS_PER_LINE,
135                    ScrollUnits::Pixels => 1.0,
136                };
137                // Only accumulate if viewport is hovered
138                if self.ctx.hovered {
139                    self.wheel_delta += delta * scale;
140                }
141            }
142            ViewportEvent::ModifiersChanged(mods) => {
143                self.modifiers = mods;
144            }
145            ViewportEvent::Key { key, state, repeat } => {
146                // Only process key events when the viewport is focused
147                if !self.ctx.focused {
148                    return;
149                }
150                match state {
151                    ButtonState::Pressed => {
152                        if !repeat {
153                            self.keys_pressed.insert(key);
154                        }
155                        self.keys_held.insert(key);
156                    }
157                    ButtonState::Released => {
158                        self.keys_held.remove(&key);
159                    }
160                }
161            }
162            ViewportEvent::PointerLeft => {
163                self.pointer_pos = None;
164                // Release all buttons on pointer leave to avoid stuck state
165                for held in &mut self.button_held {
166                    *held = false;
167                }
168                for pos in &mut self.button_press_pos {
169                    *pos = None;
170                }
171            }
172            ViewportEvent::FocusLost => {
173                // Release all buttons and keys on focus loss
174                for held in &mut self.button_held {
175                    *held = false;
176                }
177                for pos in &mut self.button_press_pos {
178                    *pos = None;
179                }
180                self.keys_held.clear();
181                self.keys_pressed.clear();
182            }
183        }
184    }
185
186    /// Resolve accumulated events into an [`ActionFrame`].
187    ///
188    /// This does NOT reset state — call [`begin_frame`](Self::begin_frame) for that.
189    pub fn resolve(&self) -> ActionFrame {
190        let mut orbit = glam::Vec2::ZERO;
191        let mut pan = glam::Vec2::ZERO;
192        let mut zoom = 0.0f32;
193        let mut actions = std::collections::HashMap::new();
194
195        // Skip pointer/wheel gesture evaluation if viewport is not hovered
196        // (and no button is actively held from a press that started inside).
197        let any_held_with_press = self.button_held.iter().enumerate().any(|(i, &held)| {
198            held && self.button_press_pos[i].is_some()
199        });
200        let pointer_active = self.ctx.hovered || any_held_with_press;
201
202        for binding in &self.bindings {
203            match &binding.gesture {
204                ViewportGesture::Drag { button, modifiers } => {
205                    if !pointer_active {
206                        continue;
207                    }
208                    let idx = button_index(*button);
209                    let held = self.button_held[idx];
210                    let press_started = self.button_press_pos[idx].is_some();
211                    if held && press_started && modifiers.matches(self.modifiers) {
212                        let delta = self.drag_delta;
213                        match binding.action {
214                            Action::Orbit => {
215                                if orbit == glam::Vec2::ZERO {
216                                    orbit += delta;
217                                    actions.entry(binding.action).or_insert(
218                                        ResolvedActionState::Delta(delta),
219                                    );
220                                }
221                            }
222                            Action::Pan => {
223                                if pan == glam::Vec2::ZERO {
224                                    pan += delta;
225                                    actions.entry(binding.action).or_insert(
226                                        ResolvedActionState::Delta(delta),
227                                    );
228                                }
229                            }
230                            Action::Zoom => {
231                                if zoom == 0.0 {
232                                    zoom += delta.y;
233                                    actions.entry(binding.action).or_insert(
234                                        ResolvedActionState::Delta(delta),
235                                    );
236                                }
237                            }
238                            _ => {
239                                actions.entry(binding.action).or_insert(
240                                    ResolvedActionState::Delta(delta),
241                                );
242                            }
243                        }
244                    }
245                }
246                ViewportGesture::WheelY { modifiers } => {
247                    if !pointer_active {
248                        continue;
249                    }
250                    if modifiers.matches(self.modifiers) && self.wheel_delta.y != 0.0 {
251                        let y = self.wheel_delta.y;
252                        match binding.action {
253                            Action::Zoom => zoom += y,
254                            Action::Orbit => orbit.y += y,
255                            Action::Pan => pan.y += y,
256                            _ => {}
257                        }
258                        actions.entry(binding.action).or_insert(
259                            ResolvedActionState::Delta(glam::Vec2::new(0.0, y)),
260                        );
261                    }
262                }
263                ViewportGesture::WheelXY { modifiers } => {
264                    if !pointer_active {
265                        continue;
266                    }
267                    if modifiers.matches(self.modifiers) && self.wheel_delta != glam::Vec2::ZERO {
268                        let delta = self.wheel_delta;
269                        match binding.action {
270                            Action::Orbit => orbit += delta,
271                            Action::Pan => pan += delta,
272                            Action::Zoom => zoom += delta.y,
273                            _ => {}
274                        }
275                        actions.entry(binding.action).or_insert(
276                            ResolvedActionState::Delta(delta),
277                        );
278                    }
279                }
280                ViewportGesture::KeyPress { key, modifiers } => {
281                    if self.keys_pressed.contains(key) && modifiers.matches(self.modifiers) {
282                        actions.entry(binding.action).or_insert(ResolvedActionState::Pressed);
283                    }
284                }
285                ViewportGesture::KeyHold { key, modifiers } => {
286                    if self.keys_held.contains(key) && modifiers.matches(self.modifiers) {
287                        actions.entry(binding.action).or_insert(ResolvedActionState::Held);
288                    }
289                }
290            }
291        }
292
293        ActionFrame {
294            navigation: NavigationActions { orbit, pan, zoom },
295            actions,
296        }
297    }
298
299    /// Current modifier state.
300    pub fn modifiers(&self) -> Modifiers {
301        self.modifiers
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::interaction::input::preset::viewport_all_bindings;
309    use crate::interaction::input::event::ButtonState;
310
311    fn focused_ctx() -> ViewportContext {
312        ViewportContext {
313            hovered: true,
314            focused: true,
315            viewport_size: [800.0, 600.0],
316        }
317    }
318
319    #[test]
320    fn key_press_fires_once_then_clears() {
321        let mut input = ViewportInput::new(viewport_all_bindings());
322        input.begin_frame(focused_ctx());
323        input.push_event(ViewportEvent::Key {
324            key: KeyCode::F,
325            state: ButtonState::Pressed,
326            repeat: false,
327        });
328        let frame = input.resolve();
329        assert!(frame.is_active(Action::FocusObject), "FocusObject should be active on first frame");
330
331        // Second frame without a new press should not fire
332        input.begin_frame(focused_ctx());
333        let frame2 = input.resolve();
334        assert!(!frame2.is_active(Action::FocusObject), "FocusObject should not be active on second frame");
335    }
336
337    #[test]
338    fn key_ignored_when_not_focused() {
339        let mut input = ViewportInput::new(viewport_all_bindings());
340        input.begin_frame(ViewportContext {
341            hovered: true,
342            focused: false,
343            viewport_size: [800.0, 600.0],
344        });
345        input.push_event(ViewportEvent::Key {
346            key: KeyCode::F,
347            state: ButtonState::Pressed,
348            repeat: false,
349        });
350        let frame = input.resolve();
351        assert!(!frame.is_active(Action::FocusObject), "key should be ignored without focus");
352    }
353}