Skip to main content

veloren_voxygen/
window.rs

1use crate::{
2    error::Error,
3    game_input::GameInput,
4    render::Renderer,
5    settings::{ControlSettings, ControllerSettings, Settings, controller::*},
6    ui,
7};
8use common_base::span;
9use crossbeam_channel as channel;
10use gilrs::{Button as GilButton, EventType, Gilrs};
11use hashbrown::{HashMap, hash_set::Iter};
12use itertools::Itertools;
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15use strum::{AsRefStr, EnumIter};
16use tracing::{error, warn};
17use vek::*;
18use winit::monitor::VideoModeHandle;
19
20/// Represents a key that the game menus recognise after input mapping
21#[derive(
22    Clone,
23    Copy,
24    Debug,
25    PartialEq,
26    Eq,
27    PartialOrd,
28    Ord,
29    Hash,
30    Deserialize,
31    Serialize,
32    AsRefStr,
33    EnumIter,
34)]
35pub enum MenuInput {
36    Up,
37    Down,
38    Left,
39    Right,
40    ScrollUp,
41    ScrollDown,
42    ScrollLeft,
43    ScrollRight,
44    PageDown,
45    PageUp,
46    Apply,
47    Back,
48    Exit,
49    LocalFocus,
50    GlobalFocus,
51    EmulateLeftClick,
52    EmulateRightClick,
53}
54
55/// Manages an Iterator over MenuInputs or GameInputs
56pub enum MappedInput<'a> {
57    Menu(Iter<'a, MenuInput>),
58    Game(Iter<'a, GameInput>),
59}
60
61impl MenuInput {
62    pub fn get_localization_key(&self) -> &str { self.as_ref() }
63}
64
65#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
66pub enum AnalogMenuInput {
67    MoveX(f32),
68    MoveY(f32),
69    ScrollX(f32),
70    ScrollY(f32),
71}
72
73#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
74pub enum AnalogGameInput {
75    MovementX(f32),
76    MovementY(f32),
77    CameraX(f32),
78    CameraY(f32),
79}
80
81/// Represents an incoming event from the window.
82#[derive(Clone, Debug)]
83pub enum Event {
84    /// The window has been requested to close.
85    Close,
86    /// The window has been resized.
87    Resize(Vec2<u32>),
88    /// The window scale factor has been changed
89    ScaleFactorChanged(f64),
90    /// The window has been moved.
91    Moved(Vec2<u32>),
92    /// The cursor has been panned across the screen while grabbed.
93    CursorPan(Vec2<f32>),
94    /// The cursor has been moved across the screen while ungrabbed.
95    CursorMove(Vec2<f32>),
96    /// A mouse button has been pressed or released
97    MouseButton(MouseButton, PressState),
98    /// The camera has been requested to zoom.
99    Zoom(f32),
100    /// A key that the game recognises has been pressed or released.
101    InputUpdate(GameInput, bool),
102    /// Event that the ui uses.
103    Ui(ui::Event),
104    /// Event that the iced ui uses.
105    IcedUi(ui::ice::Event),
106    /// The view distance has changed.
107    ViewDistanceChanged(u32),
108    /// Game settings have changed.
109    SettingsChanged,
110    /// The window is (un)focused
111    Focused(bool),
112    /// A key that the game recognises for menu navigation has been pressed or
113    /// released
114    MenuInput(MenuInput, bool),
115    /// Update of the analog inputs recognized by the menus
116    AnalogMenuInput(AnalogMenuInput),
117    /// Update of the analog inputs recognized by the game
118    AnalogGameInput(AnalogGameInput),
119    /// We tried to save a screenshot
120    ScreenshotMessage(String),
121}
122
123pub type MouseButton = winit::event::MouseButton;
124pub type PressState = winit::event::ElementState;
125pub type EventLoop = winit::event_loop::EventLoop<()>;
126
127#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
128pub enum KeyMouse {
129    Key(winit::keyboard::Key),
130    Mouse(winit::event::MouseButton),
131}
132
133impl KeyMouse {
134    pub fn into_upper(mut self) -> Self {
135        if let KeyMouse::Key(winit::keyboard::Key::Character(c)) = &mut self {
136            *c = c.to_ascii_uppercase().into();
137        }
138        self
139    }
140
141    /// Returns key description (e.g Left Shift)
142    pub fn display_string(&self) -> String {
143        use self::KeyMouse::*;
144        use winit::{event::MouseButton, keyboard::Key::*};
145
146        match self {
147            Key(key) => match key {
148                Named(key) => format!("{key:?}"),
149                Character(c) => c.to_string(),
150                Unidentified(key) => format!("Unknown ({key:?})"),
151                Dead(dead) => format!("Dead ({dead:?})"),
152            },
153            Mouse(MouseButton::Left) => String::from("Left Click"),
154            Mouse(MouseButton::Right) => String::from("Right Click"),
155            Mouse(MouseButton::Middle) => String::from("Middle Click"),
156            Mouse(MouseButton::Forward) => String::from("Mouse Forward"),
157            Mouse(MouseButton::Back) => String::from("Mouse Back"),
158            Mouse(MouseButton::Other(button)) => {
159                // Additional mouse buttons after middle click start at 1
160                format!("Mouse {}", button + 3)
161            },
162        }
163    }
164
165    /// If it exists, returns the shortened version of a key name
166    /// (e.g. Left Click -> M1)
167    pub fn try_shortened(&self) -> Option<String> {
168        use self::KeyMouse::*;
169        use winit::event::MouseButton;
170        let key_string = match self {
171            Mouse(MouseButton::Left) => "M1",
172            Mouse(MouseButton::Right) => "M2",
173            Mouse(MouseButton::Middle) => "M3",
174            Mouse(MouseButton::Other(button)) => {
175                // Additional mouse buttons after middle click start at 1
176                return Some(format!("M{}", button + 3));
177            },
178            _ => return None,
179        };
180
181        Some(key_string.to_owned())
182    }
183
184    /// Returns shortest name of key (e.g. Left Click - M1)
185    /// If key doesn't have shorter version, use regular one.
186    ///
187    /// Use it in case if space does really matter.
188    pub fn display_shortest(&self) -> String {
189        self.try_shortened()
190            .unwrap_or_else(|| self.display_string())
191    }
192}
193
194#[derive(Clone, Copy, Debug)]
195pub enum RemappingMode {
196    RemapKeyboard(GameInput),
197    RemapKeyboardMenu(MenuInput),
198    RemapGamepadButtons(GameInput),
199    RemapGamepadLayers(GameInput),
200    RemapGamepadMenu(MenuInput),
201    None,
202}
203
204// TODO: implement controller layout detection/configuration
205#[derive(Clone, Copy, Debug)]
206pub enum ControllerType {
207    Xbox,
208    Nintendo,
209    Playstation,
210    None,
211}
212
213#[derive(Clone, Copy, Debug, PartialEq)]
214pub enum LastInput {
215    Keyboard,
216    Mouse,
217    Controller,
218}
219
220pub struct Window {
221    renderer: Renderer,
222    window: Arc<winit::window::Window>,
223    cursor_grabbed: bool,
224    pub pan_sensitivity: u32,
225    pub zoom_sensitivity: u32,
226    pub zoom_inversion: bool,
227    pub mouse_y_inversion: bool,
228    fullscreen: FullScreenSettings,
229    modifiers: winit::keyboard::ModifiersState,
230    // Track if at least one Resized event has occured since the last `fetch_events` call
231    // Used for deduplication of resizes.
232    resized: bool,
233    scale_factor: f64,
234    needs_refresh_resize: bool,
235    keypress_map: HashMap<GameInput, winit::event::ElementState>,
236    pub remapping_mode: RemappingMode,
237    events: Vec<Event>,
238    pub focused: bool,
239    gilrs: Option<Gilrs>,
240    pub controller_modifiers: Vec<Button>,
241    pub menu_open: bool,
242    cursor_position: winit::dpi::PhysicalPosition<f64>,
243    mouse_emulation_vec: Vec2<f32>,
244    controller_type: ControllerType,
245    last_input: LastInput,
246    // Currently used to send and receive screenshot result messages
247    message_sender: channel::Sender<String>,
248    message_receiver: channel::Receiver<String>,
249    // Used for screenshots & fullscreen toggle to deduplicate/postpone to after event handler
250    take_screenshot: bool,
251    toggle_fullscreen: bool,
252    // Used for storing the state of checkboxes on controls>gamepad>gamelayers, does not need to be
253    // saved to file, so initialized here
254    pub gamelayer_mod1: bool,
255    pub gamelayer_mod2: bool,
256}
257
258impl Window {
259    pub fn new(
260        settings: &Settings,
261        runtime: &tokio::runtime::Runtime,
262    ) -> Result<(Window, EventLoop), Error> {
263        let event_loop = EventLoop::new().unwrap();
264
265        let window = settings.graphics.window;
266
267        #[allow(unused_mut)] //ensure no weird issues on different platforms
268        let mut attributes = winit::window::Window::default_attributes()
269            .with_title("Veloren")
270            .with_inner_size(winit::dpi::LogicalSize::new(
271                window.size[0] as f64,
272                window.size[1] as f64,
273            ))
274            .with_maximized(window.maximised);
275
276        #[cfg(not(any(target_os = "windows", target_os = "macos")))]
277        {
278            use winit::platform::wayland::WindowAttributesExtWayland;
279            attributes = attributes.with_name("net.veloren.veloren", "veloren");
280        }
281
282        // Avoid cpal / winit OleInitialize conflict
283        // See: https://github.com/rust-windowing/winit/pull/1524
284        #[cfg(target_family = "windows")]
285        let attributes = winit::platform::windows::WindowAttributesExtWindows::with_drag_and_drop(
286            attributes, false,
287        );
288
289        #[expect(deprecated)]
290        let window = Arc::new(event_loop.create_window(attributes).unwrap());
291
292        let renderer = Renderer::new(
293            Arc::clone(&window),
294            settings.graphics.render_mode.clone(),
295            runtime,
296        )?;
297
298        let keypress_map = HashMap::new();
299
300        let gilrs = match Gilrs::new() {
301            Ok(gilrs) => Some(gilrs),
302            Err(gilrs::Error::NotImplemented(_dummy)) => {
303                warn!("Controller input is unsupported on this platform.");
304                None
305            },
306            Err(gilrs::Error::InvalidAxisToBtn) => {
307                error!(
308                    "Invalid AxisToBtn controller mapping. Falling back to no controller support."
309                );
310                None
311            },
312            Err(gilrs::Error::Other(e)) => {
313                error!(
314                    ?e,
315                    "Platform-specific error when creating a Gilrs instance. Falling back to no \
316                     controller support."
317                );
318                None
319            },
320            Err(e) => {
321                error!(
322                    ?e,
323                    "Unspecified error when creating a Gilrs instance. Falling back to no \
324                     controller support."
325                );
326                None
327            },
328        };
329
330        let (message_sender, message_receiver): (
331            channel::Sender<String>,
332            channel::Receiver<String>,
333        ) = channel::unbounded::<String>();
334
335        let scale_factor = window.scale_factor();
336
337        let mut this = Self {
338            renderer,
339            window,
340            cursor_grabbed: false,
341            pan_sensitivity: settings.gameplay.pan_sensitivity,
342            zoom_sensitivity: settings.gameplay.zoom_sensitivity,
343            zoom_inversion: settings.gameplay.zoom_inversion,
344            mouse_y_inversion: settings.gameplay.mouse_y_inversion,
345            fullscreen: FullScreenSettings::default(),
346            modifiers: Default::default(),
347            scale_factor,
348            resized: false,
349            needs_refresh_resize: false,
350            keypress_map,
351            remapping_mode: RemappingMode::None,
352            events: Vec::new(),
353            focused: true,
354            gilrs,
355            controller_modifiers: Vec::new(),
356            menu_open: false,
357            cursor_position: winit::dpi::PhysicalPosition::new(0.0, 0.0),
358            mouse_emulation_vec: Vec2::zero(),
359            controller_type: ControllerType::Xbox,
360            last_input: LastInput::Mouse,
361            // Currently used to send and receive screenshot result messages
362            message_sender,
363            message_receiver,
364            take_screenshot: false,
365            toggle_fullscreen: false,
366            gamelayer_mod1: true,
367            gamelayer_mod2: false,
368        };
369
370        this.set_fullscreen_mode(settings.graphics.fullscreen);
371
372        Ok((this, event_loop))
373    }
374
375    pub fn renderer(&self) -> &Renderer { &self.renderer }
376
377    pub fn renderer_mut(&mut self) -> &mut Renderer { &mut self.renderer }
378
379    pub fn resolve_deduplicated_events(
380        &mut self,
381        settings: &mut Settings,
382        config_dir: &std::path::Path,
383    ) {
384        // Handle screenshots and toggling fullscreen
385        if self.take_screenshot {
386            self.take_screenshot = false;
387            self.take_screenshot(settings);
388        }
389        if self.toggle_fullscreen {
390            self.toggle_fullscreen = false;
391            self.toggle_fullscreen(settings, config_dir);
392        }
393    }
394
395    pub fn fetch_events(&mut self, settings: &mut Settings) -> Vec<Event> {
396        span!(_guard, "fetch_events", "Window::fetch_events");
397
398        let controller = &mut settings.controller;
399        // Refresh ui size (used when changing playstates)
400        if self.needs_refresh_resize {
401            let scale_factor = self.window.scale_factor();
402            let physical = self.window.inner_size();
403
404            let logical_size =
405                Vec2::from(<(f64, f64)>::from(physical.to_logical::<f64>(scale_factor)));
406            self.events
407                .push(Event::Ui(ui::Event::new_resize(logical_size)));
408            self.events.push(Event::IcedUi(iced::Event::Window(
409                iced::window::Event::Resized {
410                    width: logical_size.x as u32,
411                    height: logical_size.y as u32,
412                },
413            )));
414            self.events.push(Event::ScaleFactorChanged(scale_factor));
415            self.needs_refresh_resize = false;
416        }
417
418        // Handle deduplicated resizing that occured
419        if self.resized {
420            self.resized = false;
421            // We don't use the size provided by the event because more resize events could
422            // have happened since, making the value outdated, so we must query directly
423            // from the window to prevent errors
424            let physical = self.window.inner_size();
425            let scale_factor = self.window.scale_factor();
426            let is_maximized = self.window.is_maximized();
427
428            self.renderer
429                .on_resize(Vec2::new(physical.width, physical.height));
430            self.events
431                .push(Event::Resize(Vec2::new(physical.width, physical.height)));
432
433            let logical_size =
434                Vec2::from(<(f64, f64)>::from(physical.to_logical::<f64>(scale_factor)));
435
436            // Emit event for the UI
437            self.events
438                .push(Event::Ui(ui::Event::new_resize(logical_size)));
439            self.events.push(Event::IcedUi(iced::Event::Window(
440                iced::window::Event::Resized {
441                    width: logical_size.x as u32,
442                    height: logical_size.y as u32,
443                },
444            )));
445
446            // Save new window state in settings
447            //
448            // We don't save the size if it's less than 1 because wgpu fails to create a
449            // surface with zero size.
450            if logical_size.x >= 1.0 && logical_size.y >= 1.0 {
451                settings.graphics.window.size = [logical_size.x as u32, logical_size.y as u32];
452            }
453            settings.graphics.window.maximised = is_maximized;
454        }
455
456        // Receive any messages sent through the message channel
457        for message in self.message_receiver.try_iter() {
458            self.events.push(Event::ScreenshotMessage(message))
459        }
460
461        if let Some(gilrs) = &mut self.gilrs {
462            while let Some(event) = gilrs.next_event() {
463                fn handle_buttons(
464                    settings: &mut ControllerSettings,
465                    remapping: &mut RemappingMode,
466                    modifiers: &mut Vec<Button>,
467                    events: &mut Vec<Event>,
468                    button: &Button,
469                    is_pressed: bool,
470                    last_input: &mut LastInput,
471                    mod1: bool,
472                    mod2: bool,
473                    menu_open: bool,
474                ) {
475                    // update last input to be controller if button was pressed
476                    *last_input = LastInput::Controller;
477
478                    if settings.modifier_buttons.contains(button) {
479                        if is_pressed {
480                            modifiers.push(*button);
481                        // There is a possibility of voxygen not having
482                        // registered the initial press event (either because it
483                        // hadn't started yet, or whatever else) hence the
484                        // modifier has no position in the list, unwrapping
485                        // here would cause a crash in those cases
486                        } else if let Some(index) =
487                            modifiers.iter().position(|modifier| modifier == button)
488                        {
489                            modifiers.remove(index);
490                        }
491                    }
492
493                    if let Some(mapped_inputs) = Window::map_controller_input(
494                        settings, remapping, modifiers, button, mod1, mod2, menu_open,
495                    ) {
496                        match mapped_inputs {
497                            // Fetch actions to perform; if nothing was returned, then either no
498                            // action was assigned to the input, or a remapping action was performed
499                            MappedInput::Menu(menu_inputs) => {
500                                for menu_input in menu_inputs {
501                                    events.push(Event::MenuInput(*menu_input, is_pressed));
502                                }
503                            },
504                            MappedInput::Game(game_inputs) => {
505                                // Prioritize game layers over buttons
506                                for game_input in game_inputs {
507                                    events.push(Event::InputUpdate(*game_input, is_pressed));
508                                }
509                            },
510                        }
511                    }
512                }
513
514                match event.event {
515                    EventType::ButtonPressed(button, code)
516                    | EventType::ButtonRepeated(button, code) => {
517                        handle_buttons(
518                            controller,
519                            &mut self.remapping_mode,
520                            &mut self.controller_modifiers,
521                            &mut self.events,
522                            &Button::from((button, code)),
523                            true,
524                            &mut self.last_input,
525                            self.gamelayer_mod1,
526                            self.gamelayer_mod2,
527                            self.menu_open,
528                        );
529                    },
530                    EventType::ButtonReleased(button, code) => {
531                        handle_buttons(
532                            controller,
533                            &mut self.remapping_mode,
534                            &mut self.controller_modifiers,
535                            &mut self.events,
536                            &Button::from((button, code)),
537                            false,
538                            &mut self.last_input,
539                            self.gamelayer_mod1,
540                            self.gamelayer_mod2,
541                            self.menu_open,
542                        );
543                    },
544                    EventType::ButtonChanged(button, _value, code) => {
545                        if let Some(actions) = controller
546                            .inverse_game_analog_button_map
547                            .get(&AnalogButton::from((button, code)))
548                        {
549                            #[expect(clippy::never_loop)]
550                            for action in actions {
551                                match *action {}
552                            }
553                        }
554                        if let Some(actions) = controller
555                            .inverse_menu_analog_button_map
556                            .get(&AnalogButton::from((button, code)))
557                        {
558                            #[expect(clippy::never_loop)]
559                            for action in actions {
560                                match *action {}
561                            }
562                        }
563                    },
564
565                    EventType::AxisChanged(axis, value, code) => {
566                        let value = if controller.inverted_axes.contains(&Axis::from((axis, code)))
567                        {
568                            -value
569                        } else {
570                            value
571                        };
572
573                        let value =
574                            controller.apply_axis_deadzone(&Axis::from((axis, code)), value);
575
576                        // update last input to be controller if axis was moved
577                        if value.abs() > 0.0001 {
578                            self.last_input = LastInput::Controller;
579                        }
580
581                        if self.cursor_grabbed {
582                            if let Some(actions) = controller
583                                .inverse_game_axis_map
584                                .get(&Axis::from((axis, code)))
585                            {
586                                for action in actions {
587                                    match *action {
588                                        AxisGameAction::MovementX => {
589                                            self.events.push(Event::AnalogGameInput(
590                                                AnalogGameInput::MovementX(value),
591                                            ));
592                                        },
593                                        AxisGameAction::MovementY => {
594                                            self.events.push(Event::AnalogGameInput(
595                                                AnalogGameInput::MovementY(value),
596                                            ));
597                                        },
598                                        AxisGameAction::CameraX => {
599                                            self.events.push(Event::AnalogGameInput(
600                                                AnalogGameInput::CameraX(
601                                                    value * controller.pan_sensitivity as f32
602                                                        / 100.0,
603                                                ),
604                                            ));
605                                        },
606                                        AxisGameAction::CameraY => {
607                                            let pan_invert_y = match controller.pan_invert_y {
608                                                true => -1.0,
609                                                false => 1.0,
610                                            };
611
612                                            self.events.push(Event::AnalogGameInput(
613                                                AnalogGameInput::CameraY(
614                                                    -value
615                                                        * controller.pan_sensitivity as f32
616                                                        * pan_invert_y
617                                                        / 100.0,
618                                                ),
619                                            ));
620                                        },
621                                    }
622                                }
623                            }
624                        } else if let Some(actions) = controller
625                            .inverse_menu_axis_map
626                            .get(&Axis::from((axis, code)))
627                        {
628                            // TODO: possibly add sensitivity settings when this is used
629                            for action in actions {
630                                match *action {
631                                    AxisMenuAction::MoveX => {
632                                        self.events.push(Event::AnalogMenuInput(
633                                            AnalogMenuInput::MoveX(value),
634                                        ));
635                                    },
636                                    AxisMenuAction::MoveY => {
637                                        self.events.push(Event::AnalogMenuInput(
638                                            AnalogMenuInput::MoveY(value),
639                                        ));
640                                    },
641                                    AxisMenuAction::ScrollX => {
642                                        self.events.push(Event::AnalogMenuInput(
643                                            AnalogMenuInput::ScrollX(value),
644                                        ));
645                                    },
646                                    AxisMenuAction::ScrollY => {
647                                        self.events.push(Event::AnalogMenuInput(
648                                            AnalogMenuInput::ScrollY(value),
649                                        ));
650                                    },
651                                }
652                            }
653                        }
654                    },
655                    _ => {},
656                }
657            }
658        }
659
660        let mut events = std::mem::take(&mut self.events);
661        // Mouse emulation for the menus, to be removed when a proper menu navigation
662        // system is available
663        if !self.cursor_grabbed {
664            events = events
665                .into_iter()
666                .filter_map(|event| match event {
667                    Event::AnalogMenuInput(input) => match input {
668                        AnalogMenuInput::MoveX(d) => {
669                            self.mouse_emulation_vec.x = d;
670                            None
671                        },
672                        AnalogMenuInput::MoveY(d) => {
673                            // This just has to be inverted for some reason
674                            self.mouse_emulation_vec.y = -d;
675                            None
676                        },
677                        input => Some(Event::AnalogMenuInput(input)),
678                    },
679                    Event::MenuInput(menu_input, state) => {
680                        // determine if left mouse or right mouse button
681                        let mouse_button = match menu_input {
682                            MenuInput::EmulateLeftClick => {
683                                conrod_core::input::state::mouse::Button::Left
684                            },
685                            MenuInput::EmulateRightClick => {
686                                conrod_core::input::state::mouse::Button::Right
687                            },
688                            _ => return Some(event),
689                        };
690                        Some(match state {
691                            true => Event::Ui(ui::Event(conrod_core::event::Input::Press(
692                                conrod_core::input::Button::Mouse(mouse_button),
693                            ))),
694                            false => Event::Ui(ui::Event(conrod_core::event::Input::Release(
695                                conrod_core::input::Button::Mouse(mouse_button),
696                            ))),
697                        })
698                    },
699                    _ => Some(event),
700                })
701                .collect();
702
703            let sensitivity = controller.mouse_emulation_sensitivity;
704            // TODO: make this independent of framerate
705            // TODO: consider multiplying by scale factor
706            self.offset_cursor(self.mouse_emulation_vec * sensitivity as f32);
707        }
708
709        events
710    }
711
712    pub fn handle_device_event(&mut self, event: winit::event::DeviceEvent) {
713        use winit::event::DeviceEvent;
714
715        let mouse_y_inversion = match self.mouse_y_inversion {
716            true => -1.0,
717            false => 1.0,
718        };
719
720        match event {
721            DeviceEvent::MouseMotion {
722                delta: (dx, dy), ..
723            } if self.focused => {
724                // update last input to be Mouse if motion was made
725                self.last_input = LastInput::Mouse;
726
727                let delta = Vec2::new(
728                    dx as f32 * (self.pan_sensitivity as f32 / 100.0),
729                    dy as f32 * (self.pan_sensitivity as f32 * mouse_y_inversion / 100.0),
730                );
731
732                if self.cursor_grabbed {
733                    self.events.push(Event::CursorPan(delta));
734                } else {
735                    self.events.push(Event::CursorMove(delta));
736                }
737            },
738            _ => {},
739        }
740    }
741
742    pub fn handle_window_event(
743        &mut self,
744        event: winit::event::WindowEvent,
745        settings: &mut Settings,
746    ) {
747        use winit::event::WindowEvent;
748
749        let controls = &mut settings.controls;
750
751        match event {
752            WindowEvent::CloseRequested => self.events.push(Event::Close),
753            WindowEvent::Resized(_) => {
754                self.resized = true;
755            },
756            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
757                // TODO: is window resized event emitted? or do we need to handle that here?
758                self.scale_factor = scale_factor;
759                self.events.push(Event::ScaleFactorChanged(scale_factor));
760            },
761            WindowEvent::Moved(winit::dpi::PhysicalPosition { x, y }) => {
762                self.events
763                    .push(Event::Moved(Vec2::new(x as u32, y as u32)));
764            },
765            WindowEvent::MouseInput { button, state, .. } => {
766                // Mouse input not mapped to input if it is not grabbed
767                if self.cursor_grabbed
768                    && let Some(mapped_inputs) = Window::map_input(
769                        KeyMouse::Mouse(button),
770                        controls,
771                        &mut self.remapping_mode,
772                        &mut self.last_input,
773                        self.menu_open,
774                    )
775                    && let MappedInput::Game(game_inputs) = mapped_inputs
776                {
777                    for game_input in game_inputs {
778                        self.events.push(Event::InputUpdate(
779                            *game_input,
780                            state == winit::event::ElementState::Pressed,
781                        ));
782                    }
783                }
784                self.events.push(Event::MouseButton(button, state));
785            },
786            WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(),
787            WindowEvent::KeyboardInput {
788                event,
789                is_synthetic,
790                ..
791            } => {
792                // Ignore key repeat events for GameInputs
793                if event.repeat {
794                    return;
795                }
796                // Ignore synthetic keydown events so that we don't e.g. get tabs when
797                // alt-tabbing back into the window
798                if matches!(event.state, winit::event::ElementState::Pressed) && is_synthetic {
799                    return;
800                }
801                // Ignore Alt-F4 so we don't try to do anything heavy like take a screenshot
802                // when the window is about to close
803                if matches!(event, winit::event::KeyEvent {
804                    state: winit::event::ElementState::Pressed,
805                    logical_key: winit::keyboard::Key::Named(winit::keyboard::NamedKey::F4),
806                    ..
807                }) && self.modifiers.alt_key()
808                {
809                    return;
810                }
811
812                if let Some(mapped_inputs) = Window::map_input(
813                    KeyMouse::Key(event.logical_key),
814                    controls,
815                    &mut self.remapping_mode,
816                    &mut self.last_input,
817                    self.menu_open,
818                ) {
819                    match mapped_inputs {
820                        MappedInput::Menu(menu_inputs) => {
821                            for menu_input in menu_inputs {
822                                self.events.push(Event::MenuInput(
823                                    *menu_input,
824                                    event.state == winit::event::ElementState::Pressed,
825                                ));
826                            }
827                        },
828                        MappedInput::Game(game_inputs) => {
829                            for game_input in game_inputs {
830                                match game_input {
831                                    GameInput::Fullscreen => {
832                                        if event.state == winit::event::ElementState::Pressed
833                                            && !Self::is_pressed(
834                                                &mut self.keypress_map,
835                                                GameInput::Fullscreen,
836                                            )
837                                        {
838                                            self.toggle_fullscreen = !self.toggle_fullscreen;
839                                        }
840                                        Self::set_pressed(
841                                            &mut self.keypress_map,
842                                            GameInput::Fullscreen,
843                                            event.state,
844                                        );
845                                    },
846                                    GameInput::Screenshot => {
847                                        self.take_screenshot = event.state
848                                            == winit::event::ElementState::Pressed
849                                            && !Self::is_pressed(
850                                                &mut self.keypress_map,
851                                                GameInput::Screenshot,
852                                            );
853                                        Self::set_pressed(
854                                            &mut self.keypress_map,
855                                            GameInput::Screenshot,
856                                            event.state,
857                                        );
858                                    },
859                                    _ => self.events.push(Event::InputUpdate(
860                                        *game_input,
861                                        event.state == winit::event::ElementState::Pressed,
862                                    )),
863                                }
864                            }
865                        },
866                    }
867                }
868            },
869            WindowEvent::Focused(state) => {
870                self.focused = state;
871                self.events.push(Event::Focused(state));
872            },
873            WindowEvent::CursorMoved { position, .. } => {
874                if self.cursor_grabbed {
875                    self.reset_cursor_position();
876                } else {
877                    self.cursor_position = position;
878                }
879            },
880            WindowEvent::MouseWheel { delta, .. } if self.cursor_grabbed && self.focused => {
881                const DIFFERENCE_FROM_DEVICE_EVENT_ON_X11: f32 = -15.0;
882                self.events.push(Event::Zoom({
883                    let y = match delta {
884                        winit::event::MouseScrollDelta::LineDelta(_x, y) => y,
885                        // TODO: Check to see if there is a better way to find the "line
886                        // height" than just hardcoding 16.0 pixels.  Alternately we could
887                        // get rid of this and have the user set zoom sensitivity, since
888                        // it's unlikely people would expect a configuration file to work
889                        // across operating systems.
890                        winit::event::MouseScrollDelta::PixelDelta(pos) => (pos.y / 16.0) as f32,
891                    };
892                    y * (self.zoom_sensitivity as f32 / 100.0)
893                        * if self.zoom_inversion { -1.0 } else { 1.0 }
894                        * DIFFERENCE_FROM_DEVICE_EVENT_ON_X11
895                }))
896            },
897            _ => {},
898        }
899    }
900
901    /// Moves cursor by an offset
902    pub fn offset_cursor(&self, d: Vec2<f32>) {
903        if d != Vec2::zero()
904            && let Err(err) = self
905                .window
906                .set_cursor_position(winit::dpi::LogicalPosition::new(
907                    d.x as f64 + self.cursor_position.x,
908                    d.y as f64 + self.cursor_position.y,
909                ))
910        {
911            // Log this error once rather than every frame
912            static SPAM_GUARD: std::sync::Once = std::sync::Once::new();
913            SPAM_GUARD.call_once(|| {
914                error!("Error setting cursor position: {:?}", err);
915            })
916        }
917    }
918
919    pub fn is_cursor_grabbed(&self) -> bool { self.cursor_grabbed }
920
921    pub fn grab_cursor(&mut self, grab: bool) {
922        use winit::window::CursorGrabMode;
923
924        self.cursor_grabbed = grab;
925        self.window.set_cursor_visible(!grab);
926        let res = if grab {
927            self.window
928                .set_cursor_grab(CursorGrabMode::Locked)
929                .or_else(|_e| self.window.set_cursor_grab(CursorGrabMode::Confined))
930        } else {
931            self.window.set_cursor_grab(CursorGrabMode::None)
932        };
933
934        if let Err(e) = res {
935            error!(?e, ?grab, "Failed to toggle cursor grab");
936        }
937    }
938
939    /// Reset the cursor position to the last position
940    /// This is used when handling the CursorMoved event to maintain the cursor
941    /// position when it is grabbed
942    fn reset_cursor_position(&self) {
943        if let Err(err) = self.window.set_cursor_position(self.cursor_position) {
944            // Log this error once rather than every frame
945            static SPAM_GUARD: std::sync::Once = std::sync::Once::new();
946            SPAM_GUARD.call_once(|| {
947                error!("Error resetting cursor position: {:?}", err);
948            })
949        }
950    }
951
952    pub fn toggle_fullscreen(&mut self, settings: &mut Settings, config_dir: &std::path::Path) {
953        let fullscreen = FullScreenSettings {
954            enabled: !self.is_fullscreen(),
955            ..settings.graphics.fullscreen
956        };
957
958        self.set_fullscreen_mode(fullscreen);
959        settings.graphics.fullscreen = fullscreen;
960        settings.save_to_file_warn(config_dir);
961    }
962
963    pub fn is_fullscreen(&self) -> bool { self.fullscreen.enabled }
964
965    /// Select a video mode that fits the specified requirements
966    /// Returns None if a matching video mode doesn't exist or if
967    /// the current monitor can't be retrieved
968    fn select_video_mode_rec(
969        &self,
970        resolution: [u16; 2],
971        bit_depth: Option<u16>,
972        refresh_rate_millihertz: Option<u32>,
973        correct_res: Option<Vec<VideoModeHandle>>,
974        correct_depth: Option<Option<VideoModeHandle>>,
975        correct_rate: Option<Option<VideoModeHandle>>,
976    ) -> Option<VideoModeHandle> {
977        // if a previous iteration of this method filtered the available video modes for
978        // the correct resolution already, load that value, otherwise filter it
979        // in this iteration
980        let correct_res = match correct_res {
981            Some(correct_res) => correct_res,
982            None => self
983                .window
984                .current_monitor()?
985                .video_modes()
986                .filter(|mode| mode.size().width == resolution[0] as u32)
987                .filter(|mode| mode.size().height == resolution[1] as u32)
988                .collect(),
989        };
990
991        match bit_depth {
992            // A bit depth is given
993            Some(depth) => {
994                // analogous to correct_res
995                let correct_depth = correct_depth.unwrap_or_else(|| {
996                    correct_res
997                        .iter()
998                        .find(|mode| mode.bit_depth() == depth)
999                        .cloned()
1000                });
1001
1002                match refresh_rate_millihertz {
1003                    // A bit depth and a refresh rate is given
1004                    Some(rate) => {
1005                        // analogous to correct_res
1006                        let correct_rate = correct_rate.unwrap_or_else(|| {
1007                            correct_res
1008                                .iter()
1009                                .find(|mode| mode.refresh_rate_millihertz() == rate)
1010                                .cloned()
1011                        });
1012
1013                        // if no video mode with the given bit depth and refresh rate exists, fall
1014                        // back to a video mode that fits the resolution and either bit depth or
1015                        // refresh rate depending on which parameter was causing the correct video
1016                        // mode not to be found
1017                        correct_res
1018                            .iter()
1019                            .filter(|mode| mode.bit_depth() == depth)
1020                            .find(|mode| mode.refresh_rate_millihertz() == rate)
1021                            .cloned()
1022                            .or_else(|| {
1023                                if correct_depth.is_none() && correct_rate.is_none() {
1024                                    warn!(
1025                                        "Bit depth and refresh rate specified in settings are \
1026                                         incompatible with the monitor. Choosing highest bit \
1027                                         depth and refresh rate possible instead."
1028                                    );
1029                                }
1030
1031                                self.select_video_mode_rec(
1032                                    resolution,
1033                                    correct_depth.is_some().then_some(depth),
1034                                    correct_rate.is_some().then_some(rate),
1035                                    Some(correct_res),
1036                                    Some(correct_depth),
1037                                    Some(correct_rate),
1038                                )
1039                            })
1040                    },
1041                    // A bit depth and no refresh rate is given
1042                    // if no video mode with the given bit depth exists, fall
1043                    // back to a video mode that fits only the resolution
1044                    None => match correct_depth {
1045                        Some(mode) => Some(mode),
1046                        None => {
1047                            warn!(
1048                                "Bit depth specified in settings is incompatible with the \
1049                                 monitor. Choosing highest bit depth possible instead."
1050                            );
1051
1052                            self.select_video_mode_rec(
1053                                resolution,
1054                                None,
1055                                None,
1056                                Some(correct_res),
1057                                Some(correct_depth),
1058                                None,
1059                            )
1060                        },
1061                    },
1062                }
1063            },
1064            // No bit depth is given
1065            None => match refresh_rate_millihertz {
1066                // No bit depth and a refresh rate is given
1067                Some(rate) => {
1068                    // analogous to correct_res
1069                    let correct_rate = correct_rate.unwrap_or_else(|| {
1070                        correct_res
1071                            .iter()
1072                            .find(|mode| mode.refresh_rate_millihertz() == rate)
1073                            .cloned()
1074                    });
1075
1076                    // if no video mode with the given bit depth exists, fall
1077                    // back to a video mode that fits only the resolution
1078                    match correct_rate {
1079                        Some(mode) => Some(mode),
1080                        None => {
1081                            warn!(
1082                                "Refresh rate specified in settings is incompatible with the \
1083                                 monitor. Choosing highest refresh rate possible instead."
1084                            );
1085
1086                            self.select_video_mode_rec(
1087                                resolution,
1088                                None,
1089                                None,
1090                                Some(correct_res),
1091                                None,
1092                                Some(correct_rate),
1093                            )
1094                        },
1095                    }
1096                },
1097                // No bit depth and no refresh rate is given
1098                // get the video mode with the specified resolution and the max bit depth and
1099                // refresh rate
1100                None => correct_res
1101                    .into_iter()
1102                    // Prefer bit depth over refresh rate
1103                    .sorted_by_key(|mode| mode.bit_depth())
1104                    .max_by_key(|mode| mode.refresh_rate_millihertz()),
1105            },
1106        }
1107    }
1108
1109    fn select_video_mode(
1110        &self,
1111        resolution: [u16; 2],
1112        bit_depth: Option<u16>,
1113        refresh_rate_millihertz: Option<u32>,
1114    ) -> Option<VideoModeHandle> {
1115        // (resolution, bit depth, refresh rate) represents a video mode
1116        // spec: as specified
1117        // max: maximum value available
1118
1119        // order of fallbacks as follows:
1120        // (spec, spec, spec)
1121        // (spec, spec, max), (spec, max, spec)
1122        // (spec, max, max)
1123        // (max, max, max)
1124        match self.select_video_mode_rec(
1125            resolution,
1126            bit_depth,
1127            refresh_rate_millihertz,
1128            None,
1129            None,
1130            None,
1131        ) {
1132            Some(mode) => Some(mode),
1133            // if there is no video mode with the specified resolution,
1134            // fall back to the video mode with max resolution, bit depth and refresh rate
1135            None => {
1136                warn!(
1137                    "Resolution specified in settings is incompatible with the monitor. Choosing \
1138                     highest resolution possible instead."
1139                );
1140                if let Some(monitor) = self.window.current_monitor() {
1141                    let mode = monitor
1142                        .video_modes()
1143                        // Prefer bit depth over refresh rate
1144                        .sorted_by_key(|mode| mode.refresh_rate_millihertz())
1145                        .sorted_by_key(|mode| mode.bit_depth())
1146                        .max_by_key(|mode| mode.size().width);
1147
1148                    if mode.is_none() {
1149                        warn!("Failed to select video mode, no video modes available!!")
1150                    }
1151
1152                    mode
1153                } else {
1154                    warn!("Failed to select video mode, can't get the current monitor!");
1155                    None
1156                }
1157            },
1158        }
1159    }
1160
1161    pub fn set_fullscreen_mode(&mut self, fullscreen: FullScreenSettings) {
1162        let window = &self.window;
1163        self.fullscreen = fullscreen;
1164        window.set_fullscreen(fullscreen.enabled.then(|| match fullscreen.mode {
1165            FullscreenMode::Exclusive => {
1166                if let Some(video_mode) = self.select_video_mode(
1167                    fullscreen.resolution,
1168                    fullscreen.bit_depth,
1169                    fullscreen.refresh_rate_millihertz,
1170                ) {
1171                    winit::window::Fullscreen::Exclusive(video_mode)
1172                } else {
1173                    warn!(
1174                        "Failed to select a video mode for exclusive fullscreen. Falling back to \
1175                         borderless fullscreen."
1176                    );
1177                    winit::window::Fullscreen::Borderless(None)
1178                }
1179            },
1180            FullscreenMode::Borderless => {
1181                // None here will fullscreen on the current monitor
1182                winit::window::Fullscreen::Borderless(None)
1183            },
1184        }));
1185    }
1186
1187    pub fn needs_refresh_resize(&mut self) { self.needs_refresh_resize = true; }
1188
1189    pub fn set_size(&mut self, new_size: Vec2<u32>) {
1190        self.window
1191            .set_min_inner_size(Some(winit::dpi::LogicalSize::new(
1192                new_size.x as f64,
1193                new_size.y as f64,
1194            )));
1195    }
1196
1197    pub fn send_event(&mut self, event: Event) { self.events.push(event) }
1198
1199    pub fn take_screenshot(&mut self, settings: &Settings) {
1200        let sender = self.message_sender.clone();
1201        let mut path = settings.screenshots_path.clone();
1202        self.renderer.create_screenshot(move |image| {
1203            use std::time::SystemTime;
1204
1205            // Handle any error if there was one when generating the image.
1206            let image = match image {
1207                Ok(i) => i,
1208                Err(e) => {
1209                    warn!(?e, "Couldn't generate screenshot");
1210                    let _result = sender.send(format!("Error when generating screenshot: {}", e));
1211                    return;
1212                },
1213            };
1214
1215            // Check if folder exists and create it if it does not
1216            if !path.exists()
1217                && let Err(e) = std::fs::create_dir_all(&path)
1218            {
1219                warn!(?e, ?path, "Couldn't create folder for screenshot");
1220                let _result = sender.send(String::from("Couldn't create folder for screenshot"));
1221            }
1222            path.push(format!(
1223                "screenshot_{}.png",
1224                SystemTime::now()
1225                    .duration_since(SystemTime::UNIX_EPOCH)
1226                    .map(|d| d.as_millis())
1227                    .unwrap_or(0)
1228            ));
1229            // Try to save the image
1230            if let Err(e) = image.save(&path) {
1231                warn!(?e, ?path, "Couldn't save screenshot");
1232                let _result = sender.send(String::from("Couldn't save screenshot"));
1233            } else {
1234                let _result =
1235                    sender.send(format!("Screenshot saved to {}", path.to_string_lossy()));
1236            }
1237        });
1238    }
1239
1240    fn is_pressed(
1241        map: &mut HashMap<GameInput, winit::event::ElementState>,
1242        input: GameInput,
1243    ) -> bool {
1244        *(map
1245            .entry(input)
1246            .or_insert(winit::event::ElementState::Released))
1247            == winit::event::ElementState::Pressed
1248    }
1249
1250    fn set_pressed(
1251        map: &mut HashMap<GameInput, winit::event::ElementState>,
1252        input: GameInput,
1253        state: winit::event::ElementState,
1254    ) {
1255        map.insert(input, state);
1256    }
1257
1258    // Function used to handle Mouse and Key events. It first checks if we're in
1259    // remapping mode for a specific GameInput or MenuInput. If we are, we modify
1260    // the binding of that GameInput/MenuInput with the KeyMouse passed. Else,
1261    // we return an iterator of the GameInputs/MenuInputs for that KeyMouse. If
1262    // a menu is open, it will return a MenuInput unless there is not one available
1263    // (which it will then return GameInput). If a menu is not open, return
1264    // GameInput.
1265    fn map_input<'a>(
1266        key_mouse: KeyMouse,
1267        controls: &'a mut ControlSettings,
1268        remapping: &mut RemappingMode,
1269        last_input: &mut LastInput,
1270        menu_open: bool,
1271    ) -> Option<MappedInput<'a>> {
1272        let key_mouse = key_mouse.into_upper();
1273
1274        // update last input to be Keyboard if button was pressed
1275        *last_input = LastInput::Keyboard;
1276
1277        match *remapping {
1278            RemappingMode::RemapKeyboard(game_input) => {
1279                controls.modify_binding(game_input, key_mouse);
1280                *remapping = RemappingMode::None;
1281                None
1282            },
1283            RemappingMode::RemapKeyboardMenu(menu_input) => {
1284                controls.modify_menu_binding(menu_input, key_mouse);
1285                *remapping = RemappingMode::None;
1286                None
1287            },
1288            RemappingMode::None => {
1289                if !menu_open {
1290                    // If a menu is not open, simply return any game inputs
1291                    controls
1292                        .get_associated_game_inputs(&key_mouse)
1293                        .map(|game_inputs| MappedInput::Game(game_inputs.iter()))
1294                } else {
1295                    // If a menu is open, return the associated MenuInput if any, otherwise return
1296                    // the associated GameInput
1297                    if let Some(menu_inputs) = controls.get_associated_menu_inputs(&key_mouse)
1298                        && !menu_inputs.is_empty()
1299                    {
1300                        return Some(MappedInput::Menu(menu_inputs.iter()));
1301                    }
1302
1303                    controls
1304                        .get_associated_game_inputs(&key_mouse)
1305                        .map(|game_inputs| MappedInput::Game(game_inputs.iter()))
1306                }
1307            },
1308            _ => None,
1309        }
1310    }
1311
1312    // Same thing as map_input, but for controller actions
1313    #[expect(clippy::get_first)]
1314    fn map_controller_input<'a>(
1315        controller: &'a mut ControllerSettings,
1316        remapping: &mut RemappingMode,
1317        modifiers: &[Button],
1318        button: &Button,
1319        mod1_input: bool,
1320        mod2_input: bool,
1321        menu_open: bool,
1322    ) -> Option<MappedInput<'a>> {
1323        match *remapping {
1324            RemappingMode::RemapGamepadLayers(game_input) => {
1325                // create the new layer entry
1326                let new_layer_entry = LayerEntry {
1327                    button: *button,
1328                    mod1: if mod1_input {
1329                        Button::Simple(GilButton::RightTrigger)
1330                    } else {
1331                        Button::Simple(GilButton::Unknown)
1332                    },
1333                    mod2: if mod2_input {
1334                        Button::Simple(GilButton::LeftTrigger)
1335                    } else {
1336                        Button::Simple(GilButton::Unknown)
1337                    },
1338                };
1339                controller.modify_layer_binding(game_input, new_layer_entry);
1340                *remapping = RemappingMode::None;
1341                None
1342            },
1343            RemappingMode::RemapGamepadButtons(game_input) => {
1344                controller.modify_button_binding(game_input, *button);
1345                *remapping = RemappingMode::None;
1346                None
1347            },
1348            RemappingMode::RemapGamepadMenu(menu_input) => {
1349                controller.modify_menu_binding(menu_input, *button);
1350                *remapping = RemappingMode::None;
1351                None
1352            },
1353            RemappingMode::None => {
1354                // have to check l_entry1 and l_entry2 so LB+RB can be treated equivalent to
1355                // RB+LB
1356                let l_entry1 = LayerEntry {
1357                    button: *button,
1358                    mod1: modifiers.get(0).copied().unwrap_or_default(),
1359                    mod2: modifiers.get(1).copied().unwrap_or_default(),
1360                };
1361                let l_entry2 = LayerEntry {
1362                    button: *button,
1363                    mod1: modifiers.get(1).copied().unwrap_or_default(),
1364                    mod2: modifiers.get(0).copied().unwrap_or_default(),
1365                };
1366
1367                if !menu_open {
1368                    // If a menu is not open, simply return any game inputs
1369
1370                    // First check layer entries
1371                    if let Some(game_inputs) =
1372                        controller.get_associated_game_layer_inputs(&l_entry1)
1373                    {
1374                        Some(MappedInput::Game(game_inputs.iter()))
1375                    } else if let Some(game_inputs) =
1376                        controller.get_associated_game_layer_inputs(&l_entry2)
1377                    {
1378                        Some(MappedInput::Game(game_inputs.iter()))
1379                    } else {
1380                        // check button entries
1381                        controller
1382                            .get_associated_game_button_inputs(button)
1383                            .map(|game_inputs| MappedInput::Game(game_inputs.iter()))
1384                    }
1385                } else {
1386                    // If a menu is open, return the associated MenuInput if any, otherwise return
1387                    // the associated GameInput
1388                    if let Some(menu_inputs) = controller.get_associated_game_menu_inputs(button)
1389                        && !menu_inputs.is_empty()
1390                    {
1391                        return Some(MappedInput::Menu(menu_inputs.iter()));
1392                    }
1393
1394                    // First check layer entries
1395                    if let Some(game_inputs) =
1396                        controller.get_associated_game_layer_inputs(&l_entry1)
1397                    {
1398                        Some(MappedInput::Game(game_inputs.iter()))
1399                    } else if let Some(game_inputs) =
1400                        controller.get_associated_game_layer_inputs(&l_entry2)
1401                    {
1402                        Some(MappedInput::Game(game_inputs.iter()))
1403                    } else {
1404                        // check button entries
1405                        controller
1406                            .get_associated_game_button_inputs(button)
1407                            .map(|game_inputs| MappedInput::Game(game_inputs.iter()))
1408                    }
1409                }
1410            },
1411            _ => None,
1412        }
1413    }
1414
1415    pub fn set_remapping_mode(&mut self, r_mode: RemappingMode) { self.remapping_mode = r_mode; }
1416
1417    pub fn reset_mapping_mode(&mut self) { self.remapping_mode = RemappingMode::None; }
1418
1419    pub fn window(&self) -> &winit::window::Window { &self.window }
1420
1421    pub fn modifiers(&self) -> winit::keyboard::ModifiersState { self.modifiers }
1422
1423    pub fn scale_factor(&self) -> f64 { self.scale_factor }
1424
1425    pub fn last_input(&self) -> LastInput { self.last_input }
1426
1427    pub fn controller_type(&self) -> ControllerType { self.controller_type }
1428}
1429
1430#[derive(Default, Copy, Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)]
1431pub enum FullscreenMode {
1432    Exclusive,
1433    #[serde(other)]
1434    #[default]
1435    Borderless,
1436}
1437
1438#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
1439#[serde(default)]
1440pub struct WindowSettings {
1441    pub size: [u32; 2],
1442    pub maximised: bool,
1443}
1444
1445impl Default for WindowSettings {
1446    fn default() -> Self {
1447        Self {
1448            size: [1280, 720],
1449            maximised: false,
1450        }
1451    }
1452}
1453
1454#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
1455#[serde(default)]
1456pub struct FullScreenSettings {
1457    pub enabled: bool,
1458    pub mode: FullscreenMode,
1459    pub resolution: [u16; 2],
1460    pub bit_depth: Option<u16>,
1461    pub refresh_rate_millihertz: Option<u32>,
1462}
1463
1464impl Default for FullScreenSettings {
1465    fn default() -> Self {
1466        Self {
1467            enabled: true,
1468            mode: FullscreenMode::Borderless,
1469            resolution: [1920, 1080],
1470            bit_depth: None,
1471            refresh_rate_millihertz: None,
1472        }
1473    }
1474}