Skip to main content

viewport_lib/interaction/input/
query.rs

1use super::binding::{ActivationMode, KeyCode, Modifiers, MouseButton, TriggerKind};
2use std::collections::HashSet;
3
4/// Per-frame input snapshot populated by the egui adapter.
5///
6/// This is framework-agnostic - the egui-specific translation happens in the
7/// host application's viewport adapter.
8#[derive(Debug, Clone)]
9pub struct FrameInput {
10    /// Keys pressed this frame (rising edge).
11    pub keys_pressed: HashSet<KeyCode>,
12    /// Keys held this frame.
13    pub keys_held: HashSet<KeyCode>,
14    /// Current modifier state.
15    pub modifiers: Modifiers,
16    /// Mouse buttons that started a drag this frame.
17    pub drag_started: HashSet<MouseButton>,
18    /// Mouse buttons currently being dragged.
19    pub dragging: HashSet<MouseButton>,
20    /// Accumulated drag delta this frame (pixels).
21    pub drag_delta: glam::Vec2,
22    /// Scroll delta this frame (positive = up/zoom-in).
23    pub scroll_delta: f32,
24    /// Mouse buttons clicked this frame (press + release without drag).
25    pub clicked: HashSet<MouseButton>,
26    /// Raw pointer movement delta this frame (pixels), regardless of button state.
27    /// Used for manipulation modes where mouse movement alone drives transforms.
28    pub pointer_delta: glam::Vec2,
29    /// Whether the input area (viewport) is hovered.
30    pub hovered: bool,
31    /// Ctrl+scroll orbit delta in logical pixels (x = yaw, y = pitch).
32    /// Read from raw MouseWheel events to preserve 2D direction — smooth_scroll_delta
33    /// loses directional data when modifiers are held.
34    pub ctrl_scroll_orbit_delta: glam::Vec2,
35    /// Shift+scroll pan delta in logical pixels (x = right, y = up).
36    /// Read from raw MouseWheel events for the same reason as ctrl_scroll_orbit_delta.
37    pub shift_scroll_pan_delta: glam::Vec2,
38}
39
40impl Default for FrameInput {
41    fn default() -> Self {
42        Self {
43            keys_pressed: Default::default(),
44            keys_held: Default::default(),
45            modifiers: Default::default(),
46            drag_started: Default::default(),
47            dragging: Default::default(),
48            drag_delta: glam::Vec2::ZERO,
49            scroll_delta: 0.0,
50            clicked: Default::default(),
51            pointer_delta: glam::Vec2::ZERO,
52            hovered: false,
53            ctrl_scroll_orbit_delta: glam::Vec2::ZERO,
54            shift_scroll_pan_delta: glam::Vec2::ZERO,
55        }
56    }
57}
58
59/// Result of querying whether an action is active this frame.
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub enum ActionState {
62    /// Action is not active.
63    Inactive,
64    /// Action was just triggered (single-fire).
65    Pressed,
66    /// Action is continuously active with an associated delta.
67    Active {
68        /// Motion delta for this frame (pixels for drag/scroll actions, zero for key-held).
69        delta: glam::Vec2,
70    },
71}
72
73impl ActionState {
74    /// Returns true if the action was pressed this frame.
75    pub fn pressed(self) -> bool {
76        matches!(self, ActionState::Pressed)
77    }
78
79    /// Returns true if the action is active (pressed or held with delta).
80    pub fn is_active(self) -> bool {
81        !matches!(self, ActionState::Inactive)
82    }
83
84    /// Extract delta if active, otherwise zero.
85    pub fn delta(self) -> glam::Vec2 {
86        match self {
87            ActionState::Active { delta } => delta,
88            _ => glam::Vec2::ZERO,
89        }
90    }
91}
92
93/// Check whether a trigger's modifiers match the current frame modifiers.
94pub(crate) fn modifiers_match(required: &Modifiers, current: &Modifiers, ignore: bool) -> bool {
95    if ignore {
96        return true;
97    }
98    required == current
99}
100
101/// Evaluate a single trigger against the current frame input.
102pub(crate) fn evaluate_trigger(
103    kind: &TriggerKind,
104    activation: &ActivationMode,
105    required_mods: &Modifiers,
106    ignore_mods: bool,
107    input: &FrameInput,
108) -> ActionState {
109    if !modifiers_match(required_mods, &input.modifiers, ignore_mods) {
110        return ActionState::Inactive;
111    }
112
113    match (kind, activation) {
114        (TriggerKind::Key(key), ActivationMode::OnPress) => {
115            if input.keys_pressed.contains(key) {
116                ActionState::Pressed
117            } else {
118                ActionState::Inactive
119            }
120        }
121        (TriggerKind::Key(key), ActivationMode::WhileHeld) => {
122            if input.keys_held.contains(key) {
123                ActionState::Active {
124                    delta: glam::Vec2::ZERO,
125                }
126            } else {
127                ActionState::Inactive
128            }
129        }
130        (TriggerKind::MouseButton(btn), ActivationMode::OnPress) => {
131            if input.clicked.contains(btn) {
132                ActionState::Pressed
133            } else {
134                ActionState::Inactive
135            }
136        }
137        (TriggerKind::MouseButton(btn), ActivationMode::OnDrag) => {
138            if input.dragging.contains(btn) {
139                ActionState::Active {
140                    delta: input.drag_delta,
141                }
142            } else {
143                ActionState::Inactive
144            }
145        }
146        (TriggerKind::Scroll, ActivationMode::OnScroll) => {
147            if input.scroll_delta.abs() > 0.0 {
148                ActionState::Active {
149                    delta: glam::Vec2::new(0.0, input.scroll_delta),
150                }
151            } else {
152                ActionState::Inactive
153            }
154        }
155        _ => ActionState::Inactive,
156    }
157}