bevy/crates/bevy_winit/src/winit_windows.rs
Johan Klokkhammer Helsing 7ebc68bb84 Allow not preventing default event behaviors on wasm (#7304)
# Objective

On wasm, bevy applications currently prevent any of the normal browser hotkeys from working normally (Ctrl+R, F12, F5, Ctrl+F5, tab, etc.).

Some of those events you may want to override, perhaps you can hold the tab key for showing in-game stats?

However, if you want to make a well-behaved game, you probably don't want to needlessly prevent that behavior unless you have a good reason.

Secondary motivation: Also, consider the workaround presented here to get audio working: https://developer.chrome.com/blog/web-audio-autoplay/#moving-forward ; It won't work (for keydown events) if we stop event propagation.

## Solution

- Winit has a field that allows it to not stop event propagation, expose it on the window settings to allow the user to choose the desired behavior. Default to `true` for backwards compatibility.

---

## Changelog

- Added `Window::prevent_default_event_handling` . This allows bevy apps to not override default browser behavior on hotkeys like F5, F12, Ctrl+R etc.
2023-01-22 23:35:32 +00:00

311 lines
12 KiB
Rust

use bevy_ecs::entity::Entity;
use bevy_utils::{tracing::warn, HashMap};
use bevy_window::{CursorGrabMode, Window, WindowMode, WindowPosition, WindowResolution};
use winit::{
dpi::{LogicalSize, PhysicalPosition},
monitor::MonitorHandle,
};
#[derive(Debug, Default)]
pub struct WinitWindows {
pub windows: HashMap<winit::window::WindowId, winit::window::Window>,
pub entity_to_winit: HashMap<Entity, winit::window::WindowId>,
pub winit_to_entity: HashMap<winit::window::WindowId, Entity>,
// Some winit functions, such as `set_window_icon` can only be used from the main thread. If
// they are used in another thread, the app will hang. This marker ensures `WinitWindows` is
// only ever accessed with bevy's non-send functions and in NonSend systems.
_not_send_sync: core::marker::PhantomData<*const ()>,
}
impl WinitWindows {
pub fn create_window(
&mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<()>,
entity: Entity,
window: &Window,
) -> &winit::window::Window {
let mut winit_window_builder = winit::window::WindowBuilder::new();
winit_window_builder = match window.mode {
WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some(
winit::window::Fullscreen::Borderless(event_loop.primary_monitor()),
)),
WindowMode::Fullscreen => {
winit_window_builder.with_fullscreen(Some(winit::window::Fullscreen::Exclusive(
get_best_videomode(&event_loop.primary_monitor().unwrap()),
)))
}
WindowMode::SizedFullscreen => winit_window_builder.with_fullscreen(Some(
winit::window::Fullscreen::Exclusive(get_fitting_videomode(
&event_loop.primary_monitor().unwrap(),
window.width() as u32,
window.height() as u32,
)),
)),
WindowMode::Windowed => {
if let Some(position) = winit_window_position(
&window.position,
&window.resolution,
event_loop.available_monitors(),
event_loop.primary_monitor(),
None,
) {
winit_window_builder = winit_window_builder.with_position(position);
}
let logical_size = LogicalSize::new(window.width(), window.height());
if let Some(sf) = window.resolution.scale_factor_override() {
winit_window_builder.with_inner_size(logical_size.to_physical::<f64>(sf))
} else {
winit_window_builder.with_inner_size(logical_size)
}
}
};
winit_window_builder = winit_window_builder
.with_always_on_top(window.always_on_top)
.with_resizable(window.resizable)
.with_decorations(window.decorations)
.with_transparent(window.transparent);
let constraints = window.resize_constraints.check_constraints();
let min_inner_size = LogicalSize {
width: constraints.min_width,
height: constraints.min_height,
};
let max_inner_size = LogicalSize {
width: constraints.max_width,
height: constraints.max_height,
};
let winit_window_builder =
if constraints.max_width.is_finite() && constraints.max_height.is_finite() {
winit_window_builder
.with_min_inner_size(min_inner_size)
.with_max_inner_size(max_inner_size)
} else {
winit_window_builder.with_min_inner_size(min_inner_size)
};
#[allow(unused_mut)]
let mut winit_window_builder = winit_window_builder.with_title(window.title.as_str());
#[cfg(target_arch = "wasm32")]
{
use wasm_bindgen::JsCast;
use winit::platform::web::WindowBuilderExtWebSys;
if let Some(selector) = &window.canvas {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let canvas = document
.query_selector(&selector)
.expect("Cannot query for canvas element.");
if let Some(canvas) = canvas {
let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok();
winit_window_builder = winit_window_builder.with_canvas(canvas);
} else {
panic!("Cannot find element: {}.", selector);
}
}
winit_window_builder =
winit_window_builder.with_prevent_default(window.prevent_default_event_handling)
}
let winit_window = winit_window_builder.build(event_loop).unwrap();
// Do not set the grab mode on window creation if it's none, this can fail on mobile
if window.cursor.grab_mode != CursorGrabMode::None {
attempt_grab(&winit_window, window.cursor.grab_mode);
}
winit_window.set_cursor_visible(window.cursor.visible);
self.entity_to_winit.insert(entity, winit_window.id());
self.winit_to_entity.insert(winit_window.id(), entity);
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::WindowExtWebSys;
if window.canvas.is_none() {
let canvas = winit_window.canvas();
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
body.append_child(&canvas)
.expect("Append canvas to HTML body.");
}
}
self.windows
.entry(winit_window.id())
.insert(winit_window)
.into_mut()
}
/// Get the winit window that is associated with our entity.
pub fn get_window(&self, entity: Entity) -> Option<&winit::window::Window> {
self.entity_to_winit
.get(&entity)
.and_then(|winit_id| self.windows.get(winit_id))
}
/// Get the entity associated with the winit window id.
///
/// This is mostly just an intermediary step between us and winit.
pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option<Entity> {
self.winit_to_entity.get(&winit_id).cloned()
}
/// Remove a window from winit.
///
/// This should mostly just be called when the window is closing.
pub fn remove_window(&mut self, entity: Entity) -> Option<winit::window::Window> {
let winit_id = self.entity_to_winit.remove(&entity)?;
// Don't remove from winit_to_window_id, to track that we used to know about this winit window
self.windows.remove(&winit_id)
}
}
pub fn get_fitting_videomode(
monitor: &winit::monitor::MonitorHandle,
width: u32,
height: u32,
) -> winit::monitor::VideoMode {
let mut modes = monitor.video_modes().collect::<Vec<_>>();
fn abs_diff(a: u32, b: u32) -> u32 {
if a > b {
return a - b;
}
b - a
}
modes.sort_by(|a, b| {
use std::cmp::Ordering::*;
match abs_diff(a.size().width, width).cmp(&abs_diff(b.size().width, width)) {
Equal => {
match abs_diff(a.size().height, height).cmp(&abs_diff(b.size().height, height)) {
Equal => b
.refresh_rate_millihertz()
.cmp(&a.refresh_rate_millihertz()),
default => default,
}
}
default => default,
}
});
modes.first().unwrap().clone()
}
pub fn get_best_videomode(monitor: &winit::monitor::MonitorHandle) -> winit::monitor::VideoMode {
let mut modes = monitor.video_modes().collect::<Vec<_>>();
modes.sort_by(|a, b| {
use std::cmp::Ordering::*;
match b.size().width.cmp(&a.size().width) {
Equal => match b.size().height.cmp(&a.size().height) {
Equal => b
.refresh_rate_millihertz()
.cmp(&a.refresh_rate_millihertz()),
default => default,
},
default => default,
}
});
modes.first().unwrap().clone()
}
pub(crate) fn attempt_grab(winit_window: &winit::window::Window, grab_mode: CursorGrabMode) {
let grab_result = match grab_mode {
bevy_window::CursorGrabMode::None => {
winit_window.set_cursor_grab(winit::window::CursorGrabMode::None)
}
bevy_window::CursorGrabMode::Confined => winit_window
.set_cursor_grab(winit::window::CursorGrabMode::Confined)
.or_else(|_e| winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked)),
bevy_window::CursorGrabMode::Locked => winit_window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)
.or_else(|_e| winit_window.set_cursor_grab(winit::window::CursorGrabMode::Confined)),
};
if let Err(err) = grab_result {
let err_desc = match grab_mode {
bevy_window::CursorGrabMode::Confined | bevy_window::CursorGrabMode::Locked => "grab",
bevy_window::CursorGrabMode::None => "ungrab",
};
bevy_utils::tracing::error!("Unable to {} cursor: {}", err_desc, err);
}
}
// Ideally we could generify this across window backends, but we only really have winit atm
// so whatever.
pub fn winit_window_position(
position: &WindowPosition,
resolution: &WindowResolution,
mut available_monitors: impl Iterator<Item = MonitorHandle>,
primary_monitor: Option<MonitorHandle>,
current_monitor: Option<MonitorHandle>,
) -> Option<PhysicalPosition<i32>> {
match position {
WindowPosition::Automatic => {
/* Window manager will handle position */
None
}
WindowPosition::Centered(monitor_selection) => {
use bevy_window::MonitorSelection::*;
let maybe_monitor = match monitor_selection {
Current => {
if current_monitor.is_none() {
warn!("Can't select current monitor on window creation or cannot find current monitor!");
}
current_monitor
}
Primary => primary_monitor,
Index(n) => available_monitors.nth(*n),
};
if let Some(monitor) = maybe_monitor {
let screen_size = monitor.size();
let scale_factor = resolution.base_scale_factor();
// Logical to physical window size
let (width, height): (u32, u32) =
LogicalSize::new(resolution.width(), resolution.height())
.to_physical::<u32>(scale_factor)
.into();
let position = PhysicalPosition {
x: screen_size.width.saturating_sub(width) as f64 / 2.
+ monitor.position().x as f64,
y: screen_size.height.saturating_sub(height) as f64 / 2.
+ monitor.position().y as f64,
};
Some(position.cast::<i32>())
} else {
warn!("Couldn't get monitor selected with: {monitor_selection:?}");
None
}
}
WindowPosition::At(position) => {
Some(PhysicalPosition::new(position[0] as f64, position[1] as f64).cast::<i32>())
}
}
}
// WARNING: this only works under the assumption that wasm runtime is single threaded
#[cfg(target_arch = "wasm32")]
unsafe impl Send for WinitWindows {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for WinitWindows {}