Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Optionally resize Window canvas element to fit parent element #4726

Closed
wants to merge 11 commits into from
44 changes: 39 additions & 5 deletions crates/bevy_window/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ pub struct Window {
raw_window_handle: RawWindowHandleWrapper,
focused: bool,
mode: WindowMode,
#[cfg(target_arch = "wasm32")]
pub canvas: Option<String>,
canvas: Option<String>,
fit_canvas_to_parent: bool,
command_queue: Vec<WindowCommand>,
}

Expand Down Expand Up @@ -266,8 +266,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(),
}
}
Expand Down Expand Up @@ -584,6 +584,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)]
Expand All @@ -609,8 +631,20 @@ pub struct WindowDescriptor {
/// macOS X transparent works with winit out of the box, so this issue might be related to: <https://github.com/gfx-rs/wgpu/issues/687>
/// Windows 11 is related to <https://github.com/rust-windowing/winit/issues/2082>
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<String>,
/// 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 {
Expand All @@ -629,8 +663,8 @@ impl Default for WindowDescriptor {
cursor_visible: true,
mode: WindowMode::Windowed,
transparent: false,
#[cfg(target_arch = "wasm32")]
canvas: None,
fit_canvas_to_parent: false,
}
}
}
1 change: 1 addition & 0 deletions crates/bevy_winit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
52 changes: 31 additions & 21 deletions crates/bevy_winit/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod converters;
#[cfg(target_arch = "wasm32")]
mod web_resize;
mod winit_config;
mod winit_windows;

Expand Down Expand Up @@ -44,9 +46,15 @@ impl Plugin for WinitPlugin {
.init_resource::<WinitSettings>()
.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);
cart marked this conversation as resolved.
Show resolved Hide resolved
app.insert_resource(create_window_reader)
.insert_non_send_resource(event_loop);
}
}

Expand Down Expand Up @@ -254,19 +262,27 @@ impl Default for WinitPersistentState {
}
}

#[derive(Default)]
struct WinitCreateWindowReader(ManualEventReader<CreateWindow>);

pub fn winit_runner_with(mut app: App) {
let mut event_loop = app
.world
.remove_non_send_resource::<EventLoop<()>>()
.unwrap();
let mut create_window_event_reader = ManualEventReader::<CreateWindow>::default();
let mut create_window_event_reader = app
.world
.remove_resource::<WinitCreateWindowReader>()
.unwrap()
.0;
let mut app_exit_event_reader = ManualEventReader::<AppExit>::default();
let mut redraw_event_reader = ManualEventReader::<RequestRedraw>::default();
let mut winit_state = WinitPersistentState::default();
app.world
.insert_non_send_resource(event_loop.create_proxy());

let return_from_run = app.world.resource::<WinitSettings>().return_from_run;

trace!("Entering winit event loop");

let event_handler = move |event: Event<()>,
Expand Down Expand Up @@ -609,24 +625,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::<WinitWindows>();
let mut windows = world.resource_mut::<Windows>();
let mut create_window_events = world.resource_mut::<Events<CreateWindow>>();
let mut window_created_events = world.resource_mut::<Events<WindowCreated>>();
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::<web_resize::CanvasParentResizeEventChannel>();
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);
}
}
}
}
83 changes: 83 additions & 0 deletions crates/bevy_winit/src/web_resize.rs
Original file line number Diff line number Diff line change
@@ -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::<CanvasParentResizeEventChannel>()
.add_system(canvas_parent_resize_event_handler);
}
}

struct ResizeEvent {
size: LogicalSize<f32>,
window_id: WindowId,
}

pub(crate) struct CanvasParentResizeEventChannel {
sender: Sender<ResizeEvent>,
receiver: Receiver<ResizeEvent>,
}

fn canvas_parent_resize_event_handler(
winit_windows: Res<WinitWindows>,
resize_events: Res<CanvasParentResizeEventChannel>,
) {
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<LogicalSize<f32>> {
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<dyn FnMut(_)>);
let window = web_sys::window().unwrap();

window
.add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
}