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

Handle scale factor change on web-sys backend #1690

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- |
Expand Down
3 changes: 3 additions & 0 deletions src/dpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,16 @@
//! - **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)
//! [windows_1]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
//! [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<f64> {
fn from_f64(f: f64) -> Self;
Expand Down
7 changes: 5 additions & 2 deletions src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ impl<T> fmt::Debug for EventLoopWindowTarget<T> {
#[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,
Expand Down
3 changes: 1 addition & 2 deletions src/platform_impl/web/event_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ impl<T> EventLoop<T> {

pub fn run<F>(self, mut event_handler: F) -> !
where
F: 'static
+ FnMut(Event<'static, T>, &root::EventLoopWindowTarget<T>, &mut root::ControlFlow),
F: 'static + FnMut(Event<'_, T>, &root::EventLoopWindowTarget<T>, &mut root::ControlFlow),
{
let target = root::EventLoopWindowTarget {
p: self.elw.p.clone(),
Expand Down
185 changes: 148 additions & 37 deletions src/platform_impl/web/event_loop/runner.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,23 +24,60 @@ pub struct Execution<T: 'static> {
runner: RefCell<Option<Runner<T>>>,
events: RefCell<VecDeque<Event<'static, T>>>,
id: RefCell<u32>,
all_canvases: RefCell<Vec<(WindowId, backend::RawCanvasType)>>,
redraw_pending: RefCell<HashSet<WindowId>>,
scale_change_detector: RefCell<Option<backend::ScaleChangeDetector>>,
}

struct Runner<T: 'static> {
state: State,
is_busy: bool,
event_handler: Box<dyn FnMut(Event<'static, T>, &mut root::ControlFlow)>,
event_handler: Box<dyn FnMut(Event<'_, T>, &mut root::ControlFlow)>,
}

impl<T: 'static> Runner<T> {
pub fn new(event_handler: Box<dyn FnMut(Event<'static, T>, &mut root::ControlFlow)>) -> Self {
pub fn new(event_handler: Box<dyn FnMut(Event<'_, T>, &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<StartCause> {
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<T: 'static> Shared<T> {
Expand All @@ -49,16 +86,22 @@ impl<T: 'static> Shared<T> {
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<dyn FnMut(Event<'static, T>, &mut root::ControlFlow)>,
event_handler: Box<dyn FnMut(Event<'_, T>, &mut root::ControlFlow)>,
) {
self.0.runner.replace(Some(Runner::new(event_handler)));
self.init();
Expand All @@ -67,6 +110,14 @@ impl<T: 'static> Shared<T> {
backend::on_unload(move || close_instance.handle_unload());
}

pub(crate) fn set_on_scale_change<F>(&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 {
Expand Down Expand Up @@ -138,25 +189,15 @@ impl<T: 'static> Shared<T> {
}
// 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
Expand Down Expand Up @@ -191,37 +232,107 @@ impl<T: 'static> Shared<T> {
}
}

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::<f64>(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_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 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);
}
_ => 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 send_event
// 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) {
let is_closed = self.is_closed();
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.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;
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() {
Expand Down
7 changes: 6 additions & 1 deletion src/platform_impl/web/event_loop/window_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ impl<T> WindowTarget<T> {
Proxy::new(self.runner.clone())
}

pub fn run(&self, event_handler: Box<dyn FnMut(Event<'static, T>, &mut ControlFlow)>) {
pub fn run(&self, event_handler: Box<dyn FnMut(Event<'_, T>, &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 {
Expand All @@ -40,6 +44,7 @@ impl<T> WindowTarget<T> {
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 {
Expand Down
6 changes: 6 additions & 0 deletions src/platform_impl/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
2 changes: 2 additions & 0 deletions src/platform_impl/web/stdweb/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
13 changes: 13 additions & 0 deletions src/platform_impl/web/stdweb/scaling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use super::super::ScaleChangeArgs;

pub struct ScaleChangeDetector(());

impl ScaleChangeDetector {
pub(crate) fn new<F>(_handler: F) -> Self
where
F: 'static + FnMut(ScaleChangeArgs),
{
// TODO: Stub, unimplemented (see web_sys for reference).
Self(())
}
}
2 changes: 2 additions & 0 deletions src/platform_impl/web/web_sys/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
Loading