diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index dc4225db9b3844..f7d0e1d54a857d 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -165,8 +165,8 @@ pub struct Window { raw_window_handle: RawWindowHandleWrapper, focused: bool, mode: WindowMode, - #[cfg(target_arch = "wasm32")] - pub canvas: Option, + canvas: Option, + fit_canvas_to_parent: bool, command_queue: Vec, } @@ -267,8 +267,8 @@ impl Window { raw_window_handle: RawWindowHandleWrapper::new(raw_window_handle), focused: true, mode: window_descriptor.mode, - #[cfg(target_arch = "wasm32")] canvas: window_descriptor.canvas.clone(), + fit_canvas_to_parent: window_descriptor.fit_canvas_to_parent, command_queue: Vec::new(), } } @@ -600,6 +600,28 @@ impl Window { pub fn raw_window_handle(&self) -> RawWindowHandleWrapper { self.raw_window_handle.clone() } + + /// The "html canvas" element selector. If set, this selector will be used to find a matching html canvas element, + /// rather than creating a new one. + /// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector). + /// + /// This value has no effect on non-web platforms. + #[inline] + pub fn canvas(&self) -> Option<&str> { + self.canvas.as_deref() + } + + /// Whether or not to fit the canvas element's size to its parent element's size. + /// + /// **Warning**: this will not behave as expected for parents that set their size according to the size of their + /// children. This creates a "feedback loop" that will result in the canvas growing on each resize. When using this + /// feature, ensure the parent's size is not affected by its children. + /// + /// This value has no effect on non-web platforms. + #[inline] + pub fn fit_canvas_to_parent(&self) -> bool { + self.fit_canvas_to_parent + } } #[derive(Debug, Clone)] @@ -625,8 +647,20 @@ pub struct WindowDescriptor { /// macOS X transparent works with winit out of the box, so this issue might be related to: /// Windows 11 is related to pub transparent: bool, - #[cfg(target_arch = "wasm32")] + /// The "html canvas" element selector. If set, this selector will be used to find a matching html canvas element, + /// rather than creating a new one. + /// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector). + /// + /// This value has no effect on non-web platforms. pub canvas: Option, + /// Whether or not to fit the canvas element's size to its parent element's size. + /// + /// **Warning**: this will not behave as expected for parents that set their size according to the size of their + /// children. This creates a "feedback loop" that will result in the canvas growing on each resize. When using this + /// feature, ensure the parent's size is not affected by its children. + /// + /// This value has no effect on non-web platforms. + pub fit_canvas_to_parent: bool, } impl Default for WindowDescriptor { @@ -645,8 +679,8 @@ impl Default for WindowDescriptor { cursor_visible: true, mode: WindowMode::Windowed, transparent: false, - #[cfg(target_arch = "wasm32")] canvas: None, + fit_canvas_to_parent: false, } } } diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index d4e4eb49d85eb8..e896dd6c400be1 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -30,6 +30,7 @@ raw-window-handle = "0.4.2" winit = { version = "0.26.0", default-features = false } wasm-bindgen = { version = "0.2" } web-sys = "0.3" +crossbeam-channel = "0.5" [package.metadata.docs.rs] features = ["x11"] diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index fb77c5fc22b176..676801fe2ad37a 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,4 +1,6 @@ mod converters; +#[cfg(target_arch = "wasm32")] +mod web_resize; mod winit_config; mod winit_windows; @@ -43,9 +45,15 @@ impl Plugin for WinitPlugin { .init_resource::() .set_runner(winit_runner) .add_system_to_stage(CoreStage::PostUpdate, change_window.label(ModifiesWindows)); + #[cfg(target_arch = "wasm32")] + app.add_plugin(web_resize::CanvasParentResizePlugin); let event_loop = EventLoop::new(); - handle_initial_window_events(&mut app.world, &event_loop); - app.insert_non_send_resource(event_loop); + let mut create_window_reader = WinitCreateWindowReader::default(); + // Note that we create a window here "early" because WASM/WebGL requires the window to exist prior to initializing + // the renderer. + handle_create_window_events(&mut app.world, &event_loop, &mut create_window_reader.0); + app.insert_resource(create_window_reader) + .insert_non_send_resource(event_loop); } } @@ -271,12 +279,19 @@ impl Default for WinitPersistentState { } } +#[derive(Default)] +struct WinitCreateWindowReader(ManualEventReader); + pub fn winit_runner_with(mut app: App) { let mut event_loop = app .world .remove_non_send_resource::>() .unwrap(); - let mut create_window_event_reader = ManualEventReader::::default(); + let mut create_window_event_reader = app + .world + .remove_resource::() + .unwrap() + .0; let mut app_exit_event_reader = ManualEventReader::::default(); let mut redraw_event_reader = ManualEventReader::::default(); let mut winit_state = WinitPersistentState::default(); @@ -284,6 +299,7 @@ pub fn winit_runner_with(mut app: App) { .insert_non_send_resource(event_loop.create_proxy()); let return_from_run = app.world.resource::().return_from_run; + trace!("Entering winit event loop"); let event_handler = move |event: Event<()>, @@ -627,24 +643,18 @@ fn handle_create_window_events( window_created_events.send(WindowCreated { id: create_window_event.id, }); - } -} -fn handle_initial_window_events(world: &mut World, event_loop: &EventLoop<()>) { - let world = world.cell(); - let mut winit_windows = world.non_send_resource_mut::(); - let mut windows = world.resource_mut::(); - let mut create_window_events = world.resource_mut::>(); - let mut window_created_events = world.resource_mut::>(); - for create_window_event in create_window_events.drain() { - let window = winit_windows.create_window( - event_loop, - create_window_event.id, - &create_window_event.descriptor, - ); - windows.add(window); - window_created_events.send(WindowCreated { - id: create_window_event.id, - }); + #[cfg(target_arch = "wasm32")] + { + let channel = world.resource_mut::(); + if create_window_event.descriptor.fit_canvas_to_parent { + let selector = if let Some(selector) = &create_window_event.descriptor.canvas { + selector + } else { + web_resize::WINIT_CANVAS_SELECTOR + }; + channel.listen_to_selector(create_window_event.id, selector); + } + } } } diff --git a/crates/bevy_winit/src/web_resize.rs b/crates/bevy_winit/src/web_resize.rs new file mode 100644 index 00000000000000..637dc06e0eaed4 --- /dev/null +++ b/crates/bevy_winit/src/web_resize.rs @@ -0,0 +1,83 @@ +use crate::WinitWindows; +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; +use bevy_window::WindowId; +use crossbeam_channel::{Receiver, Sender}; +use wasm_bindgen::JsCast; +use winit::dpi::LogicalSize; + +pub(crate) struct CanvasParentResizePlugin; + +impl Plugin for CanvasParentResizePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_system(canvas_parent_resize_event_handler); + } +} + +struct ResizeEvent { + size: LogicalSize, + window_id: WindowId, +} + +pub(crate) struct CanvasParentResizeEventChannel { + sender: Sender, + receiver: Receiver, +} + +fn canvas_parent_resize_event_handler( + winit_windows: Res, + resize_events: Res, +) { + for event in resize_events.receiver.try_iter() { + if let Some(window) = winit_windows.get_window(event.window_id) { + window.set_inner_size(event.size); + } + } +} + +fn get_size(selector: &str) -> Option> { + let win = web_sys::window().unwrap(); + let doc = win.document().unwrap(); + let element = doc.query_selector(selector).ok()??; + let parent_element = element.parent_element()?; + let rect = parent_element.get_bounding_client_rect(); + return Some(winit::dpi::LogicalSize::new( + rect.width() as f32, + rect.height() as f32, + )); +} + +pub(crate) const WINIT_CANVAS_SELECTOR: &str = "canvas[data-raw-handle]"; + +impl Default for CanvasParentResizeEventChannel { + fn default() -> Self { + let (sender, receiver) = crossbeam_channel::unbounded(); + return Self { sender, receiver }; + } +} + +impl CanvasParentResizeEventChannel { + pub(crate) fn listen_to_selector(&self, window_id: WindowId, selector: &str) { + let sender = self.sender.clone(); + let owned_selector = selector.to_string(); + let resize = move || { + if let Some(size) = get_size(&owned_selector) { + sender.send(ResizeEvent { size, window_id }).unwrap(); + } + }; + + // ensure resize happens on startup + resize(); + + let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::Event| { + resize(); + }) as Box); + let window = web_sys::window().unwrap(); + + window + .add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); + } +}