veloren/voxygen/src/window.rs
2026-07-14 04:08:18 +00:00

1474 lines
58 KiB
Rust

use crate::{
error::Error,
game_input::GameInput,
render::Renderer,
settings::{ControlSettings, ControllerSettings, Settings, controller::*},
ui,
};
use common_base::span;
use crossbeam_channel as channel;
use gilrs::{Button as GilButton, EventType, Gilrs};
use hashbrown::{HashMap, hash_set::Iter};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use strum::{AsRefStr, EnumIter};
use tracing::{error, warn};
use vek::*;
use winit::monitor::VideoModeHandle;
/// Represents a key that the game menus recognise after input mapping
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Deserialize,
Serialize,
AsRefStr,
EnumIter,
)]
pub enum MenuInput {
Up,
Down,
Left,
Right,
ScrollUp,
ScrollDown,
ScrollLeft,
ScrollRight,
PageDown,
PageUp,
Apply,
Back,
Exit,
LocalFocus,
GlobalFocus,
EmulateLeftClick,
EmulateRightClick,
}
/// Manages an Iterator over MenuInputs or GameInputs
pub enum MappedInput<'a> {
Menu(Iter<'a, MenuInput>),
Game(Iter<'a, GameInput>),
}
impl MenuInput {
pub fn get_localization_key(&self) -> &str { self.as_ref() }
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum AnalogMenuInput {
MoveX(f32),
MoveY(f32),
ScrollX(f32),
ScrollY(f32),
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum AnalogGameInput {
MovementX(f32),
MovementY(f32),
CameraX(f32),
CameraY(f32),
}
/// Represents an incoming event from the window.
#[derive(Clone, Debug)]
pub enum Event {
/// The window has been requested to close.
Close,
/// The window has been resized.
Resize(Vec2<u32>),
/// The window scale factor has been changed
ScaleFactorChanged(f64),
/// The window has been moved.
Moved(Vec2<u32>),
/// The cursor has been panned across the screen while grabbed.
CursorPan(Vec2<f32>),
/// The cursor has been moved across the screen while ungrabbed.
CursorMove(Vec2<f32>),
/// A mouse button has been pressed or released
MouseButton(MouseButton, PressState),
/// The camera has been requested to zoom.
Zoom(f32),
/// A key that the game recognises has been pressed or released.
InputUpdate(GameInput, bool),
/// Event that the ui uses.
Ui(ui::Event),
/// Event that the iced ui uses.
IcedUi(ui::ice::Event),
/// The view distance has changed.
ViewDistanceChanged(u32),
/// Game settings have changed.
SettingsChanged,
/// The window is (un)focused
Focused(bool),
/// A key that the game recognises for menu navigation has been pressed or
/// released
MenuInput(MenuInput, bool),
/// Update of the analog inputs recognized by the menus
AnalogMenuInput(AnalogMenuInput),
/// Update of the analog inputs recognized by the game
AnalogGameInput(AnalogGameInput),
/// We tried to save a screenshot
ScreenshotMessage(String),
}
pub type MouseButton = winit::event::MouseButton;
pub type PressState = winit::event::ElementState;
pub type EventLoop = winit::event_loop::EventLoop<()>;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum KeyMouse {
Key(winit::keyboard::Key),
Mouse(winit::event::MouseButton),
}
impl KeyMouse {
pub fn into_upper(mut self) -> Self {
if let KeyMouse::Key(winit::keyboard::Key::Character(c)) = &mut self {
*c = c.to_ascii_uppercase().into();
}
self
}
/// Returns key description (e.g Left Shift)
pub fn display_string(&self) -> String {
use self::KeyMouse::*;
use winit::{event::MouseButton, keyboard::Key::*};
match self {
Key(key) => match key {
Named(key) => format!("{key:?}"),
Character(c) => c.to_string(),
Unidentified(key) => format!("Unknown ({key:?})"),
Dead(dead) => format!("Dead ({dead:?})"),
},
Mouse(MouseButton::Left) => String::from("Left Click"),
Mouse(MouseButton::Right) => String::from("Right Click"),
Mouse(MouseButton::Middle) => String::from("Middle Click"),
Mouse(MouseButton::Forward) => String::from("Mouse Forward"),
Mouse(MouseButton::Back) => String::from("Mouse Back"),
Mouse(MouseButton::Other(button)) => {
// Additional mouse buttons after middle click start at 1
format!("Mouse {}", button + 3)
},
}
}
/// If it exists, returns the shortened version of a key name
/// (e.g. Left Click -> M1)
pub fn try_shortened(&self) -> Option<String> {
use self::KeyMouse::*;
use winit::event::MouseButton;
let key_string = match self {
Mouse(MouseButton::Left) => "M1",
Mouse(MouseButton::Right) => "M2",
Mouse(MouseButton::Middle) => "M3",
Mouse(MouseButton::Other(button)) => {
// Additional mouse buttons after middle click start at 1
return Some(format!("M{}", button + 3));
},
_ => return None,
};
Some(key_string.to_owned())
}
/// Returns shortest name of key (e.g. Left Click - M1)
/// If key doesn't have shorter version, use regular one.
///
/// Use it in case if space does really matter.
pub fn display_shortest(&self) -> String {
self.try_shortened()
.unwrap_or_else(|| self.display_string())
}
}
#[derive(Clone, Copy, Debug)]
pub enum RemappingMode {
RemapKeyboard(GameInput),
RemapKeyboardMenu(MenuInput),
RemapGamepadButtons(GameInput),
RemapGamepadLayers(GameInput),
RemapGamepadMenu(MenuInput),
None,
}
// TODO: implement controller layout detection/configuration
#[derive(Clone, Copy, Debug)]
pub enum ControllerType {
Xbox,
Nintendo,
Playstation,
None,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LastInput {
Keyboard,
Mouse,
Controller,
}
pub struct Window {
renderer: Renderer,
window: Arc<winit::window::Window>,
cursor_grabbed: bool,
pub pan_sensitivity: u32,
pub zoom_sensitivity: u32,
pub zoom_inversion: bool,
pub mouse_y_inversion: bool,
fullscreen: FullScreenSettings,
modifiers: winit::keyboard::ModifiersState,
// Track if at least one Resized event has occured since the last `fetch_events` call
// Used for deduplication of resizes.
resized: bool,
scale_factor: f64,
needs_refresh_resize: bool,
keypress_map: HashMap<GameInput, winit::event::ElementState>,
pub remapping_mode: RemappingMode,
events: Vec<Event>,
pub focused: bool,
gilrs: Option<Gilrs>,
pub controller_modifiers: Vec<Button>,
pub menu_open: bool,
cursor_position: winit::dpi::PhysicalPosition<f64>,
mouse_emulation_vec: Vec2<f32>,
controller_type: ControllerType,
last_input: LastInput,
// Currently used to send and receive screenshot result messages
message_sender: channel::Sender<String>,
message_receiver: channel::Receiver<String>,
// Used for screenshots & fullscreen toggle to deduplicate/postpone to after event handler
take_screenshot: bool,
toggle_fullscreen: bool,
// Used for storing the state of checkboxes on controls>gamepad>gamelayers, does not need to be
// saved to file, so initialized here
pub gamelayer_mod1: bool,
pub gamelayer_mod2: bool,
}
impl Window {
pub fn new(
settings: &Settings,
runtime: &tokio::runtime::Runtime,
) -> Result<(Window, EventLoop), Error> {
let event_loop = EventLoop::new().unwrap();
let window = settings.graphics.window;
#[allow(unused_mut)] //ensure no weird issues on different platforms
let mut attributes = winit::window::Window::default_attributes()
.with_title("Veloren")
.with_inner_size(winit::dpi::LogicalSize::new(
window.size[0] as f64,
window.size[1] as f64,
))
.with_maximized(window.maximised);
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
use winit::platform::wayland::WindowAttributesExtWayland;
attributes = attributes.with_name("net.veloren.veloren", "veloren");
}
// Avoid cpal / winit OleInitialize conflict
// See: https://github.com/rust-windowing/winit/pull/1524
#[cfg(target_family = "windows")]
let attributes = winit::platform::windows::WindowAttributesExtWindows::with_drag_and_drop(
attributes, false,
);
#[expect(deprecated)]
let window = Arc::new(event_loop.create_window(attributes).unwrap());
let renderer = Renderer::new(
Arc::clone(&window),
settings.graphics.render_mode.clone(),
runtime,
)?;
let keypress_map = HashMap::new();
let gilrs = match Gilrs::new() {
Ok(gilrs) => Some(gilrs),
Err(gilrs::Error::NotImplemented(_dummy)) => {
warn!("Controller input is unsupported on this platform.");
None
},
Err(gilrs::Error::InvalidAxisToBtn) => {
error!(
"Invalid AxisToBtn controller mapping. Falling back to no controller support."
);
None
},
Err(gilrs::Error::Other(e)) => {
error!(
?e,
"Platform-specific error when creating a Gilrs instance. Falling back to no \
controller support."
);
None
},
Err(e) => {
error!(
?e,
"Unspecified error when creating a Gilrs instance. Falling back to no \
controller support."
);
None
},
};
let (message_sender, message_receiver): (
channel::Sender<String>,
channel::Receiver<String>,
) = channel::unbounded::<String>();
let scale_factor = window.scale_factor();
let mut this = Self {
renderer,
window,
cursor_grabbed: false,
pan_sensitivity: settings.gameplay.pan_sensitivity,
zoom_sensitivity: settings.gameplay.zoom_sensitivity,
zoom_inversion: settings.gameplay.zoom_inversion,
mouse_y_inversion: settings.gameplay.mouse_y_inversion,
fullscreen: FullScreenSettings::default(),
modifiers: Default::default(),
scale_factor,
resized: false,
needs_refresh_resize: false,
keypress_map,
remapping_mode: RemappingMode::None,
events: Vec::new(),
focused: true,
gilrs,
controller_modifiers: Vec::new(),
menu_open: false,
cursor_position: winit::dpi::PhysicalPosition::new(0.0, 0.0),
mouse_emulation_vec: Vec2::zero(),
controller_type: ControllerType::Xbox,
last_input: LastInput::Mouse,
// Currently used to send and receive screenshot result messages
message_sender,
message_receiver,
take_screenshot: false,
toggle_fullscreen: false,
gamelayer_mod1: true,
gamelayer_mod2: false,
};
this.set_fullscreen_mode(settings.graphics.fullscreen);
Ok((this, event_loop))
}
pub fn renderer(&self) -> &Renderer { &self.renderer }
pub fn renderer_mut(&mut self) -> &mut Renderer { &mut self.renderer }
pub fn resolve_deduplicated_events(
&mut self,
settings: &mut Settings,
config_dir: &std::path::Path,
) {
// Handle screenshots and toggling fullscreen
if self.take_screenshot {
self.take_screenshot = false;
self.take_screenshot(settings);
}
if self.toggle_fullscreen {
self.toggle_fullscreen = false;
self.toggle_fullscreen(settings, config_dir);
}
}
pub fn fetch_events(&mut self, settings: &mut Settings) -> Vec<Event> {
span!(_guard, "fetch_events", "Window::fetch_events");
let controller = &mut settings.controller;
// Refresh ui size (used when changing playstates)
if self.needs_refresh_resize {
let scale_factor = self.window.scale_factor();
let physical = self.window.inner_size();
let logical_size =
Vec2::from(<(f64, f64)>::from(physical.to_logical::<f64>(scale_factor)));
self.events
.push(Event::Ui(ui::Event::new_resize(logical_size)));
self.events.push(Event::IcedUi(iced::Event::Window(
iced::window::Event::Resized {
width: logical_size.x as u32,
height: logical_size.y as u32,
},
)));
self.events.push(Event::ScaleFactorChanged(scale_factor));
self.needs_refresh_resize = false;
}
// Handle deduplicated resizing that occured
if self.resized {
self.resized = false;
// We don't use the size provided by the event because more resize events could
// have happened since, making the value outdated, so we must query directly
// from the window to prevent errors
let physical = self.window.inner_size();
let scale_factor = self.window.scale_factor();
let is_maximized = self.window.is_maximized();
self.renderer
.on_resize(Vec2::new(physical.width, physical.height));
self.events
.push(Event::Resize(Vec2::new(physical.width, physical.height)));
let logical_size =
Vec2::from(<(f64, f64)>::from(physical.to_logical::<f64>(scale_factor)));
// Emit event for the UI
self.events
.push(Event::Ui(ui::Event::new_resize(logical_size)));
self.events.push(Event::IcedUi(iced::Event::Window(
iced::window::Event::Resized {
width: logical_size.x as u32,
height: logical_size.y as u32,
},
)));
// Save new window state in settings
//
// We don't save the size if it's less than 1 because wgpu fails to create a
// surface with zero size.
if logical_size.x >= 1.0 && logical_size.y >= 1.0 {
settings.graphics.window.size = [logical_size.x as u32, logical_size.y as u32];
}
settings.graphics.window.maximised = is_maximized;
}
// Receive any messages sent through the message channel
for message in self.message_receiver.try_iter() {
self.events.push(Event::ScreenshotMessage(message))
}
if let Some(gilrs) = &mut self.gilrs {
while let Some(event) = gilrs.next_event() {
fn handle_buttons(
settings: &mut ControllerSettings,
remapping: &mut RemappingMode,
modifiers: &mut Vec<Button>,
events: &mut Vec<Event>,
button: &Button,
is_pressed: bool,
last_input: &mut LastInput,
mod1: bool,
mod2: bool,
menu_open: bool,
) {
// update last input to be controller if button was pressed
*last_input = LastInput::Controller;
if settings.modifier_buttons.contains(button) {
if is_pressed {
modifiers.push(*button);
// There is a possibility of voxygen not having
// registered the initial press event (either because it
// hadn't started yet, or whatever else) hence the
// modifier has no position in the list, unwrapping
// here would cause a crash in those cases
} else if let Some(index) =
modifiers.iter().position(|modifier| modifier == button)
{
modifiers.remove(index);
}
}
if let Some(mapped_inputs) = Window::map_controller_input(
settings, remapping, modifiers, button, mod1, mod2, menu_open,
) {
match mapped_inputs {
// Fetch actions to perform; if nothing was returned, then either no
// action was assigned to the input, or a remapping action was performed
MappedInput::Menu(menu_inputs) => {
for menu_input in menu_inputs {
events.push(Event::MenuInput(*menu_input, is_pressed));
}
},
MappedInput::Game(game_inputs) => {
// Prioritize game layers over buttons
for game_input in game_inputs {
events.push(Event::InputUpdate(*game_input, is_pressed));
}
},
}
}
}
match event.event {
EventType::ButtonPressed(button, code)
| EventType::ButtonRepeated(button, code) => {
handle_buttons(
controller,
&mut self.remapping_mode,
&mut self.controller_modifiers,
&mut self.events,
&Button::from((button, code)),
true,
&mut self.last_input,
self.gamelayer_mod1,
self.gamelayer_mod2,
self.menu_open,
);
},
EventType::ButtonReleased(button, code) => {
handle_buttons(
controller,
&mut self.remapping_mode,
&mut self.controller_modifiers,
&mut self.events,
&Button::from((button, code)),
false,
&mut self.last_input,
self.gamelayer_mod1,
self.gamelayer_mod2,
self.menu_open,
);
},
EventType::ButtonChanged(button, _value, code) => {
if let Some(actions) = controller
.inverse_game_analog_button_map
.get(&AnalogButton::from((button, code)))
{
#[expect(clippy::never_loop)]
for action in actions {
match *action {}
}
}
if let Some(actions) = controller
.inverse_menu_analog_button_map
.get(&AnalogButton::from((button, code)))
{
#[expect(clippy::never_loop)]
for action in actions {
match *action {}
}
}
},
EventType::AxisChanged(axis, value, code) => {
let value = if controller.inverted_axes.contains(&Axis::from((axis, code)))
{
-value
} else {
value
};
let value =
controller.apply_axis_deadzone(&Axis::from((axis, code)), value);
// update last input to be controller if axis was moved
if value.abs() > 0.0001 {
self.last_input = LastInput::Controller;
}
if self.cursor_grabbed {
if let Some(actions) = controller
.inverse_game_axis_map
.get(&Axis::from((axis, code)))
{
for action in actions {
match *action {
AxisGameAction::MovementX => {
self.events.push(Event::AnalogGameInput(
AnalogGameInput::MovementX(value),
));
},
AxisGameAction::MovementY => {
self.events.push(Event::AnalogGameInput(
AnalogGameInput::MovementY(value),
));
},
AxisGameAction::CameraX => {
self.events.push(Event::AnalogGameInput(
AnalogGameInput::CameraX(
value * controller.pan_sensitivity as f32
/ 100.0,
),
));
},
AxisGameAction::CameraY => {
let pan_invert_y = match controller.pan_invert_y {
true => -1.0,
false => 1.0,
};
self.events.push(Event::AnalogGameInput(
AnalogGameInput::CameraY(
-value
* controller.pan_sensitivity as f32
* pan_invert_y
/ 100.0,
),
));
},
}
}
}
} else if let Some(actions) = controller
.inverse_menu_axis_map
.get(&Axis::from((axis, code)))
{
// TODO: possibly add sensitivity settings when this is used
for action in actions {
match *action {
AxisMenuAction::MoveX => {
self.events.push(Event::AnalogMenuInput(
AnalogMenuInput::MoveX(value),
));
},
AxisMenuAction::MoveY => {
self.events.push(Event::AnalogMenuInput(
AnalogMenuInput::MoveY(value),
));
},
AxisMenuAction::ScrollX => {
self.events.push(Event::AnalogMenuInput(
AnalogMenuInput::ScrollX(value),
));
},
AxisMenuAction::ScrollY => {
self.events.push(Event::AnalogMenuInput(
AnalogMenuInput::ScrollY(value),
));
},
}
}
}
},
_ => {},
}
}
}
let mut events = std::mem::take(&mut self.events);
// Mouse emulation for the menus, to be removed when a proper menu navigation
// system is available
if !self.cursor_grabbed {
events = events
.into_iter()
.filter_map(|event| match event {
Event::AnalogMenuInput(input) => match input {
AnalogMenuInput::MoveX(d) => {
self.mouse_emulation_vec.x = d;
None
},
AnalogMenuInput::MoveY(d) => {
// This just has to be inverted for some reason
self.mouse_emulation_vec.y = -d;
None
},
input => Some(Event::AnalogMenuInput(input)),
},
Event::MenuInput(menu_input, state) => {
// determine if left mouse or right mouse button
let mouse_button = match menu_input {
MenuInput::EmulateLeftClick => {
conrod_core::input::state::mouse::Button::Left
},
MenuInput::EmulateRightClick => {
conrod_core::input::state::mouse::Button::Right
},
_ => return Some(event),
};
Some(match state {
true => Event::Ui(ui::Event(conrod_core::event::Input::Press(
conrod_core::input::Button::Mouse(mouse_button),
))),
false => Event::Ui(ui::Event(conrod_core::event::Input::Release(
conrod_core::input::Button::Mouse(mouse_button),
))),
})
},
_ => Some(event),
})
.collect();
let sensitivity = controller.mouse_emulation_sensitivity;
// TODO: make this independent of framerate
// TODO: consider multiplying by scale factor
self.offset_cursor(self.mouse_emulation_vec * sensitivity as f32);
}
events
}
pub fn handle_device_event(&mut self, event: winit::event::DeviceEvent) {
use winit::event::DeviceEvent;
let mouse_y_inversion = match self.mouse_y_inversion {
true => -1.0,
false => 1.0,
};
match event {
DeviceEvent::MouseMotion {
delta: (dx, dy), ..
} if self.focused => {
// update last input to be Mouse if motion was made
self.last_input = LastInput::Mouse;
let delta = Vec2::new(
dx as f32 * (self.pan_sensitivity as f32 / 100.0),
dy as f32 * (self.pan_sensitivity as f32 * mouse_y_inversion / 100.0),
);
if self.cursor_grabbed {
self.events.push(Event::CursorPan(delta));
} else {
self.events.push(Event::CursorMove(delta));
}
},
_ => {},
}
}
pub fn handle_window_event(
&mut self,
event: winit::event::WindowEvent,
settings: &mut Settings,
) {
use winit::event::WindowEvent;
let controls = &mut settings.controls;
match event {
WindowEvent::CloseRequested => self.events.push(Event::Close),
WindowEvent::Resized(_) => {
self.resized = true;
},
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
// TODO: is window resized event emitted? or do we need to handle that here?
self.scale_factor = scale_factor;
self.events.push(Event::ScaleFactorChanged(scale_factor));
},
WindowEvent::Moved(winit::dpi::PhysicalPosition { x, y }) => {
self.events
.push(Event::Moved(Vec2::new(x as u32, y as u32)));
},
WindowEvent::MouseInput { button, state, .. } => {
let map_input = Window::map_input(
KeyMouse::Mouse(button),
controls,
&mut self.remapping_mode,
&mut self.last_input,
self.menu_open,
);
// Mouse input not mapped to input if it is not grabbed
if self.cursor_grabbed
&& let Some(MappedInput::Game(game_inputs)) = map_input
{
for game_input in game_inputs {
self.events.push(Event::InputUpdate(
*game_input,
state == winit::event::ElementState::Pressed,
));
}
}
self.events.push(Event::MouseButton(button, state));
},
WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(),
WindowEvent::KeyboardInput {
event,
is_synthetic,
..
} => {
// Ignore key repeat events for GameInputs
if event.repeat {
return;
}
// Ignore synthetic keydown events so that we don't e.g. get tabs when
// alt-tabbing back into the window
if matches!(event.state, winit::event::ElementState::Pressed) && is_synthetic {
return;
}
// Ignore Alt-F4 so we don't try to do anything heavy like take a screenshot
// when the window is about to close
if matches!(event, winit::event::KeyEvent {
state: winit::event::ElementState::Pressed,
logical_key: winit::keyboard::Key::Named(winit::keyboard::NamedKey::F4),
..
}) && self.modifiers.alt_key()
{
return;
}
if let Some(mapped_inputs) = Window::map_input(
KeyMouse::Key(event.logical_key),
controls,
&mut self.remapping_mode,
&mut self.last_input,
self.menu_open,
) {
match mapped_inputs {
MappedInput::Menu(menu_inputs) => {
for menu_input in menu_inputs {
self.events.push(Event::MenuInput(
*menu_input,
event.state == winit::event::ElementState::Pressed,
));
}
},
MappedInput::Game(game_inputs) => {
for game_input in game_inputs {
match game_input {
GameInput::Fullscreen => {
if event.state == winit::event::ElementState::Pressed
&& !Self::is_pressed(
&mut self.keypress_map,
GameInput::Fullscreen,
)
{
self.toggle_fullscreen = !self.toggle_fullscreen;
}
Self::set_pressed(
&mut self.keypress_map,
GameInput::Fullscreen,
event.state,
);
},
GameInput::Screenshot => {
self.take_screenshot = event.state
== winit::event::ElementState::Pressed
&& !Self::is_pressed(
&mut self.keypress_map,
GameInput::Screenshot,
);
Self::set_pressed(
&mut self.keypress_map,
GameInput::Screenshot,
event.state,
);
},
_ => self.events.push(Event::InputUpdate(
*game_input,
event.state == winit::event::ElementState::Pressed,
)),
}
}
},
}
}
},
WindowEvent::Focused(state) => {
self.focused = state;
self.events.push(Event::Focused(state));
},
WindowEvent::CursorMoved { position, .. } => {
if self.cursor_grabbed {
self.reset_cursor_position();
} else {
self.cursor_position = position;
}
},
WindowEvent::MouseWheel { delta, .. } if self.cursor_grabbed && self.focused => {
const DIFFERENCE_FROM_DEVICE_EVENT_ON_X11: f32 = -15.0;
self.events.push(Event::Zoom({
let y = match delta {
winit::event::MouseScrollDelta::LineDelta(_x, y) => y,
// TODO: Check to see if there is a better way to find the "line
// height" than just hardcoding 16.0 pixels. Alternately we could
// get rid of this and have the user set zoom sensitivity, since
// it's unlikely people would expect a configuration file to work
// across operating systems.
winit::event::MouseScrollDelta::PixelDelta(pos) => (pos.y / 16.0) as f32,
};
y * (self.zoom_sensitivity as f32 / 100.0)
* if self.zoom_inversion { -1.0 } else { 1.0 }
* DIFFERENCE_FROM_DEVICE_EVENT_ON_X11
}))
},
_ => {},
}
}
/// Moves cursor by an offset
pub fn offset_cursor(&self, d: Vec2<f32>) {
if d != Vec2::zero()
&& let Err(err) = self
.window
.set_cursor_position(winit::dpi::LogicalPosition::new(
d.x as f64 + self.cursor_position.x,
d.y as f64 + self.cursor_position.y,
))
{
// Log this error once rather than every frame
static SPAM_GUARD: std::sync::Once = std::sync::Once::new();
SPAM_GUARD.call_once(|| {
error!("Error setting cursor position: {:?}", err);
})
}
}
pub fn is_cursor_grabbed(&self) -> bool { self.cursor_grabbed }
pub fn grab_cursor(&mut self, grab: bool) {
use winit::window::CursorGrabMode;
self.cursor_grabbed = grab;
self.window.set_cursor_visible(!grab);
let res = if grab {
self.window
.set_cursor_grab(CursorGrabMode::Locked)
.or_else(|_e| self.window.set_cursor_grab(CursorGrabMode::Confined))
} else {
self.window.set_cursor_grab(CursorGrabMode::None)
};
if let Err(e) = res {
error!(?e, ?grab, "Failed to toggle cursor grab");
}
}
/// Reset the cursor position to the last position
/// This is used when handling the CursorMoved event to maintain the cursor
/// position when it is grabbed
fn reset_cursor_position(&self) {
if let Err(err) = self.window.set_cursor_position(self.cursor_position) {
// Log this error once rather than every frame
static SPAM_GUARD: std::sync::Once = std::sync::Once::new();
SPAM_GUARD.call_once(|| {
error!("Error resetting cursor position: {:?}", err);
})
}
}
pub fn toggle_fullscreen(&mut self, settings: &mut Settings, config_dir: &std::path::Path) {
let fullscreen = FullScreenSettings {
enabled: !self.is_fullscreen(),
..settings.graphics.fullscreen
};
self.set_fullscreen_mode(fullscreen);
settings.graphics.fullscreen = fullscreen;
settings.save_to_file_warn(config_dir);
}
pub fn is_fullscreen(&self) -> bool { self.fullscreen.enabled }
/// Select a video mode that fits the specified requirements
/// Returns None if a matching video mode doesn't exist or if
/// the current monitor can't be retrieved
fn select_video_mode_rec(
&self,
resolution: [u16; 2],
bit_depth: Option<u16>,
refresh_rate_millihertz: Option<u32>,
correct_res: Option<Vec<VideoModeHandle>>,
correct_depth: Option<Option<VideoModeHandle>>,
correct_rate: Option<Option<VideoModeHandle>>,
) -> Option<VideoModeHandle> {
// if a previous iteration of this method filtered the available video modes for
// the correct resolution already, load that value, otherwise filter it
// in this iteration
let correct_res = match correct_res {
Some(correct_res) => correct_res,
None => self
.window
.current_monitor()?
.video_modes()
.filter(|mode| mode.size().width == resolution[0] as u32)
.filter(|mode| mode.size().height == resolution[1] as u32)
.collect(),
};
match bit_depth {
// A bit depth is given
Some(depth) => {
// analogous to correct_res
let correct_depth = correct_depth.unwrap_or_else(|| {
correct_res
.iter()
.find(|mode| mode.bit_depth() == depth)
.cloned()
});
match refresh_rate_millihertz {
// A bit depth and a refresh rate is given
Some(rate) => {
// analogous to correct_res
let correct_rate = correct_rate.unwrap_or_else(|| {
correct_res
.iter()
.find(|mode| mode.refresh_rate_millihertz() == rate)
.cloned()
});
// if no video mode with the given bit depth and refresh rate exists, fall
// back to a video mode that fits the resolution and either bit depth or
// refresh rate depending on which parameter was causing the correct video
// mode not to be found
correct_res
.iter()
.filter(|mode| mode.bit_depth() == depth)
.find(|mode| mode.refresh_rate_millihertz() == rate)
.cloned()
.or_else(|| {
if correct_depth.is_none() && correct_rate.is_none() {
warn!(
"Bit depth and refresh rate specified in settings are \
incompatible with the monitor. Choosing highest bit \
depth and refresh rate possible instead."
);
}
self.select_video_mode_rec(
resolution,
correct_depth.is_some().then_some(depth),
correct_rate.is_some().then_some(rate),
Some(correct_res),
Some(correct_depth),
Some(correct_rate),
)
})
},
// A bit depth and no refresh rate is given
// if no video mode with the given bit depth exists, fall
// back to a video mode that fits only the resolution
None => match correct_depth {
Some(mode) => Some(mode),
None => {
warn!(
"Bit depth specified in settings is incompatible with the \
monitor. Choosing highest bit depth possible instead."
);
self.select_video_mode_rec(
resolution,
None,
None,
Some(correct_res),
Some(correct_depth),
None,
)
},
},
}
},
// No bit depth is given
None => match refresh_rate_millihertz {
// No bit depth and a refresh rate is given
Some(rate) => {
// analogous to correct_res
let correct_rate = correct_rate.unwrap_or_else(|| {
correct_res
.iter()
.find(|mode| mode.refresh_rate_millihertz() == rate)
.cloned()
});
// if no video mode with the given bit depth exists, fall
// back to a video mode that fits only the resolution
match correct_rate {
Some(mode) => Some(mode),
None => {
warn!(
"Refresh rate specified in settings is incompatible with the \
monitor. Choosing highest refresh rate possible instead."
);
self.select_video_mode_rec(
resolution,
None,
None,
Some(correct_res),
None,
Some(correct_rate),
)
},
}
},
// No bit depth and no refresh rate is given
// get the video mode with the specified resolution and the max bit depth and
// refresh rate
None => correct_res
.into_iter()
// Prefer bit depth over refresh rate
.sorted_by_key(|mode| mode.bit_depth())
.max_by_key(|mode| mode.refresh_rate_millihertz()),
},
}
}
fn select_video_mode(
&self,
resolution: [u16; 2],
bit_depth: Option<u16>,
refresh_rate_millihertz: Option<u32>,
) -> Option<VideoModeHandle> {
// (resolution, bit depth, refresh rate) represents a video mode
// spec: as specified
// max: maximum value available
// order of fallbacks as follows:
// (spec, spec, spec)
// (spec, spec, max), (spec, max, spec)
// (spec, max, max)
// (max, max, max)
match self.select_video_mode_rec(
resolution,
bit_depth,
refresh_rate_millihertz,
None,
None,
None,
) {
Some(mode) => Some(mode),
// if there is no video mode with the specified resolution,
// fall back to the video mode with max resolution, bit depth and refresh rate
None => {
warn!(
"Resolution specified in settings is incompatible with the monitor. Choosing \
highest resolution possible instead."
);
if let Some(monitor) = self.window.current_monitor() {
let mode = monitor
.video_modes()
// Prefer bit depth over refresh rate
.sorted_by_key(|mode| mode.refresh_rate_millihertz())
.sorted_by_key(|mode| mode.bit_depth())
.max_by_key(|mode| mode.size().width);
if mode.is_none() {
warn!("Failed to select video mode, no video modes available!!")
}
mode
} else {
warn!("Failed to select video mode, can't get the current monitor!");
None
}
},
}
}
pub fn set_fullscreen_mode(&mut self, fullscreen: FullScreenSettings) {
let window = &self.window;
self.fullscreen = fullscreen;
window.set_fullscreen(fullscreen.enabled.then(|| match fullscreen.mode {
FullscreenMode::Exclusive => {
if let Some(video_mode) = self.select_video_mode(
fullscreen.resolution,
fullscreen.bit_depth,
fullscreen.refresh_rate_millihertz,
) {
winit::window::Fullscreen::Exclusive(video_mode)
} else {
warn!(
"Failed to select a video mode for exclusive fullscreen. Falling back to \
borderless fullscreen."
);
winit::window::Fullscreen::Borderless(None)
}
},
FullscreenMode::Borderless => {
// None here will fullscreen on the current monitor
winit::window::Fullscreen::Borderless(None)
},
}));
}
pub fn needs_refresh_resize(&mut self) { self.needs_refresh_resize = true; }
pub fn set_size(&mut self, new_size: Vec2<u32>) {
self.window
.set_min_inner_size(Some(winit::dpi::LogicalSize::new(
new_size.x as f64,
new_size.y as f64,
)));
}
pub fn send_event(&mut self, event: Event) { self.events.push(event) }
pub fn take_screenshot(&mut self, settings: &Settings) {
let sender = self.message_sender.clone();
let mut path = settings.screenshots_path.clone();
self.renderer.create_screenshot(move |image| {
use std::time::SystemTime;
// Handle any error if there was one when generating the image.
let image = match image {
Ok(i) => i,
Err(e) => {
warn!(?e, "Couldn't generate screenshot");
let _result = sender.send(format!("Error when generating screenshot: {}", e));
return;
},
};
// Check if folder exists and create it if it does not
if !path.exists()
&& let Err(e) = std::fs::create_dir_all(&path)
{
warn!(?e, ?path, "Couldn't create folder for screenshot");
let _result = sender.send(String::from("Couldn't create folder for screenshot"));
}
path.push(format!(
"screenshot_{}.png",
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
));
// Try to save the image
if let Err(e) = image.save(&path) {
warn!(?e, ?path, "Couldn't save screenshot");
let _result = sender.send(String::from("Couldn't save screenshot"));
} else {
let _result =
sender.send(format!("Screenshot saved to {}", path.to_string_lossy()));
}
});
}
fn is_pressed(
map: &mut HashMap<GameInput, winit::event::ElementState>,
input: GameInput,
) -> bool {
*(map
.entry(input)
.or_insert(winit::event::ElementState::Released))
== winit::event::ElementState::Pressed
}
fn set_pressed(
map: &mut HashMap<GameInput, winit::event::ElementState>,
input: GameInput,
state: winit::event::ElementState,
) {
map.insert(input, state);
}
// Function used to handle Mouse and Key events. It first checks if we're in
// remapping mode for a specific GameInput or MenuInput. If we are, we modify
// the binding of that GameInput/MenuInput with the KeyMouse passed. Else,
// we return an iterator of the GameInputs/MenuInputs for that KeyMouse. If
// a menu is open, it will return a MenuInput unless there is not one available
// (which it will then return GameInput). If a menu is not open, return
// GameInput.
fn map_input<'a>(
key_mouse: KeyMouse,
controls: &'a mut ControlSettings,
remapping: &mut RemappingMode,
last_input: &mut LastInput,
menu_open: bool,
) -> Option<MappedInput<'a>> {
let key_mouse = key_mouse.into_upper();
// update last input to be Keyboard if button was pressed
*last_input = LastInput::Keyboard;
match *remapping {
RemappingMode::RemapKeyboard(game_input) => {
controls.modify_binding(game_input, key_mouse);
*remapping = RemappingMode::None;
None
},
RemappingMode::RemapKeyboardMenu(menu_input) => {
controls.modify_menu_binding(menu_input, key_mouse);
*remapping = RemappingMode::None;
None
},
RemappingMode::None => {
if !menu_open {
// If a menu is not open, simply return any game inputs
controls
.get_associated_game_inputs(&key_mouse)
.map(|game_inputs| MappedInput::Game(game_inputs.iter()))
} else {
// If a menu is open, return the associated MenuInput if any, otherwise return
// the associated GameInput
if let Some(menu_inputs) = controls.get_associated_menu_inputs(&key_mouse)
&& !menu_inputs.is_empty()
{
return Some(MappedInput::Menu(menu_inputs.iter()));
}
controls
.get_associated_game_inputs(&key_mouse)
.map(|game_inputs| MappedInput::Game(game_inputs.iter()))
}
},
_ => None,
}
}
// Same thing as map_input, but for controller actions
#[expect(clippy::get_first)]
fn map_controller_input<'a>(
controller: &'a mut ControllerSettings,
remapping: &mut RemappingMode,
modifiers: &[Button],
button: &Button,
mod1_input: bool,
mod2_input: bool,
menu_open: bool,
) -> Option<MappedInput<'a>> {
match *remapping {
RemappingMode::RemapGamepadLayers(game_input) => {
// create the new layer entry
let new_layer_entry = LayerEntry {
button: *button,
mod1: if mod1_input {
Button::Simple(GilButton::RightTrigger)
} else {
Button::Simple(GilButton::Unknown)
},
mod2: if mod2_input {
Button::Simple(GilButton::LeftTrigger)
} else {
Button::Simple(GilButton::Unknown)
},
};
controller.modify_layer_binding(game_input, new_layer_entry);
*remapping = RemappingMode::None;
None
},
RemappingMode::RemapGamepadButtons(game_input) => {
controller.modify_button_binding(game_input, *button);
*remapping = RemappingMode::None;
None
},
RemappingMode::RemapGamepadMenu(menu_input) => {
controller.modify_menu_binding(menu_input, *button);
*remapping = RemappingMode::None;
None
},
RemappingMode::None => {
// have to check l_entry1 and l_entry2 so LB+RB can be treated equivalent to
// RB+LB
let l_entry1 = LayerEntry {
button: *button,
mod1: modifiers.get(0).copied().unwrap_or_default(),
mod2: modifiers.get(1).copied().unwrap_or_default(),
};
let l_entry2 = LayerEntry {
button: *button,
mod1: modifiers.get(1).copied().unwrap_or_default(),
mod2: modifiers.get(0).copied().unwrap_or_default(),
};
if !menu_open {
// If a menu is not open, simply return any game inputs
// First check layer entries
if let Some(game_inputs) =
controller.get_associated_game_layer_inputs(&l_entry1)
{
Some(MappedInput::Game(game_inputs.iter()))
} else if let Some(game_inputs) =
controller.get_associated_game_layer_inputs(&l_entry2)
{
Some(MappedInput::Game(game_inputs.iter()))
} else {
// check button entries
controller
.get_associated_game_button_inputs(button)
.map(|game_inputs| MappedInput::Game(game_inputs.iter()))
}
} else {
// If a menu is open, return the associated MenuInput if any, otherwise return
// the associated GameInput
if let Some(menu_inputs) = controller.get_associated_game_menu_inputs(button)
&& !menu_inputs.is_empty()
{
return Some(MappedInput::Menu(menu_inputs.iter()));
}
// First check layer entries
if let Some(game_inputs) =
controller.get_associated_game_layer_inputs(&l_entry1)
{
Some(MappedInput::Game(game_inputs.iter()))
} else if let Some(game_inputs) =
controller.get_associated_game_layer_inputs(&l_entry2)
{
Some(MappedInput::Game(game_inputs.iter()))
} else {
// check button entries
controller
.get_associated_game_button_inputs(button)
.map(|game_inputs| MappedInput::Game(game_inputs.iter()))
}
}
},
_ => None,
}
}
pub fn set_remapping_mode(&mut self, r_mode: RemappingMode) { self.remapping_mode = r_mode; }
pub fn reset_mapping_mode(&mut self) { self.remapping_mode = RemappingMode::None; }
pub fn window(&self) -> &winit::window::Window { &self.window }
pub fn modifiers(&self) -> winit::keyboard::ModifiersState { self.modifiers }
pub fn scale_factor(&self) -> f64 { self.scale_factor }
pub fn last_input(&self) -> LastInput { self.last_input }
pub fn controller_type(&self) -> ControllerType { self.controller_type }
}
#[derive(Default, Copy, Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub enum FullscreenMode {
Exclusive,
#[serde(other)]
#[default]
Borderless,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct WindowSettings {
pub size: [u32; 2],
pub maximised: bool,
}
impl Default for WindowSettings {
fn default() -> Self {
Self {
size: [1280, 720],
maximised: false,
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct FullScreenSettings {
pub enabled: bool,
pub mode: FullscreenMode,
pub resolution: [u16; 2],
pub bit_depth: Option<u16>,
pub refresh_rate_millihertz: Option<u32>,
}
impl Default for FullScreenSettings {
fn default() -> Self {
Self {
enabled: true,
mode: FullscreenMode::Borderless,
resolution: [1920, 1080],
bit_depth: None,
refresh_rate_millihertz: None,
}
}
}