diff --git a/CHANGELOG.md b/CHANGELOG.md index 578187a0..68fda1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On Web, fix pen treated as mouse input. - On Web, send mouse position on button release as well. - On Web, fix touch input not gaining or loosing focus. -- **Breaking:** On Web, dropped support for Safari versions below 13. +- **Breaking:** On Web, dropped support for Safari versions below 13.1. - On Web, prevent clicks on the canvas to select text. - On Web, `EventLoopProxy` now implements `Send`. - On Web, `Window` now implements `Send` and `Sync`. @@ -79,6 +79,8 @@ And please only add new entries to the top of this list, right below the `# Unre - On Web, fix scale factor resize suggestion always overwriting the canvas size. - On macOS, fix crash when dropping `Window`. - On Web, use `Window.requestIdleCallback()` for `ControlFlow::Poll` when available. +- **Breaking:** On Web, the canvas size is not controlled by Winit anymore and external changes to + the canvas size will be reported through `WindowEvent::Resized`. # 0.28.6 diff --git a/Cargo.toml b/Cargo.toml index 9ff849e3..a9198675 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,12 +129,13 @@ redox_syscall = "0.3" [target.'cfg(target_family = "wasm")'.dependencies.web_sys] package = "web-sys" -version = "0.3" +version = "0.3.64" features = [ 'console', 'CssStyleDeclaration', 'Document', 'DomRect', + 'DomRectReadOnly', 'Element', 'Event', 'EventTarget', @@ -145,6 +146,11 @@ features = [ 'MediaQueryList', 'Node', 'PointerEvent', + 'ResizeObserver', + 'ResizeObserverBoxOptions', + 'ResizeObserverEntry', + 'ResizeObserverOptions', + 'ResizeObserverSize', 'Window', 'WheelEvent' ] diff --git a/examples/web.rs b/examples/web.rs index ea32f06e..c1df6987 100644 --- a/examples/web.rs +++ b/examples/web.rs @@ -1,9 +1,10 @@ #![allow(clippy::disallowed_methods, clippy::single_match)] use winit::{ - event::{Event, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::EventLoop, - window::WindowBuilder, + keyboard::KeyCode, + window::{Fullscreen, WindowBuilder}, }; pub fn main() { @@ -31,6 +32,25 @@ pub fn main() { Event::MainEventsCleared => { window.request_redraw(); } + Event::WindowEvent { + window_id, + event: + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: KeyCode::KeyF, + state: ElementState::Released, + .. + }, + .. + }, + } if window_id == window.id() => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + window.set_fullscreen(Some(Fullscreen::Borderless(None))); + } + } _ => (), } }); diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 6bec70c9..e93a79da 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -1,5 +1,5 @@ mod proxy; -mod runner; +pub(crate) mod runner; mod state; mod window_target; diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index 42494cfd..f255dc2f 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,4 +1,5 @@ -use super::{super::ScaleChangeArgs, backend, state::State}; +use super::{backend, state::State}; +use crate::dpi::PhysicalSize; use crate::event::{Event, StartCause}; use crate::event_loop::ControlFlow; use crate::window::WindowId; @@ -25,13 +26,12 @@ impl Clone for Shared { pub struct Execution { runner: RefCell>, - events: RefCell>>, + events: RefCell>>, id: RefCell, window: web_sys::Window, all_canvases: RefCell>)>>, redraw_pending: RefCell>, destroy_pending: RefCell>, - scale_change_detector: RefCell>, unload_event_handle: RefCell>, } @@ -86,10 +86,31 @@ impl Runner { }) } - fn handle_single_event(&mut self, event: Event<'_, T>, control: &mut ControlFlow) { + fn handle_single_event( + &mut self, + runner: &Shared, + event: impl Into>, + control: &mut ControlFlow, + ) { let is_closed = matches!(*control, ControlFlow::ExitWithCode(_)); - (self.event_handler)(event, control); + match event.into() { + EventWrapper::Event(event) => (self.event_handler)(event, control), + EventWrapper::ScaleChange { + canvas, + size, + scale, + } => { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().handle_scale_change( + runner, + |event| (self.event_handler)(event, control), + size, + scale, + ) + } + } + } // Maintain closed state, even if the callback changes it if is_closed { @@ -109,7 +130,6 @@ impl Shared { all_canvases: RefCell::new(Vec::new()), redraw_pending: RefCell::new(HashSet::new()), destroy_pending: RefCell::new(VecDeque::new()), - scale_change_detector: RefCell::new(None), unload_event_handle: RefCell::new(None), })) } @@ -147,16 +167,6 @@ impl Shared { })); } - pub(crate) fn set_on_scale_change(&self, handler: F) - where - F: 'static + FnMut(ScaleChangeArgs), - { - *self.0.scale_change_detector.borrow_mut() = Some(backend::ScaleChangeDetector::new( - self.window().clone(), - handler, - )); - } - // Generate a strictly increasing ID // This is used to differentiate windows when handling events pub fn generate_id(&self) -> u32 { @@ -168,7 +178,7 @@ impl Shared { pub fn request_redraw(&self, id: WindowId) { self.0.redraw_pending.borrow_mut().insert(id); - self.send_events(iter::empty()); + self.send_events::>(iter::empty()); } pub fn init(&self) { @@ -196,14 +206,17 @@ impl Shared { // Add an event to the event loop runner, from the user or an event handler // // It will determine if the event should be immediately sent to the user or buffered for later - pub fn send_event(&self, event: Event<'static, T>) { + pub(crate) fn send_event>>(&self, event: E) { self.send_events(iter::once(event)); } // Add a series of events to the event loop runner // // It will determine if the event should be immediately sent to the user or buffered for later - pub fn send_events(&self, events: impl IntoIterator>) { + pub(crate) fn send_events>>( + &self, + events: impl IntoIterator, + ) { // If the event loop is closed, it should discard any new events if self.is_closed() { return; @@ -232,7 +245,10 @@ impl Shared { } if !process_immediately { // Queue these events to look at later - self.0.events.borrow_mut().extend(events); + self.0 + .events + .borrow_mut() + .extend(events.into_iter().map(Into::into)); return; } // At this point, we know this is a fresh set of events @@ -250,13 +266,13 @@ impl Shared { // Take the start event, then the events provided to this function, and run an iteration of // the event loop let start_event = Event::NewEvents(start_cause); - let events = iter::once(start_event).chain(events); + let events = + iter::once(EventWrapper::from(start_event)).chain(events.into_iter().map(Into::into)); self.run_until_cleared(events); } // Process the destroy-pending windows. This should only be called from - // `run_until_cleared` and `handle_scale_changed`, somewhere between emitting - // `NewEvents` and `MainEventsCleared`. + // `run_until_cleared`, somewhere between emitting `NewEvents` and `MainEventsCleared`. fn process_destroy_pending_windows(&self, control: &mut ControlFlow) { while let Some(id) = self.0.destroy_pending.borrow_mut().pop_front() { self.0 @@ -278,10 +294,10 @@ impl Shared { // cleared // // This will also process any events that have been queued or that are queued during processing - fn run_until_cleared(&self, events: impl Iterator>) { + fn run_until_cleared>>(&self, events: impl Iterator) { let mut control = self.current_control_flow(); for event in events { - self.handle_event(event, &mut control); + self.handle_event(event.into(), &mut control); } self.process_destroy_pending_windows(&mut control); self.handle_event(Event::MainEventsCleared, &mut control); @@ -301,85 +317,6 @@ impl Shared { } } - pub fn handle_scale_changed(&self, old_scale: f64, new_scale: f64) { - // If there aren't any windows, then there is nothing to do here. - if self.0.all_canvases.borrow().is_empty() { - return; - } - - let start_cause = match (self.0.runner.borrow().maybe_runner()) - .unwrap_or_else(|| unreachable!("`scale_changed` should not happen without a runner")) - .maybe_start_cause() - { - Some(c) => c, - // If we're in the exit state, don't do event processing - None => return, - }; - let mut control = self.current_control_flow(); - - // Handle the start event and all other events in the queue. - self.handle_event(Event::NewEvents(start_cause), &mut control); - - // It is possible for windows to be dropped before this point. We don't - // want to send `ScaleFactorChanged` for destroyed windows, so we process - // the destroy-pending windows here. - self.process_destroy_pending_windows(&mut control); - - // Now handle the `ScaleFactorChanged` events. - for &(id, ref canvas) in &*self.0.all_canvases.borrow() { - let rc = match canvas.upgrade() { - Some(rc) => rc, - // This shouldn't happen, but just in case... - None => continue, - }; - let canvas = rc.borrow(); - // First, we send the `ScaleFactorChanged` event: - let current_size = canvas.size().get(); - let logical_size = current_size.to_logical::(old_scale); - let mut new_size = logical_size.to_physical(new_scale); - self.handle_single_event_sync( - Event::WindowEvent { - window_id: id, - event: crate::event::WindowEvent::ScaleFactorChanged { - scale_factor: new_scale, - new_inner_size: &mut new_size, - }, - }, - &mut control, - ); - - // Then we resize the canvas to the new size and send a `Resized` event: - if current_size != new_size { - backend::set_canvas_size(&canvas, crate::dpi::Size::Physical(new_size)); - self.handle_single_event_sync( - Event::WindowEvent { - window_id: id, - event: crate::event::WindowEvent::Resized(new_size), - }, - &mut control, - ); - } - } - - // Process the destroy-pending windows again. - self.process_destroy_pending_windows(&mut control); - self.handle_event(Event::MainEventsCleared, &mut control); - - // Discard all the pending redraw as we shall just redraw all windows. - self.0.redraw_pending.borrow_mut().clear(); - for &(window_id, _) in &*self.0.all_canvases.borrow() { - self.handle_event(Event::RedrawRequested(window_id), &mut control); - } - self.handle_event(Event::RedrawEventsCleared, &mut control); - - self.apply_control_flow(control); - // If the event loop is closed, it has been closed this iteration and now the closing - // event should be emitted - if self.is_closed() { - self.handle_loop_destroyed(&mut control); - } - } - fn handle_unload(&self) { self.apply_control_flow(ControlFlow::Exit); let mut control = self.current_control_flow(); @@ -388,35 +325,20 @@ impl Shared { self.handle_event(Event::LoopDestroyed, &mut control); } - // handle_single_event_sync takes in an event and handles it synchronously. - // - // It should only ever be called from `scale_changed`. - fn handle_single_event_sync(&self, event: Event<'_, T>, control: &mut ControlFlow) { - if self.is_closed() { - *control = ControlFlow::Exit; - } - match *self.0.runner.borrow_mut() { - RunnerEnum::Running(ref mut runner) => { - runner.handle_single_event(event, control); - } - _ => panic!("Cannot handle event synchronously without a runner"), - } - } - // handle_event takes in events and either queues them or applies a callback // - // It should only ever be called from `run_until_cleared` and `scale_changed`. - fn handle_event(&self, event: Event<'static, T>, control: &mut ControlFlow) { + // It should only ever be called from `run_until_cleared`. + fn handle_event(&self, event: impl Into>, control: &mut ControlFlow) { if self.is_closed() { *control = ControlFlow::Exit; } match *self.0.runner.borrow_mut() { RunnerEnum::Running(ref mut runner) => { - runner.handle_single_event(event, control); + runner.handle_single_event(self, event, control); } // If an event is being handled without a runner somehow, add it to the event queue so // it will eventually be processed - RunnerEnum::Pending => self.0.events.borrow_mut().push_back(event), + RunnerEnum::Pending => self.0.events.borrow_mut().push_back(event.into()), // If the Runner has been destroyed, there is nothing to do. RunnerEnum::Destroyed => return, } @@ -482,7 +404,6 @@ impl Shared { fn handle_loop_destroyed(&self, control: &mut ControlFlow) { self.handle_event(Event::LoopDestroyed, control); let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut()); - *self.0.scale_change_detector.borrow_mut() = None; *self.0.unload_event_handle.borrow_mut() = None; // Dropping the `Runner` drops the event handler closure, which will in // turn drop all `Window`s moved into the closure. @@ -530,3 +451,18 @@ impl Shared { } } } + +pub(crate) enum EventWrapper { + Event(Event<'static, T>), + ScaleChange { + canvas: Weak>, + size: PhysicalSize, + scale: f64, + }, +} + +impl From> for EventWrapper { + fn from(value: Event<'static, T>) -> Self { + Self::Event(value) + } +} diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index d39de982..5f477d5c 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use raw_window_handle::{RawDisplayHandle, WebDisplayHandle}; +use super::runner::EventWrapper; use super::{ super::{monitor::MonitorHandle, KeyEventExtra}, backend, @@ -16,7 +17,6 @@ use super::{ runner, window::WindowId, }; -use crate::dpi::Size; use crate::event::{ DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, WindowEvent, @@ -71,10 +71,6 @@ impl EventLoopWindowTarget { pub fn run(&self, event_handler: Box>) { self.runner.set_listener(event_handler); - let runner = self.runner.clone(); - self.runner.set_on_scale_change(move |arg| { - runner.handle_scale_changed(arg.old_scale, arg.new_scale) - }); } pub fn generate_id(&self) -> WindowId { @@ -595,35 +591,6 @@ impl EventLoopWindowTarget { prevent_default, ); - // The size to restore to after exiting fullscreen. - let mut intended_size = canvas.size().get(); - - canvas.on_fullscreen_change({ - let window = self.runner.window().clone(); - let runner = self.runner.clone(); - - move || { - let canvas = canvas_clone.borrow(); - - // If the canvas is marked as fullscreen, it is moving *into* fullscreen - // If it is not, it is moving *out of* fullscreen - let new_size = if backend::is_fullscreen(&window, canvas.raw()) { - intended_size = canvas.size().get(); - - backend::window_size(&window).to_physical(backend::scale_factor(&window)) - } else { - intended_size - }; - - backend::set_canvas_size(&canvas, Size::Physical(new_size)); - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::Resized(new_size), - }); - runner.request_redraw(RootWindowId(id)); - } - }); - let runner = self.runner.clone(); canvas.on_touch_cancel(move |device_id, location, force| { runner.send_event(Event::WindowEvent { @@ -650,6 +617,37 @@ impl EventLoopWindowTarget { event: WindowEvent::ThemeChanged(theme), }); }); + + canvas.on_resize_scale( + { + let runner = self.runner.clone(); + let canvas = canvas_clone.clone(); + + move |size, scale| { + runner.send_event(EventWrapper::ScaleChange { + canvas: Rc::downgrade(&canvas), + size, + scale, + }) + } + }, + { + let runner = self.runner.clone(); + + move |new_size| { + let canvas = RefCell::borrow(&canvas_clone); + canvas.set_current_size(new_size); + if canvas.old_size() != new_size { + canvas.set_old_size(new_size); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized(new_size), + }); + runner.request_redraw(RootWindowId(id)); + } + } + }, + ); } pub fn available_monitors(&self) -> VecDequeIter { diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index bea9f0cf..dc178179 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -39,9 +39,3 @@ pub use self::window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId pub(crate) use self::keyboard::KeyEventExtra; pub(crate) use crate::icon::NoIcon as PlatformIcon; pub(self) use crate::platform_impl::Fullscreen; - -#[derive(Clone, Copy)] -pub(crate) struct ScaleChangeArgs { - old_scale: f64, - new_scale: f64, -} diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 273c7c6e..9138d405 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -1,13 +1,14 @@ +use super::super::WindowId; use super::event_handle::EventListenerHandle; use super::media_query_handle::MediaQueryListHandle; use super::pointer::PointerHandler; -use super::{event, ButtonsState}; -use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; +use super::{event, ButtonsState, ResizeScaleHandle}; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; use crate::event::{Force, MouseButton, MouseScrollDelta}; use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState}; use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes}; -use crate::window::WindowAttributes; +use crate::window::{WindowAttributes, WindowId as RootWindowId}; use std::cell::{Cell, RefCell}; use std::rc::Rc; @@ -22,6 +23,7 @@ use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent}; #[allow(dead_code)] pub struct Canvas { common: Common, + id: WindowId, on_touch_start: Option>, on_touch_end: Option>, on_focus: Option>, @@ -29,21 +31,23 @@ pub struct Canvas { on_keyboard_release: Option>, on_keyboard_press: Option>, on_mouse_wheel: Option>, - on_fullscreen_change: Option>, on_dark_mode: Option, pointer_handler: PointerHandler, + on_resize_scale: Option, } pub struct Common { pub window: web_sys::Window, /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained. pub raw: HtmlCanvasElement, - size: Rc>>, + old_size: Rc>>, + current_size: Rc>>, wants_fullscreen: Rc>, } impl Canvas { pub fn create( + id: WindowId, window: web_sys::Window, attr: &WindowAttributes, platform_attr: PlatformSpecificWindowBuilderAttributes, @@ -73,24 +77,20 @@ impl Canvas { .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; } - let size = attr - .inner_size - .unwrap_or( - LogicalSize { - width: 1024.0, - height: 768.0, - } - .into(), - ) - .to_physical(super::scale_factor(&window)); + if let Some(size) = attr.inner_size { + let size = size.to_logical(super::scale_factor(&window)); + super::set_canvas_size(&window, &canvas, size); + } - let canvas = Canvas { + Ok(Canvas { common: Common { window, raw: canvas, - size: Rc::new(Cell::new(size)), + old_size: Rc::default(), + current_size: Rc::default(), wants_fullscreen: Rc::new(RefCell::new(false)), }, + id, on_touch_start: None, on_touch_end: None, on_blur: None, @@ -98,14 +98,10 @@ impl Canvas { on_keyboard_release: None, on_keyboard_press: None, on_mouse_wheel: None, - on_fullscreen_change: None, on_dark_mode: None, pointer_handler: PointerHandler::new(), - }; - - super::set_canvas_size(&canvas, size.into()); - - Ok(canvas) + on_resize_scale: None, + }) } pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> { @@ -138,12 +134,24 @@ impl Canvas { } } - pub fn window(&self) -> &web_sys::Window { - &self.common.window + pub fn old_size(&self) -> PhysicalSize { + self.common.old_size.get() } - pub fn size(&self) -> &Rc>> { - &self.common.size + pub fn inner_size(&self) -> PhysicalSize { + self.common.current_size.get() + } + + pub fn set_old_size(&self, size: PhysicalSize) { + self.common.old_size.set(size) + } + + pub fn set_current_size(&self, size: PhysicalSize) { + self.common.current_size.set(size) + } + + pub fn window(&self) -> &web_sys::Window { + &self.common.window } pub fn raw(&self) -> &HtmlCanvasElement { @@ -329,16 +337,6 @@ impl Canvas { })); } - pub fn on_fullscreen_change(&mut self, mut handler: F) - where - F: 'static + FnMut(), - { - self.on_fullscreen_change = Some( - self.common - .add_event("fullscreenchange", move |_: Event| handler()), - ); - } - pub fn on_dark_mode(&mut self, mut handler: F) where F: 'static + FnMut(bool), @@ -350,6 +348,19 @@ impl Canvas { )); } + pub(crate) fn on_resize_scale(&mut self, scale_handler: S, size_handler: R) + where + S: 'static + FnMut(PhysicalSize, f64), + R: 'static + FnMut(PhysicalSize), + { + self.on_resize_scale = Some(ResizeScaleHandle::new( + self.window().clone(), + self.raw().clone(), + scale_handler, + size_handler, + )); + } + pub fn request_fullscreen(&self) { self.common.request_fullscreen() } @@ -358,15 +369,54 @@ impl Canvas { self.common.is_fullscreen() } + pub(crate) fn handle_scale_change( + &self, + runner: &super::super::event_loop::runner::Shared, + event_handler: impl FnOnce(crate::event::Event<'_, T>), + current_size: PhysicalSize, + scale: f64, + ) { + // First, we send the `ScaleFactorChanged` event: + self.set_current_size(current_size); + let mut new_size = current_size; + event_handler(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::ScaleFactorChanged { + scale_factor: scale, + new_inner_size: &mut new_size, + }, + }); + + if current_size != new_size { + // Then we resize the canvas to the new size, a new + // `Resized` event will be sent by the `ResizeObserver`: + let new_size = new_size.to_logical(scale); + super::set_canvas_size(self.window(), self.raw(), new_size); + + // Set the size might not trigger the event because the calculation is inaccurate. + self.on_resize_scale + .as_ref() + .expect("expected Window to still be active") + .notify_resize(); + } else if self.old_size() != new_size { + // Then we at least send a resized event. + self.set_old_size(new_size); + runner.send_event(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::Resized(new_size), + }) + } + } + pub fn remove_listeners(&mut self) { self.on_focus = None; self.on_blur = None; self.on_keyboard_release = None; self.on_keyboard_press = None; self.on_mouse_wheel = None; - self.on_fullscreen_change = None; self.on_dark_mode = None; - self.pointer_handler.remove_listeners() + self.pointer_handler.remove_listeners(); + self.on_resize_scale = None; } } diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 4a47ebbc..8f6cf630 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -3,19 +3,19 @@ mod event; mod event_handle; mod media_query_handle; mod pointer; -mod scaling; +mod resize_scaling; mod timeout; pub use self::canvas::Canvas; pub use self::event::ButtonsState; -pub use self::scaling::ScaleChangeDetector; +pub use self::resize_scaling::ResizeScaleHandle; pub use self::timeout::{IdleCallback, Timeout}; -use crate::dpi::{LogicalSize, Size}; +use crate::dpi::LogicalSize; use crate::platform::web::WindowExtWebSys; use crate::window::Window; use wasm_bindgen::closure::Closure; -use web_sys::{Element, HtmlCanvasElement}; +use web_sys::{CssStyleDeclaration, Element, HtmlCanvasElement}; pub fn throw(msg: &str) { wasm_bindgen::throw_str(msg); @@ -52,38 +52,52 @@ impl WindowExtWebSys for Window { } } -pub fn window_size(window: &web_sys::Window) -> LogicalSize { - let width = window - .inner_width() - .expect("Failed to get width") - .as_f64() - .expect("Failed to get width as f64"); - let height = window - .inner_height() - .expect("Failed to get height") - .as_f64() - .expect("Failed to get height as f64"); - - LogicalSize { width, height } -} - pub fn scale_factor(window: &web_sys::Window) -> f64 { window.device_pixel_ratio() } -pub fn set_canvas_size(canvas: &Canvas, new_size: Size) { - let scale_factor = scale_factor(canvas.window()); +pub fn set_canvas_size( + window: &web_sys::Window, + raw: &HtmlCanvasElement, + mut new_size: LogicalSize, +) { + let document = window.document().expect("Failed to obtain document"); - let physical_size = new_size.to_physical(scale_factor); - canvas.size().set(physical_size); + let style = window + .get_computed_style(raw) + .expect("Failed to obtain computed style") + // this can't fail: we aren't using a pseudo-element + .expect("Invalid pseudo-element"); - let logical_size = new_size.to_logical::(scale_factor); - set_canvas_style_property(canvas.raw(), "width", &format!("{}px", logical_size.width)); - set_canvas_style_property( - canvas.raw(), - "height", - &format!("{}px", logical_size.height), - ); + if !document.contains(Some(raw)) || style.get_property_value("display").unwrap() == "none" { + return; + } + + if style.get_property_value("box-sizing").unwrap() == "border-box" { + new_size.width += style_size_property(&style, "border-left-width") + + style_size_property(&style, "border-right-width") + + style_size_property(&style, "padding-left") + + style_size_property(&style, "padding-right"); + new_size.height += style_size_property(&style, "border-top-width") + + style_size_property(&style, "border-bottom-width") + + style_size_property(&style, "padding-top") + + style_size_property(&style, "padding-bottom"); + } + + set_canvas_style_property(raw, "width", &format!("{}px", new_size.width)); + set_canvas_style_property(raw, "height", &format!("{}px", new_size.height)); +} + +/// This function will panic if the element is not inserted in the DOM +/// or is not a CSS property that represents a size in pixel. +pub fn style_size_property(style: &CssStyleDeclaration, property: &str) -> f64 { + let prop = style + .get_property_value(property) + .expect("Found invalid property"); + prop.strip_suffix("px") + .expect("Element was not inserted into the DOM or is not a size in pixel") + .parse() + .expect("CSS property is not a size in pixel") } pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: &str) { diff --git a/src/platform_impl/web/web_sys/resize_scaling.rs b/src/platform_impl/web/web_sys/resize_scaling.rs new file mode 100644 index 00000000..43b0faa7 --- /dev/null +++ b/src/platform_impl/web/web_sys/resize_scaling.rs @@ -0,0 +1,321 @@ +use js_sys::{Array, Object}; +use once_cell::unsync::Lazy; +use wasm_bindgen::prelude::{wasm_bindgen, Closure}; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{ + HtmlCanvasElement, MediaQueryList, ResizeObserver, ResizeObserverBoxOptions, + ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize, Window, +}; + +use crate::dpi::{LogicalSize, PhysicalSize}; + +use super::super::backend; +use super::media_query_handle::MediaQueryListHandle; + +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +pub struct ResizeScaleHandle(Rc>); + +impl ResizeScaleHandle { + pub(crate) fn new( + window: Window, + canvas: HtmlCanvasElement, + scale_handler: S, + resize_handler: R, + ) -> Self + where + S: 'static + FnMut(PhysicalSize, f64), + R: 'static + FnMut(PhysicalSize), + { + Self(ResizeScaleInternal::new( + window, + canvas, + scale_handler, + resize_handler, + )) + } + + pub(crate) fn notify_resize(&self) { + self.0.borrow_mut().notify() + } +} + +/// This is a helper type to help manage the `MediaQueryList` used for detecting +/// changes of the `devicePixelRatio`. +struct ResizeScaleInternal { + window: Window, + canvas: HtmlCanvasElement, + mql: MediaQueryListHandle, + observer: ResizeObserver, + _observer_closure: Closure, + scale_handler: Box, f64)>, + resize_handler: Box)>, + notify_scale: Cell, +} + +impl ResizeScaleInternal { + fn new( + window: Window, + canvas: HtmlCanvasElement, + scale_handler: S, + resize_handler: R, + ) -> Rc> + where + S: 'static + FnMut(PhysicalSize, f64), + R: 'static + FnMut(PhysicalSize), + { + Rc::>::new_cyclic(|weak_self| { + let mql = Self::create_mql(&window, { + let weak_self = weak_self.clone(); + move |mql| { + if let Some(rc_self) = weak_self.upgrade() { + Self::handle_scale(rc_self, mql); + } + } + }); + + let weak_self = weak_self.clone(); + let observer_closure = Closure::new(move |entries: Array, _| { + if let Some(rc_self) = weak_self.upgrade() { + let mut this = rc_self.borrow_mut(); + + let size = Self::process_entry(&this.window, &this.canvas, entries); + + if this.notify_scale.replace(false) { + let scale = backend::scale_factor(&this.window); + (this.scale_handler)(size, scale) + } else { + (this.resize_handler)(size) + } + } + }); + let observer = Self::create_observer(&canvas, observer_closure.as_ref()); + + RefCell::new(Self { + window, + canvas, + mql, + observer, + _observer_closure: observer_closure, + scale_handler: Box::new(scale_handler), + resize_handler: Box::new(resize_handler), + notify_scale: Cell::new(false), + }) + }) + } + + fn create_mql(window: &Window, closure: F) -> MediaQueryListHandle + where + F: 'static + FnMut(&MediaQueryList), + { + let current_scale = super::scale_factor(window); + // TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16. + let media_query = format!( + "(resolution: {current_scale}dppx), + (-webkit-device-pixel-ratio: {current_scale})", + ); + let mql = MediaQueryListHandle::new(window, &media_query, closure); + assert!( + mql.mql().matches(), + "created media query doesn't match, {current_scale} != {}", + super::scale_factor(window) + ); + mql + } + + fn create_observer(canvas: &HtmlCanvasElement, closure: &JsValue) -> ResizeObserver { + let observer = ResizeObserver::new(closure.as_ref().unchecked_ref()) + .expect("Failed to create `ResizeObserver`"); + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + observer.observe_with_options( + canvas, + ResizeObserverOptions::new().box_(ResizeObserverBoxOptions::DevicePixelContentBox), + ); + } else { + observer.observe(canvas); + } + + observer + } + + fn notify(&mut self) { + let style = self + .window + .get_computed_style(&self.canvas) + .expect("Failed to obtain computed style") + // this can't fail: we aren't using a pseudo-element + .expect("Invalid pseudo-element"); + + let document = self.window.document().expect("Failed to obtain document"); + + if !document.contains(Some(&self.canvas)) + || style.get_property_value("display").unwrap() == "none" + { + let size = PhysicalSize::new(0, 0); + + if self.notify_scale.replace(false) { + let scale = backend::scale_factor(&self.window); + (self.scale_handler)(size, scale) + } else { + (self.resize_handler)(size) + } + + return; + } + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + self.observer.unobserve(&self.canvas); + self.observer.observe(&self.canvas); + + return; + } + + let mut size = LogicalSize::new( + backend::style_size_property(&style, "width"), + backend::style_size_property(&style, "height"), + ); + + if style.get_property_value("box-sizing").unwrap() == "border-box" { + size.width -= backend::style_size_property(&style, "border-left-width") + + backend::style_size_property(&style, "border-right-width") + + backend::style_size_property(&style, "padding-left") + + backend::style_size_property(&style, "padding-right"); + size.height -= backend::style_size_property(&style, "border-top-width") + + backend::style_size_property(&style, "border-bottom-width") + + backend::style_size_property(&style, "padding-top") + + backend::style_size_property(&style, "padding-bottom"); + } + + let size = size.to_physical(backend::scale_factor(&self.window)); + + if self.notify_scale.replace(false) { + let scale = backend::scale_factor(&self.window); + (self.scale_handler)(size, scale) + } else { + (self.resize_handler)(size) + } + } + + fn handle_scale(this: Rc>, mql: &MediaQueryList) { + let weak_self = Rc::downgrade(&this); + let mut this = this.borrow_mut(); + let scale = super::scale_factor(&this.window); + + // TODO: confirm/reproduce this problem, see: + // . + // This should never happen, but if it does then apparently the scale factor didn't change. + if mql.matches() { + warn!( + "media query tracking scale factor was triggered without a change:\n\ + Media Query: {}\n\ + Current Scale: {scale}", + mql.media(), + ); + return; + } + + let new_mql = Self::create_mql(&this.window, move |mql| { + if let Some(rc_self) = weak_self.upgrade() { + Self::handle_scale(rc_self, mql); + } + }); + this.mql = new_mql; + + this.notify_scale.set(true); + this.notify(); + } + + fn process_entry( + window: &Window, + canvas: &HtmlCanvasElement, + entries: Array, + ) -> PhysicalSize { + let entry: ResizeObserverEntry = entries.get(0).unchecked_into(); + + // Safari doesn't support `devicePixelContentBoxSize` + if !has_device_pixel_support() { + let rect = entry.content_rect(); + + return LogicalSize::new(rect.width(), rect.height()) + .to_physical(backend::scale_factor(window)); + } + + let entry: ResizeObserverSize = entry + .device_pixel_content_box_size() + .get(0) + .unchecked_into(); + + let style = window + .get_computed_style(canvas) + .expect("Failed to get computed style of canvas") + // this can only be empty if we provided an invalid `pseudoElt` + .expect("`getComputedStyle` can not be empty"); + + let writing_mode = style + .get_property_value("writing-mode") + .expect("`wirting-mode` is a valid CSS property"); + + // means the canvas is not inserted into the DOM + if writing_mode.is_empty() { + debug_assert_eq!(entry.inline_size(), 0.); + debug_assert_eq!(entry.block_size(), 0.); + + return PhysicalSize::new(0, 0); + } + + let horizontal = match writing_mode.as_str() { + _ if writing_mode.starts_with("horizontal") => true, + _ if writing_mode.starts_with("vertical") | writing_mode.starts_with("sideways") => { + false + } + // deprecated values + "lr" | "lr-tb" | "rl" => true, + "tb" | "tb-lr" | "tb-rl" => false, + _ => { + warn!("unrecognized `writing-mode`, assuming horizontal"); + true + } + }; + + if horizontal { + PhysicalSize::new(entry.inline_size() as u32, entry.block_size() as u32) + } else { + PhysicalSize::new(entry.block_size() as u32, entry.inline_size() as u32) + } + } +} + +impl Drop for ResizeScaleInternal { + fn drop(&mut self) { + self.observer.disconnect(); + } +} + +// TODO: Remove when Safari supports `devicePixelContentBoxSize`. +// See . +pub fn has_device_pixel_support() -> bool { + thread_local! { + static DEVICE_PIXEL_SUPPORT: Lazy = Lazy::new(|| { + #[wasm_bindgen] + extern "C" { + type ResizeObserverEntryExt; + + #[wasm_bindgen(js_class = ResizeObserverEntry, static_method_of = ResizeObserverEntryExt, getter)] + fn prototype() -> Object; + } + + let prototype = ResizeObserverEntryExt::prototype(); + let descriptor = Object::get_own_property_descriptor( + &prototype, + &JsValue::from_str("devicePixelContentBoxSize"), + ); + !descriptor.is_undefined() + }); + } + + DEVICE_PIXEL_SUPPORT.with(|support| **support) +} diff --git a/src/platform_impl/web/web_sys/scaling.rs b/src/platform_impl/web/web_sys/scaling.rs deleted file mode 100644 index 1c13e1b0..00000000 --- a/src/platform_impl/web/web_sys/scaling.rs +++ /dev/null @@ -1,102 +0,0 @@ -use web_sys::MediaQueryList; - -use super::super::ScaleChangeArgs; -use super::media_query_handle::MediaQueryListHandle; - -use std::{cell::RefCell, rc::Rc}; - -pub struct ScaleChangeDetector(Rc>); - -impl ScaleChangeDetector { - pub(crate) fn new(window: web_sys::Window, handler: F) -> Self - where - F: 'static + FnMut(ScaleChangeArgs), - { - Self(ScaleChangeDetectorInternal::new(window, handler)) - } -} - -/// This is a helper type to help manage the `MediaQueryList` used for detecting -/// changes of the `devicePixelRatio`. -struct ScaleChangeDetectorInternal { - window: web_sys::Window, - callback: Box, - mql: MediaQueryListHandle, - last_scale: f64, -} - -impl ScaleChangeDetectorInternal { - fn new(window: web_sys::Window, handler: F) -> Rc> - where - F: 'static + FnMut(ScaleChangeArgs), - { - let current_scale = super::scale_factor(&window); - Rc::new_cyclic(|weak_self| { - let weak_self = weak_self.clone(); - let mql = Self::create_mql(&window, move |mql| { - if let Some(rc_self) = weak_self.upgrade() { - Self::handler(rc_self, mql); - } - }); - - RefCell::new(Self { - window, - callback: Box::new(handler), - mql, - last_scale: current_scale, - }) - }) - } - - fn create_mql(window: &web_sys::Window, closure: F) -> MediaQueryListHandle - where - F: 'static + FnMut(&MediaQueryList), - { - let current_scale = super::scale_factor(window); - // TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16. - let media_query = format!( - "(resolution: {current_scale}dppx), - (-webkit-device-pixel-ratio: {current_scale})", - ); - let mql = MediaQueryListHandle::new(window, &media_query, closure); - assert!( - mql.mql().matches(), - "created media query doesn't match, {current_scale} != {}", - super::scale_factor(window) - ); - mql - } - - fn handler(this: Rc>, mql: &MediaQueryList) { - let weak_self = Rc::downgrade(&this); - let mut this = this.borrow_mut(); - let old_scale = this.last_scale; - let new_scale = super::scale_factor(&this.window); - - // TODO: confirm/reproduce this problem, see: - // . - // This should never happen, but if it does then apparently the scale factor didn't change. - if mql.matches() { - warn!( - "media query tracking scale factor was triggered without a change:\n\ - Media Query: {}\n\ - Current Scale: {new_scale}", - mql.media(), - ); - return; - } - - (this.callback)(ScaleChangeArgs { - old_scale, - new_scale, - }); - - let new_mql = Self::create_mql(&this.window, move |mql| { - if let Some(rc_self) = weak_self.upgrade() { - Self::handler(rc_self, mql); - } - }); - this.mql = new_mql; - this.last_scale = new_scale; - } -} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 9b1caefb..2557054a 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -1,6 +1,5 @@ use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; -use crate::event; use crate::icon::Icon; use crate::window::{ CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, @@ -31,7 +30,6 @@ pub struct Inner { canvas: Rc>, previous_pointer: RefCell<&'static str>, register_redraw_request: Box, - resize_notify_fn: Box)>, destroy_fn: Option>, } @@ -48,7 +46,7 @@ impl Window { let prevent_default = platform_attr.prevent_default; let window = target.runner.window(); - let canvas = backend::Canvas::create(window.clone(), &attr, platform_attr)?; + let canvas = backend::Canvas::create(id, window.clone(), &attr, platform_attr)?; let canvas = Rc::new(RefCell::new(canvas)); let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); @@ -56,14 +54,6 @@ impl Window { let has_focus = Arc::new(AtomicBool::new(false)); target.register(&canvas, id, prevent_default, has_focus.clone()); - let runner = target.runner.clone(); - let resize_notify_fn = Box::new(move |new_size| { - runner.send_event(event::Event::WindowEvent { - window_id: RootWI(id), - event: event::WindowEvent::Resized(new_size), - }); - }); - let runner = target.runner.clone(); let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); @@ -75,7 +65,6 @@ impl Window { canvas, previous_pointer: RefCell::new("auto"), register_redraw_request, - resize_notify_fn, destroy_fn: Some(destroy_fn), }) .unwrap(), @@ -149,7 +138,7 @@ impl Window { #[inline] pub fn inner_size(&self) -> PhysicalSize { - self.inner.queue(|inner| inner.inner_size()) + self.inner.queue(|inner| inner.canvas.borrow().inner_size()) } #[inline] @@ -161,12 +150,9 @@ impl Window { #[inline] pub fn set_inner_size(&self, size: Size) { self.inner.dispatch(move |inner| { - let old_size = inner.inner_size(); - backend::set_canvas_size(&inner.canvas.borrow(), size); - let new_size = inner.inner_size(); - if old_size != new_size { - (inner.resize_notify_fn)(new_size); - } + let size = size.to_logical(inner.scale_factor()); + let canvas = inner.canvas.borrow(); + backend::set_canvas_size(canvas.window(), canvas.raw(), size); }); } @@ -442,11 +428,6 @@ impl Inner { pub fn scale_factor(&self) -> f64 { super::backend::scale_factor(&self.window) } - - #[inline] - pub fn inner_size(&self) -> PhysicalSize { - self.canvas.borrow().size().get() - } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]