diff --git a/CHANGELOG.md b/CHANGELOG.md index cc4386ea..2eb312ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ - On macOS, add `NSWindow.hasShadow` support. - On Web, fix vertical mouse wheel scrolling being inverted. - On Web, implement mouse capturing for click-dragging out of the canvas. +- On Web, fix `ControlFlow::Exit` not properly handled. +- On Web (web-sys only), send `WindowEvent::ScaleFactorChanged` event when `window.devicePixelRatio` is changed. - **Breaking:** On Web, `set_cursor_position` and `set_cursor_grab` will now always return an error. - **Breaking:** `PixelDelta` scroll events now return a `PhysicalPosition`. - On NetBSD, fixed crash due to incorrect detection of the main thread. diff --git a/FEATURES.md b/FEATURES.md index d7791f5b..ca28c417 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -182,9 +182,11 @@ Legend: |Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ | |Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ | |Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |✔️ |**N/A**| -|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |**N/A**| +|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |✔️ \*1| |Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**| +\*1: `WindowEvent::ScaleFactorChanged` is not sent on `stdweb` backend. + ### System information |Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- | diff --git a/src/dpi.rs b/src/dpi.rs index 8a56ae6e..ed30abb7 100644 --- a/src/dpi.rs +++ b/src/dpi.rs @@ -89,6 +89,8 @@ //! - **Android:** Scale factors are set by the manufacturer to the value that best suits the //! device, and range from `1.0` to `4.0`. See [this article][android_1] for more information. //! - **Web:** The scale factor is the ratio between CSS pixels and the physical device pixels. +//! In other words, it is the value of [`window.devicePixelRatio`][web_1]. It is affected by +//! both the screen scaling and the browser zoom level and can go below `1.0`. //! //! [points]: https://en.wikipedia.org/wiki/Point_(typography) //! [picas]: https://en.wikipedia.org/wiki/Pica_(typography) @@ -96,6 +98,7 @@ //! [apple_1]: https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html //! [apple_2]: https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/ //! [android_1]: https://developer.android.com/training/multiscreen/screendensities +//! [web_1]: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio pub trait Pixel: Copy + Into { fn from_f64(f: f64) -> Self; diff --git a/src/event_loop.rs b/src/event_loop.rs index a523c01a..02402930 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -72,8 +72,11 @@ impl fmt::Debug for EventLoopWindowTarget { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ControlFlow { /// When the current loop iteration finishes, immediately begin a new iteration regardless of - /// whether or not new events are available to process. For web, events are sent when - /// `requestAnimationFrame` fires. + /// whether or not new events are available to process. + /// + /// For web, events are queued and usually sent when `requestAnimationFrame` fires but sometimes + /// the events in the queue may be sent before the next `requestAnimationFrame` callback, for + /// example when the scaling of the page has changed. Poll, /// When the current loop iteration finishes, suspend the thread until another event arrives. Wait, diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index bf9806bd..24be4d3d 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -28,8 +28,7 @@ impl EventLoop { pub fn run(self, mut event_handler: F) -> ! where - F: 'static - + FnMut(Event<'static, T>, &root::EventLoopWindowTarget, &mut root::ControlFlow), + F: 'static + FnMut(Event<'_, T>, &root::EventLoopWindowTarget, &mut root::ControlFlow), { let target = root::EventLoopWindowTarget { p: self.elw.p.clone(), diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index d381b942..0ccd7862 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,4 +1,4 @@ -use super::{backend, state::State}; +use super::{super::ScaleChangeArgs, backend, state::State}; use crate::event::{Event, StartCause}; use crate::event_loop as root; use crate::window::WindowId; @@ -24,23 +24,60 @@ pub struct Execution { runner: RefCell>>, events: RefCell>>, id: RefCell, + all_canvases: RefCell>, redraw_pending: RefCell>, + scale_change_detector: RefCell>, } struct Runner { state: State, is_busy: bool, - event_handler: Box, &mut root::ControlFlow)>, + event_handler: Box, &mut root::ControlFlow)>, } impl Runner { - pub fn new(event_handler: Box, &mut root::ControlFlow)>) -> Self { + pub fn new(event_handler: Box, &mut root::ControlFlow)>) -> Self { Runner { state: State::Init, is_busy: false, event_handler, } } + + /// Returns the cooresponding `StartCause` for the current `state`, or `None` + /// when in `Exit` state. + fn maybe_start_cause(&self) -> Option { + Some(match self.state { + State::Init => StartCause::Init, + State::Poll { .. } => StartCause::Poll, + State::Wait { start } => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + State::WaitUntil { start, end, .. } => StartCause::WaitCancelled { + start, + requested_resume: Some(end), + }, + State::Exit => return None, + }) + } + + fn handle_single_event(&mut self, event: Event<'_, T>, control: &mut root::ControlFlow) { + let is_closed = *control == root::ControlFlow::Exit; + + // An event is being processed, so the runner should be marked busy + self.is_busy = true; + + (self.event_handler)(event, control); + + // Maintain closed state, even if the callback changes it + if is_closed { + *control = root::ControlFlow::Exit; + } + + // An event is no longer being processed + self.is_busy = false; + } } impl Shared { @@ -49,16 +86,22 @@ impl Shared { runner: RefCell::new(None), events: RefCell::new(VecDeque::new()), id: RefCell::new(0), + all_canvases: RefCell::new(Vec::new()), redraw_pending: RefCell::new(HashSet::new()), + scale_change_detector: RefCell::new(None), })) } + pub fn add_canvas(&self, id: WindowId, canvas: backend::RawCanvasType) { + self.0.all_canvases.borrow_mut().push((id, canvas)); + } + // Set the event callback to use for the event loop runner // This the event callback is a fairly thin layer over the user-provided callback that closes // over a RootEventLoopWindowTarget reference pub fn set_listener( &self, - event_handler: Box, &mut root::ControlFlow)>, + event_handler: Box, &mut root::ControlFlow)>, ) { self.0.runner.replace(Some(Runner::new(event_handler))); self.init(); @@ -67,6 +110,14 @@ impl Shared { backend::on_unload(move || close_instance.handle_unload()); } + 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(handler)); + } + // Generate a strictly increasing ID // This is used to differentiate windows when handling events pub fn generate_id(&self) -> u32 { @@ -138,25 +189,15 @@ impl Shared { } // At this point, we know this is a fresh set of events // Now we determine why new events are incoming, and handle the events - let start_cause = if let Some(runner) = &*self.0.runner.borrow() { - match runner.state { - State::Init => StartCause::Init, - State::Poll { .. } => StartCause::Poll, - State::Wait { start } => StartCause::WaitCancelled { - start, - requested_resume: None, - }, - State::WaitUntil { start, end, .. } => StartCause::WaitCancelled { - start, - requested_resume: Some(end), - }, - State::Exit => { - // If we're in the exit state, don't do event processing - return; - } - } - } else { - unreachable!("The runner cannot process events when it is not attached"); + let start_cause = match (self.0.runner.borrow().as_ref()) + .unwrap_or_else(|| { + unreachable!("The runner cannot process events when it is not attached") + }) + .maybe_start_cause() + { + Some(c) => c, + // If we're in the exit state, don't do event processing + None => return, }; // Take the start event, then the events provided to this function, and run an iteration of // the event loop @@ -191,37 +232,107 @@ impl Shared { } } + pub fn handle_scale_changed(&self, old_scale: f64, new_scale: f64) { + let start_cause = match (self.0.runner.borrow().as_ref()) + .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); + + // Now handle the `ScaleFactorChanged` events. + for &(id, ref canvas) in &*self.0.all_canvases.borrow() { + // First, we send the `ScaleFactorChanged` event: + let current_size = crate::dpi::PhysicalSize { + width: canvas.width() as u32, + height: canvas.height() as u32, + }; + 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: + 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, + ); + } + + 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_event(Event::LoopDestroyed, &mut control); + } + } + fn handle_unload(&self) { self.apply_control_flow(root::ControlFlow::Exit); let mut control = self.current_control_flow(); self.handle_event(Event::LoopDestroyed, &mut control); } - // handle_event takes in events and either queues them or applies a callback + // handle_single_event_sync takes in an event and handles it synchronously. // - // It should only ever be called from send_event - fn handle_event(&self, event: Event<'static, T>, control: &mut root::ControlFlow) { - let is_closed = self.is_closed(); + // It should only ever be called from `scale_changed`. + fn handle_single_event_sync(&self, event: Event<'_, T>, control: &mut root::ControlFlow) { + if self.is_closed() { + *control = root::ControlFlow::Exit; + } match *self.0.runner.borrow_mut() { Some(ref mut runner) => { - // An event is being processed, so the runner should be marked busy - runner.is_busy = true; + runner.handle_single_event(event, control); + } + _ => panic!("Cannot handle event synchronously without a runner"), + } + } - (runner.event_handler)(event, control); - - // Maintain closed state, even if the callback changes it - if is_closed { - *control = root::ControlFlow::Exit; - } - - // An event is no longer being processed - runner.is_busy = false; + // 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 root::ControlFlow) { + if self.is_closed() { + *control = root::ControlFlow::Exit; + } + match *self.0.runner.borrow_mut() { + Some(ref mut runner) => { + runner.handle_single_event(event, control); } // If an event is being handled without a runner somehow, add it to the event queue so // it will eventually be processed _ => self.0.events.borrow_mut().push_back(event), } + let is_closed = *control == root::ControlFlow::Exit; + // Don't take events out of the queue if the loop is closed or the runner doesn't exist // If the runner doesn't exist and this method recurses, it will recurse infinitely if !is_closed && self.0.runner.borrow().is_some() { diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 03fe2110..0815bd40 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -29,8 +29,12 @@ impl WindowTarget { Proxy::new(self.runner.clone()) } - pub fn run(&self, event_handler: Box, &mut ControlFlow)>) { + pub fn run(&self, event_handler: Box, &mut ControlFlow)>) { 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) -> window::Id { @@ -40,6 +44,7 @@ impl WindowTarget { pub fn register(&self, canvas: &mut backend::Canvas, id: window::Id) { let runner = self.runner.clone(); canvas.set_attribute("data-raw-handle", &id.0.to_string()); + runner.add_canvas(WindowId(id), canvas.raw().clone()); canvas.on_blur(move || { runner.send_event(Event::WindowEvent { diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 43bbb2af..ed170b83 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -46,3 +46,9 @@ pub use self::window::{ }; pub(crate) use crate::icon::NoIcon as PlatformIcon; + +#[derive(Clone, Copy)] +pub(crate) struct ScaleChangeArgs { + old_scale: f64, + new_scale: f64, +} diff --git a/src/platform_impl/web/stdweb/mod.rs b/src/platform_impl/web/stdweb/mod.rs index e3dafb1b..2f38c178 100644 --- a/src/platform_impl/web/stdweb/mod.rs +++ b/src/platform_impl/web/stdweb/mod.rs @@ -1,8 +1,10 @@ mod canvas; mod event; +mod scaling; mod timeout; pub use self::canvas::Canvas; +pub use self::scaling::ScaleChangeDetector; pub use self::timeout::{AnimationFrameRequest, Timeout}; use crate::dpi::{LogicalSize, Size}; diff --git a/src/platform_impl/web/stdweb/scaling.rs b/src/platform_impl/web/stdweb/scaling.rs new file mode 100644 index 00000000..28024735 --- /dev/null +++ b/src/platform_impl/web/stdweb/scaling.rs @@ -0,0 +1,13 @@ +use super::super::ScaleChangeArgs; + +pub struct ScaleChangeDetector(()); + +impl ScaleChangeDetector { + pub(crate) fn new(_handler: F) -> Self + where + F: 'static + FnMut(ScaleChangeArgs), + { + // TODO: Stub, unimplemented (see web_sys for reference). + Self(()) + } +} diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index a4268b52..338869ac 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -1,8 +1,10 @@ mod canvas; mod event; +mod scaling; mod timeout; pub use self::canvas::Canvas; +pub use self::scaling::ScaleChangeDetector; pub use self::timeout::{AnimationFrameRequest, Timeout}; use crate::dpi::{LogicalSize, Size}; diff --git a/src/platform_impl/web/web_sys/scaling.rs b/src/platform_impl/web/web_sys/scaling.rs new file mode 100644 index 00000000..5018257f --- /dev/null +++ b/src/platform_impl/web/web_sys/scaling.rs @@ -0,0 +1,110 @@ +use super::super::ScaleChangeArgs; + +use std::{cell::RefCell, rc::Rc}; +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::{MediaQueryList, MediaQueryListEvent}; + +pub struct ScaleChangeDetector(Rc>); + +impl ScaleChangeDetector { + pub(crate) fn new(handler: F) -> Self + where + F: 'static + FnMut(ScaleChangeArgs), + { + Self(ScaleChangeDetectorInternal::new(handler)) + } +} + +/// This is a helper type to help manage the `MediaQueryList` used for detecting +/// changes of the `devicePixelRatio`. +struct ScaleChangeDetectorInternal { + callback: Box, + closure: Option>, + mql: Option, + last_scale: f64, +} + +impl ScaleChangeDetectorInternal { + fn new(handler: F) -> Rc> + where + F: 'static + FnMut(ScaleChangeArgs), + { + let current_scale = super::scale_factor(); + let new_self = Rc::new(RefCell::new(Self { + callback: Box::new(handler), + closure: None, + mql: None, + last_scale: current_scale, + })); + + let cloned_self = new_self.clone(); + let closure = Closure::wrap(Box::new(move |event: MediaQueryListEvent| { + cloned_self.borrow_mut().handler(event) + }) as Box); + + let mql = Self::create_mql(&closure); + { + let mut borrowed_self = new_self.borrow_mut(); + borrowed_self.closure = Some(closure); + borrowed_self.mql = mql; + } + new_self + } + + fn create_mql(closure: &Closure) -> Option { + let window = web_sys::window().expect("Failed to obtain window"); + let current_scale = super::scale_factor(); + // This media query initially matches the current `devicePixelRatio`. + // We add 0.0001 to the lower and upper bounds such that it won't fail + // due to floating point precision limitations. + let media_query = format!( + "(min-resolution: {:.4}dppx) and (max-resolution: {:.4}dppx)", + current_scale - 0.0001, + current_scale + 0.0001, + ); + window + .match_media(&media_query) + .ok() + .flatten() + .and_then(|mql| { + assert_eq!(mql.matches(), true); + mql.add_listener_with_opt_callback(Some(&closure.as_ref().unchecked_ref())) + .map(|_| mql) + .ok() + }) + } + + fn handler(&mut self, event: MediaQueryListEvent) { + assert_eq!(event.matches(), false); + let closure = self + .closure + .as_ref() + .expect("DevicePixelRatioChangeDetector::closure should not be None"); + let mql = self + .mql + .take() + .expect("DevicePixelRatioChangeDetector::mql should not be None"); + mql.remove_listener_with_opt_callback(Some(closure.as_ref().unchecked_ref())) + .expect("Failed to remove listener from MediaQueryList"); + let new_scale = super::scale_factor(); + (self.callback)(ScaleChangeArgs { + old_scale: self.last_scale, + new_scale, + }); + let new_mql = Self::create_mql(closure); + self.mql = new_mql; + self.last_scale = new_scale; + } +} + +impl Drop for ScaleChangeDetectorInternal { + fn drop(&mut self) { + match (self.closure.as_ref(), self.mql.as_ref()) { + (Some(closure), Some(mql)) => { + let _ = + mql.remove_listener_with_opt_callback(Some(closure.as_ref().unchecked_ref())); + } + _ => {} + } + } +}