Skip to main content

dreamwell_runtime/
input.rs

1// Input — winit event adapter for the Dreamwell runtime.
2// Translates winit events into engine VirtualKeys and delegates to FabricInput.
3
4use dreamwell_engine::input::VirtualKey;
5use dreamwell_fabric::FabricInput;
6use dreamwell_gpu::camera::Camera;
7use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
8use winit::keyboard::{KeyCode, PhysicalKey};
9
10const SCROLL_DEAD_ZONE: f32 = 0.01;
11const CURSOR_DEAD_ZONE: f32 = 0.1;
12const PAN_SPEED: f32 = 0.5;
13const SCROLL_SENSITIVITY: f32 = 0.5;
14const PIXEL_SCROLL_SCALE: f32 = 0.01;
15
16/// Orbit camera sensitivity (radians per pixel of mouse movement).
17const ORBIT_YAW_SENSITIVITY: f32 = 0.005;
18/// Vertical orbit is slightly less sensitive than horizontal (feels natural).
19const ORBIT_PITCH_SENSITIVITY: f32 = 0.003;
20
21/// Runtime action vocabulary — every player input mapped to a control field change.
22/// Cross-platform: PC first, maps to Xbox/PS/Switch controllers.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum RuntimeAction {
25    /// WASD / Left Stick → camera-relative direction + target speed + phase bias + shape compression.
26    MoveIntent,
27    /// Mouse / Right Stick → orbit yaw/pitch.
28    LookIntent,
29    /// Left Shift / L3 → accel cap increase + drag reduction + cohesion spike + forward compression + FOV push.
30    BurstStart,
31    BurstEnd,
32    /// Q / LB / L1 / L → coherence → 1.0 (compact fibonacci particle).
33    SetFormCohere,
34    /// E / RB / R1 / R → coherence → 0.0 (broad wave fabric).
35    SetFormWave,
36    /// Left Ctrl / LT / L2 / ZL → increase cohesion + reduce amplitude + strengthen restoring force + tighten collision.
37    GatherStart,
38    GatherEnd,
39    /// Left Mouse / RT / R2 / ZR → emit/wave push (analog strength).
40    PrimaryActionStart,
41    PrimaryActionEnd,
42    /// F / X / Square / Y → POI couple.
43    Interact,
44    /// Space / A / Cross / B → vertical impulse.
45    Jump,
46    /// C / Y / Triangle / X → snap camera behind player.
47    CameraRecenter,
48}
49
50/// Runtime input state. Wraps FabricInput with winit-specific translation.
51pub struct InputState {
52    pub fabric: FabricInput,
53    /// Raw scroll delta for camera application (before dead zone).
54    scroll_raw: f32,
55    /// Active touch points for mobile input.
56    active_touches: std::collections::HashMap<u64, [f32; 2]>,
57    /// Accumulated orbit yaw delta from right-click drag (consumed by chase camera).
58    pub orbit_dx: f32,
59    /// Accumulated orbit pitch delta from right-click drag (consumed by chase camera).
60    pub orbit_dy: f32,
61    /// Scroll zoom delta (consumed by chase camera distance).
62    pub scroll_delta: f32,
63}
64
65impl InputState {
66    pub fn new() -> Self {
67        Self {
68            fabric: FabricInput::new(),
69            scroll_raw: 0.0,
70            active_touches: std::collections::HashMap::new(),
71            orbit_dx: 0.0,
72            orbit_dy: 0.0,
73            scroll_delta: 0.0,
74        }
75    }
76
77    /// Translate a winit KeyEvent into VirtualKey press/release.
78    pub fn handle_keyboard(&mut self, event: &KeyEvent) {
79        if let PhysicalKey::Code(code) = event.physical_key {
80            if let Some(vk) = winit_to_virtual_key(code) {
81                match event.state {
82                    ElementState::Pressed => self.fabric.key_down(vk),
83                    ElementState::Released => self.fabric.key_up(vk),
84                }
85            }
86        }
87    }
88
89    /// Translate a winit mouse button event.
90    pub fn handle_mouse_button(&mut self, button: MouseButton, state: ElementState) {
91        let vk = match button {
92            MouseButton::Left => VirtualKey::MouseLeft,
93            MouseButton::Right => VirtualKey::MouseRight,
94            MouseButton::Middle => VirtualKey::MouseMiddle,
95            MouseButton::Back => VirtualKey::MouseButton4,
96            MouseButton::Forward => VirtualKey::MouseButton5,
97            _ => return,
98        };
99        match state {
100            ElementState::Pressed => self.fabric.key_down(vk),
101            ElementState::Released => self.fabric.key_up(vk),
102        }
103    }
104
105    /// Feed cursor movement.
106    pub fn handle_cursor_move(&mut self, x: f32, y: f32) {
107        self.fabric.cursor_moved(x, y);
108    }
109
110    /// Feed scroll wheel.
111    pub fn handle_scroll(&mut self, delta: &MouseScrollDelta) {
112        let y = match delta {
113            MouseScrollDelta::LineDelta(_, y) => *y,
114            MouseScrollDelta::PixelDelta(pos) => pos.y as f32 * PIXEL_SCROLL_SCALE,
115        };
116        self.scroll_raw = y;
117        self.fabric.scroll(y);
118    }
119
120    /// Apply input state to camera. `dt` is frame delta time in seconds
121    /// for frame-rate independent movement.
122    pub fn apply_to_camera(&mut self, camera: &mut Camera, dt: f32) {
123        let frame = self.fabric.build_frame();
124        let dt_pan = PAN_SPEED * dt * 60.0; // Normalize to 60fps baseline
125
126        // Scroll zoom
127        if self.scroll_raw.abs() > SCROLL_DEAD_ZONE {
128            camera.zoom(-self.scroll_raw * SCROLL_SENSITIVITY);
129            self.scroll_raw = 0.0;
130        }
131
132        // Right-click drag: rotate
133        if frame.has_action(dreamwell_engine::input::InputAction::CameraRotate) {
134            let [dx, dy] = frame.cursor_delta;
135            if dx.abs() > CURSOR_DEAD_ZONE || dy.abs() > CURSOR_DEAD_ZONE {
136                camera.rotate(dx, dy);
137            }
138        }
139
140        // Middle-click drag: pan
141        if frame.has_action(dreamwell_engine::input::InputAction::CameraPan) {
142            let [dx, dy] = frame.cursor_delta;
143            if dx.abs() > CURSOR_DEAD_ZONE || dy.abs() > CURSOR_DEAD_ZONE {
144                camera.pan(-dx, dy);
145            }
146        }
147
148        // WASD: pan (from movement vector), dt-scaled for frame-rate independence.
149        let [mx, my] = frame.movement;
150        if my > 0.0 {
151            camera.pan(0.0, dt_pan);
152        }
153        if my < 0.0 {
154            camera.pan(0.0, -dt_pan);
155        }
156        if mx < 0.0 {
157            camera.pan(dt_pan, 0.0);
158        }
159        if mx > 0.0 {
160            camera.pan(-dt_pan, 0.0);
161        }
162    }
163
164    /// Apply only mouse/scroll input for chase camera (no WASD pan).
165    /// Used when simulation is active and WASD drives the player kernel.
166    /// Accumulates orbit_dx, orbit_dy, and scroll_delta for the chase camera in app.rs.
167    pub fn apply_mouse_to_camera(&mut self, _camera: &mut Camera, _dt: f32) {
168        // Scroll zoom → accumulate delta for chase camera distance
169        if self.scroll_raw.abs() > SCROLL_DEAD_ZONE {
170            self.scroll_delta += -self.scroll_raw * SCROLL_SENSITIVITY;
171            self.scroll_raw = 0.0;
172        }
173
174        // Right-click drag → accumulate orbit yaw + pitch delta
175        let frame = self.fabric.build_frame();
176        if frame.has_action(dreamwell_engine::input::InputAction::CameraRotate) {
177            let [dx, dy] = frame.cursor_delta;
178            if dx.abs() > CURSOR_DEAD_ZONE {
179                self.orbit_dx += dx * ORBIT_YAW_SENSITIVITY;
180            }
181            if dy.abs() > CURSOR_DEAD_ZONE {
182                self.orbit_dy += dy * ORBIT_PITCH_SENSITIVITY;
183            }
184        }
185    }
186
187    /// Poll gamepad input and apply to FabricInput.
188    /// Only available when the `gamepad` feature is enabled.
189    #[cfg(feature = "gamepad")]
190    pub fn handle_gamepad(&mut self, gilrs: &mut gilrs::Gilrs, camera: &mut Camera) {
191        while let Some(event) = gilrs.next_event() {
192            // Process but don't act on events — we read state below.
193            let _ = event;
194        }
195        // Read first active gamepad.
196        if let Some((_id, gamepad)) = gilrs.gamepads().next() {
197            let lx = gamepad.value(gilrs::Axis::LeftStickX);
198            let ly = gamepad.value(gilrs::Axis::LeftStickY);
199            let rx = gamepad.value(gilrs::Axis::RightStickX);
200            let ry = gamepad.value(gilrs::Axis::RightStickY);
201
202            const DEAD_ZONE: f32 = 0.15;
203
204            // Left stick: camera pan.
205            if lx.abs() > DEAD_ZONE || ly.abs() > DEAD_ZONE {
206                camera.pan(-lx * PAN_SPEED, ly * PAN_SPEED);
207            }
208
209            // Right stick: camera rotate.
210            if rx.abs() > DEAD_ZONE || ry.abs() > DEAD_ZONE {
211                camera.rotate(rx * 2.0, ry * 2.0);
212            }
213
214            // Triggers: zoom.
215            let lt = gamepad.value(gilrs::Axis::LeftZ);
216            let rt = gamepad.value(gilrs::Axis::RightZ);
217            let zoom_delta = rt - lt;
218            if zoom_delta.abs() > DEAD_ZONE {
219                camera.zoom(zoom_delta * SCROLL_SENSITIVITY);
220            }
221        }
222    }
223
224    /// Handle touch input (mobile/tablet).
225    /// Single-finger drag: pan. Two-finger pinch: zoom. Two-finger rotate: orbit.
226    pub fn handle_touch(&mut self, phase: TouchPhase, id: u64, x: f32, y: f32) {
227        match phase {
228            TouchPhase::Started => {
229                self.active_touches.insert(id, [x, y]);
230            }
231            TouchPhase::Moved => {
232                if let Some(prev) = self.active_touches.get_mut(&id) {
233                    let dx = x - prev[0];
234                    let dy = y - prev[1];
235                    *prev = [x, y];
236
237                    if self.active_touches.len() == 1 {
238                        // Single finger: pan.
239                        self.fabric.cursor_moved(dx, dy);
240                    }
241                }
242            }
243            TouchPhase::Ended | TouchPhase::Cancelled => {
244                self.active_touches.remove(&id);
245            }
246        }
247    }
248
249    /// End-of-frame cleanup.
250    pub fn end_frame(&mut self) {
251        self.fabric.end_frame();
252        self.scroll_raw = 0.0;
253        self.orbit_dx = 0.0;
254        self.orbit_dy = 0.0;
255        self.scroll_delta = 0.0;
256    }
257}
258
259/// Touch event phase (matches winit::event::TouchPhase semantics).
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum TouchPhase {
262    Started,
263    Moved,
264    Ended,
265    Cancelled,
266}
267
268impl Default for InputState {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274/// Translate winit KeyCode to engine VirtualKey.
275fn winit_to_virtual_key(code: KeyCode) -> Option<VirtualKey> {
276    Some(match code {
277        KeyCode::KeyA => VirtualKey::A,
278        KeyCode::KeyB => VirtualKey::B,
279        KeyCode::KeyC => VirtualKey::C,
280        KeyCode::KeyD => VirtualKey::D,
281        KeyCode::KeyE => VirtualKey::E,
282        KeyCode::KeyF => VirtualKey::F,
283        KeyCode::KeyG => VirtualKey::G,
284        KeyCode::KeyH => VirtualKey::H,
285        KeyCode::KeyI => VirtualKey::I,
286        KeyCode::KeyJ => VirtualKey::J,
287        KeyCode::KeyK => VirtualKey::K,
288        KeyCode::KeyL => VirtualKey::L,
289        KeyCode::KeyM => VirtualKey::M,
290        KeyCode::KeyN => VirtualKey::N,
291        KeyCode::KeyO => VirtualKey::O,
292        KeyCode::KeyP => VirtualKey::P,
293        KeyCode::KeyQ => VirtualKey::Q,
294        KeyCode::KeyR => VirtualKey::R,
295        KeyCode::KeyS => VirtualKey::S,
296        KeyCode::KeyT => VirtualKey::T,
297        KeyCode::KeyU => VirtualKey::U,
298        KeyCode::KeyV => VirtualKey::V,
299        KeyCode::KeyW => VirtualKey::W,
300        KeyCode::KeyX => VirtualKey::X,
301        KeyCode::KeyY => VirtualKey::Y,
302        KeyCode::KeyZ => VirtualKey::Z,
303        KeyCode::Digit0 => VirtualKey::Num0,
304        KeyCode::Digit1 => VirtualKey::Num1,
305        KeyCode::Digit2 => VirtualKey::Num2,
306        KeyCode::Digit3 => VirtualKey::Num3,
307        KeyCode::Digit4 => VirtualKey::Num4,
308        KeyCode::Digit5 => VirtualKey::Num5,
309        KeyCode::Digit6 => VirtualKey::Num6,
310        KeyCode::Digit7 => VirtualKey::Num7,
311        KeyCode::Digit8 => VirtualKey::Num8,
312        KeyCode::Digit9 => VirtualKey::Num9,
313        KeyCode::ArrowUp => VirtualKey::ArrowUp,
314        KeyCode::ArrowDown => VirtualKey::ArrowDown,
315        KeyCode::ArrowLeft => VirtualKey::ArrowLeft,
316        KeyCode::ArrowRight => VirtualKey::ArrowRight,
317        KeyCode::ShiftLeft => VirtualKey::ShiftLeft,
318        KeyCode::ShiftRight => VirtualKey::ShiftRight,
319        KeyCode::ControlLeft => VirtualKey::CtrlLeft,
320        KeyCode::ControlRight => VirtualKey::CtrlRight,
321        KeyCode::AltLeft => VirtualKey::AltLeft,
322        KeyCode::AltRight => VirtualKey::AltRight,
323        KeyCode::Space => VirtualKey::Space,
324        KeyCode::Enter => VirtualKey::Enter,
325        KeyCode::Escape => VirtualKey::Escape,
326        KeyCode::Tab => VirtualKey::Tab,
327        KeyCode::Backspace => VirtualKey::Backspace,
328        KeyCode::Delete => VirtualKey::Delete,
329        KeyCode::Home => VirtualKey::Home,
330        KeyCode::End => VirtualKey::End,
331        KeyCode::PageUp => VirtualKey::PageUp,
332        KeyCode::PageDown => VirtualKey::PageDown,
333        KeyCode::F1 => VirtualKey::F1,
334        KeyCode::F2 => VirtualKey::F2,
335        KeyCode::F3 => VirtualKey::F3,
336        KeyCode::F4 => VirtualKey::F4,
337        KeyCode::F5 => VirtualKey::F5,
338        KeyCode::F6 => VirtualKey::F6,
339        KeyCode::F7 => VirtualKey::F7,
340        KeyCode::F8 => VirtualKey::F8,
341        KeyCode::F9 => VirtualKey::F9,
342        KeyCode::F10 => VirtualKey::F10,
343        KeyCode::F11 => VirtualKey::F11,
344        KeyCode::F12 => VirtualKey::F12,
345        _ => return None,
346    })
347}