Skip to main content

graphics/
input.rs

1//! Handles keyboard and mouse input, eg for moving the camera.
2
3use lin_alg::f32::{Quaternion, Vec3};
4// todo: remove Winit from this module if you can, and make it agnostic?
5use winit::event::{DeviceEvent, ElementState, MouseScrollDelta};
6use winit::keyboard::{KeyCode, PhysicalKey::Code};
7
8use crate::{
9    ScrollBehavior,
10    camera::Camera,
11    graphics::{FWD_VEC, RIGHT_VEC, UP_VEC},
12    types::InputSettings,
13};
14
15const EPS_MOUSE: f32 = 0.00001;
16
17#[derive(Default, Debug)]
18pub struct InputsCommanded {
19    pub fwd: bool,
20    pub back: bool,
21    pub left: bool,
22    pub right: bool,
23    pub up: bool,
24    pub down: bool,
25    pub roll_ccw: bool,
26    pub roll_cw: bool,
27    pub mouse_delta_x: f32,
28    pub mouse_delta_y: f32,
29    pub run: bool,
30    pub scroll_up: bool,
31    pub scroll_down: bool,
32    pub free_look: bool,
33    pub panning: bool, // todo: Implement A/R
34    // todo: Move this A/R. Currently use it e.g. to disable scrolling moving if cursor
35    // todo is not in window,
36    pub cursor_out_of_window: bool,
37}
38
39impl InputsCommanded {
40    /// Return true if there are any inputs.
41    pub fn inputs_present(&self) -> bool {
42        // Note; We don't include `run` or `free_look` here, since they're modifiers.
43        self.fwd
44            || self.back
45            || self.left
46            || self.right
47            || self.up
48            || self.down
49            || self.roll_ccw
50            || self.roll_cw
51            || self.mouse_delta_x.abs() > EPS_MOUSE
52            || self.mouse_delta_y.abs() > EPS_MOUSE
53            || self.scroll_up
54            || self.scroll_down
55    }
56}
57
58/// Modifies the commanded inputs in place; triggered by a single input event.
59/// dt is in seconds.
60/// pub(crate) fn handle_event(event: DeviceEvent, cam: &mut Camera, input_settings: &InputSettings, dt: f32) {
61pub(crate) fn add_input_cmd(event: &DeviceEvent, inputs: &mut InputsCommanded) {
62    // This blocks all key and mouse commands from activating if the cursor has left
63    // the window.
64    if inputs.cursor_out_of_window {
65        // Resetting inputs effectively "lifts" all pressed keys, so we don't get stuck
66        // moving in a direction when the cursor leaves.
67        *inputs = InputsCommanded {
68            cursor_out_of_window: true,
69            ..Default::default()
70        };
71        return;
72    }
73
74    match event {
75        DeviceEvent::Key(key) => {
76            let Code(key_code) = key.physical_key else {
77                return;
78            };
79
80            if key.state == ElementState::Pressed {
81                match key_code {
82                    KeyCode::KeyW => {
83                        inputs.fwd = true;
84                    }
85                    KeyCode::KeyS => {
86                        inputs.back = true;
87                    }
88                    KeyCode::KeyA => {
89                        inputs.left = true;
90                    }
91                    KeyCode::KeyD => {
92                        inputs.right = true;
93                    }
94                    KeyCode::Space => {
95                        inputs.up = true;
96                    }
97                    KeyCode::KeyC => {
98                        inputs.down = true;
99                    }
100                    KeyCode::KeyQ => {
101                        inputs.roll_ccw = true;
102                    }
103                    KeyCode::KeyE => {
104                        inputs.roll_cw = true;
105                    }
106                    KeyCode::ShiftLeft => {
107                        inputs.run = true;
108                    }
109                    _ => (),
110                }
111            } else if key.state == ElementState::Released {
112                // todo: DRY
113                match key_code {
114                    KeyCode::KeyW => {
115                        inputs.fwd = false;
116                    }
117                    KeyCode::KeyS => {
118                        inputs.back = false;
119                    }
120                    KeyCode::KeyA => {
121                        inputs.left = false;
122                    }
123                    KeyCode::KeyD => {
124                        inputs.right = false;
125                    }
126                    KeyCode::Space => {
127                        inputs.up = false;
128                    }
129                    KeyCode::KeyC => {
130                        inputs.down = false;
131                    }
132                    KeyCode::KeyQ => {
133                        inputs.roll_ccw = false;
134                    }
135                    KeyCode::KeyE => {
136                        inputs.roll_cw = false;
137                    }
138                    KeyCode::ShiftLeft => {
139                        inputs.run = false;
140                    }
141                    _ => (),
142                }
143            }
144        }
145        DeviceEvent::Button { button, state } => {
146            // todo: Experiment?
147            #[cfg(target_os = "linux")]
148            let left_click = 1;
149            #[cfg(not(target_os = "linux"))]
150            let left_click = 0;
151
152            // What happened: left click (event 0) triggered behavior of event 1.
153
154            if *button == left_click {
155                inputs.free_look = match state {
156                    ElementState::Pressed => true,
157                    ElementState::Released => false,
158                }
159            }
160        }
161        DeviceEvent::MouseMotion { delta } => {
162            inputs.mouse_delta_x += delta.0 as f32;
163            inputs.mouse_delta_y += delta.1 as f32;
164        }
165        // Move the camera forward and back on scroll.
166        DeviceEvent::MouseWheel { delta } => match delta {
167            MouseScrollDelta::PixelDelta(_) => (),
168            MouseScrollDelta::LineDelta(_x, y) => {
169                if *y > 0. {
170                    inputs.scroll_down = true;
171                } else {
172                    inputs.scroll_up = true;
173                }
174            }
175        },
176        _ => (),
177    }
178}
179
180fn handle_scroll(
181    cam: &mut Camera,
182    inputs: &mut InputsCommanded,
183    input_settings: &InputSettings,
184    dt: f32,
185    movement_vec: &mut Vec3,
186    rotation: &mut Quaternion,
187    cam_moved: &mut bool,
188    cam_rotated: &mut bool,
189) {
190    if inputs.scroll_down || inputs.scroll_up {
191        if let ScrollBehavior::MoveRoll {
192            move_amt,
193            rotate_amt,
194        } = input_settings.scroll_behavior
195        {
196            if inputs.free_look {
197                // Roll if left button down while scrolling
198                let fwd = cam.orientation.rotate_vec(FWD_VEC);
199
200                let mut rot_amt = -rotate_amt * dt;
201                if inputs.scroll_down {
202                    rot_amt *= -1.; // todo: Allow reversed behavior for arc cam?
203                }
204
205                *rotation = Quaternion::from_axis_angle(fwd, rot_amt);
206                *cam_rotated = true;
207            } else {
208                // Otherwise, move forward and backward.
209                let mut movement = Vec3::new(0., 0., move_amt);
210                if inputs.scroll_up {
211                    movement *= -1.;
212                }
213                *movement_vec += movement;
214
215                *cam_moved = true;
216            }
217        }
218
219        // Immediately send the "release" command; not on a Release event like keys.
220        inputs.scroll_down = false;
221        inputs.scroll_up = false;
222    }
223}
224
225/// Used internally for inputs, and externally, e.g. to command an arc rotation.
226pub fn arc_rotation(cam: &mut Camera, axis: Vec3, amt: f32, center: Vec3) {
227    let rotation = Quaternion::from_axis_angle(axis, amt);
228
229    cam.orientation = (rotation * cam.orientation).to_normalized();
230
231    let dist = (cam.position - center).magnitude();
232    // Update position based on the new orientation.
233    cam.position = center - cam.orientation.rotate_vec(FWD_VEC) * dist;
234}
235
236/// Adjust the camera orientation and position. Return if there was a change, so we know to update the buffer.
237/// For the free (6DOF first-person) camera.
238pub fn adjust_camera_free(
239    cam: &mut Camera,
240    inputs: &mut InputsCommanded,
241    input_settings: &InputSettings,
242    dt: f32,
243) -> bool {
244    let mut move_amt = input_settings.move_sens * dt;
245    let mut rotate_key_amt = input_settings.rotate_key_sens * dt;
246
247    let mut cam_moved = false;
248    let mut cam_rotated = false;
249
250    let mut movement_vec = Vec3::new_zero();
251    let mut rotation = Quaternion::new_identity();
252
253    if inputs.run {
254        move_amt *= input_settings.run_factor;
255        rotate_key_amt *= input_settings.run_factor;
256    }
257
258    if inputs.fwd {
259        movement_vec.z += move_amt;
260        cam_moved = true;
261    } else if inputs.back {
262        movement_vec.z -= move_amt;
263        cam_moved = true;
264    }
265
266    if inputs.right {
267        movement_vec.x += move_amt;
268        cam_moved = true;
269    } else if inputs.left {
270        movement_vec.x -= move_amt;
271        cam_moved = true;
272    }
273
274    if inputs.up {
275        movement_vec.y += move_amt;
276        cam_moved = true;
277    } else if inputs.down {
278        movement_vec.y -= move_amt;
279        cam_moved = true;
280    }
281
282    if inputs.roll_cw {
283        let fwd = cam.orientation.rotate_vec(FWD_VEC);
284        rotation = Quaternion::from_axis_angle(fwd, -rotate_key_amt);
285        cam_rotated = true;
286    } else if inputs.roll_ccw {
287        let fwd = cam.orientation.rotate_vec(FWD_VEC);
288        rotation = Quaternion::from_axis_angle(fwd, rotate_key_amt);
289        cam_rotated = true;
290    }
291
292    if inputs.free_look
293        && (inputs.mouse_delta_x.abs() > EPS_MOUSE || inputs.mouse_delta_y.abs() > EPS_MOUSE)
294    {
295        let rotate_amt = input_settings.rotate_sens * dt;
296        let up = cam.orientation.rotate_vec(-UP_VEC);
297        let right = cam.orientation.rotate_vec(-RIGHT_VEC);
298
299        rotation = Quaternion::from_axis_angle(up, -inputs.mouse_delta_x * rotate_amt)
300            * Quaternion::from_axis_angle(right, -inputs.mouse_delta_y * rotate_amt)
301            * rotation;
302
303        cam_rotated = true;
304    }
305
306    handle_scroll(
307        cam,
308        inputs,
309        input_settings,
310        dt,
311        &mut movement_vec,
312        &mut rotation,
313        &mut cam_moved,
314        &mut cam_rotated,
315    );
316
317    // todo: Handle middleclick + drag here too. Move `mol_dock`'s impl here.
318
319    if cam_rotated {
320        cam.orientation = (rotation * cam.orientation).to_normalized();
321    }
322
323    if cam_moved {
324        cam.position += cam.orientation.rotate_vec(movement_vec);
325    }
326
327    cam_moved || cam_rotated
328}
329
330/// Adjust the camera orientation and position. Return if there was a change, so we know to update the buffer.
331/// For the arc (orbital) camera.
332pub fn adjust_camera_arc(
333    cam: &mut Camera,
334    inputs: &mut InputsCommanded,
335    input_settings: &InputSettings,
336    center: Vec3,
337    dt: f32,
338) -> bool {
339    // How fast we rotate, derived from your input settings:
340    // Track if we actually moved/rotated:
341    let mut cam_moved = false;
342    let mut cam_rotated = false;
343
344    let mut movement_vec = Vec3::new_zero();
345    let mut rotation = Quaternion::new_identity();
346
347    // todo: Combine this fn with adjust_free, and accept ControlScheme as a param.
348
349    // Inverse of free
350    let rotate_key_amt = -input_settings.rotate_key_sens * dt;
351
352    if inputs.roll_cw {
353        let fwd = cam.orientation.rotate_vec(FWD_VEC);
354        rotation = Quaternion::from_axis_angle(fwd, -rotate_key_amt);
355        cam_rotated = true;
356    } else if inputs.roll_ccw {
357        let fwd = cam.orientation.rotate_vec(FWD_VEC);
358        rotation = Quaternion::from_axis_angle(fwd, rotate_key_amt);
359        cam_rotated = true;
360    }
361
362    let mut skip_move_vec = false;
363    // Only rotate if "free look" is active and the mouse moved enough:
364    if inputs.free_look
365        && (inputs.mouse_delta_x.abs() > EPS_MOUSE || inputs.mouse_delta_y.abs() > EPS_MOUSE)
366    {
367        let rotate_amt = input_settings.rotate_sens * dt;
368        let up = cam.orientation.rotate_vec(-UP_VEC);
369        let right = cam.orientation.rotate_vec(-RIGHT_VEC);
370
371        // Rotation logic: Equivalent to the free camera.
372        rotation = Quaternion::from_axis_angle(up, -inputs.mouse_delta_x * rotate_amt)
373            * Quaternion::from_axis_angle(right, -inputs.mouse_delta_y * rotate_amt);
374
375        // Distance between cam and center is invariant under this change.
376        skip_move_vec = true;
377
378        cam_moved = true;
379        cam_rotated = true;
380    }
381
382    handle_scroll(
383        cam,
384        inputs,
385        input_settings,
386        dt,
387        &mut movement_vec,
388        &mut rotation,
389        &mut cam_moved,
390        &mut cam_rotated,
391    );
392
393    // todo: Handle middleclick + drag here too. Move `mol_dock`'s impl here.
394
395    // note: We are not using our `arc_roation` fn here due to needing to handle both x and y. Hmm.
396    // arc_rotation(cam, axis, amt, center);
397
398    if cam_rotated {
399        cam.orientation = (rotation * cam.orientation).to_normalized();
400    }
401
402    if cam_moved && !skip_move_vec {
403        cam.position += cam.orientation.rotate_vec(movement_vec);
404    } else if cam_moved {
405        // todo: Bit odd to break this off from the above.
406        let dist = (cam.position - center).magnitude();
407        // Update position based on the new orientation.
408        cam.position = center - cam.orientation.rotate_vec(FWD_VEC) * dist;
409    }
410
411    cam_moved || cam_rotated
412}