Implement ResizeObserver (#2859)

Co-authored-by: Liam Murphy <43807659+Liamolucko@users.noreply.github.com>
This commit is contained in:
daxpedda 2023-06-14 09:43:53 +02:00 committed by GitHub
parent 7ce86c3d2a
commit 9a9c9b15ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 584 additions and 364 deletions

View file

@ -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

View file

@ -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'
]

View file

@ -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)));
}
}
_ => (),
}
});

View file

@ -1,5 +1,5 @@
mod proxy;
mod runner;
pub(crate) mod runner;
mod state;
mod window_target;

View file

@ -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<T> Clone for Shared<T> {
pub struct Execution<T: 'static> {
runner: RefCell<RunnerEnum<T>>,
events: RefCell<VecDeque<Event<'static, T>>>,
events: RefCell<VecDeque<EventWrapper<T>>>,
id: RefCell<u32>,
window: web_sys::Window,
all_canvases: RefCell<Vec<(WindowId, Weak<RefCell<backend::Canvas>>)>>,
redraw_pending: RefCell<HashSet<WindowId>>,
destroy_pending: RefCell<VecDeque<WindowId>>,
scale_change_detector: RefCell<Option<backend::ScaleChangeDetector>>,
unload_event_handle: RefCell<Option<backend::UnloadEventHandle>>,
}
@ -86,10 +86,31 @@ impl<T: 'static> Runner<T> {
})
}
fn handle_single_event(&mut self, event: Event<'_, T>, control: &mut ControlFlow) {
fn handle_single_event(
&mut self,
runner: &Shared<T>,
event: impl Into<EventWrapper<T>>,
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<T: 'static> Shared<T> {
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<T: 'static> Shared<T> {
}));
}
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(
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<T: 'static> Shared<T> {
pub fn request_redraw(&self, id: WindowId) {
self.0.redraw_pending.borrow_mut().insert(id);
self.send_events(iter::empty());
self.send_events::<EventWrapper<T>>(iter::empty());
}
pub fn init(&self) {
@ -196,14 +206,17 @@ impl<T: 'static> Shared<T> {
// 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<E: Into<EventWrapper<T>>>(&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<Item = Event<'static, T>>) {
pub(crate) fn send_events<E: Into<EventWrapper<T>>>(
&self,
events: impl IntoIterator<Item = E>,
) {
// If the event loop is closed, it should discard any new events
if self.is_closed() {
return;
@ -232,7 +245,10 @@ impl<T: 'static> Shared<T> {
}
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<T: 'static> Shared<T> {
// 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<T: 'static> Shared<T> {
// 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<Item = Event<'static, T>>) {
fn run_until_cleared<E: Into<EventWrapper<T>>>(&self, events: impl Iterator<Item = E>) {
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<T: 'static> Shared<T> {
}
}
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::<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:
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<T: 'static> Shared<T> {
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<EventWrapper<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);
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<T: 'static> Shared<T> {
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<T: 'static> Shared<T> {
}
}
}
pub(crate) enum EventWrapper<T: 'static> {
Event(Event<'static, T>),
ScaleChange {
canvas: Weak<RefCell<backend::Canvas>>,
size: PhysicalSize<u32>,
scale: f64,
},
}
impl<T> From<Event<'static, T>> for EventWrapper<T> {
fn from(value: Event<'static, T>) -> Self {
Self::Event(value)
}
}

View file

@ -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<T> EventLoopWindowTarget<T> {
pub fn run(&self, event_handler: Box<runner::EventHandler<T>>) {
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<T> EventLoopWindowTarget<T> {
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<T> EventLoopWindowTarget<T> {
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<MonitorHandle> {

View file

@ -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,
}

View file

@ -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<EventListenerHandle<dyn FnMut(Event)>>,
on_touch_end: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_focus: Option<EventListenerHandle<dyn FnMut(FocusEvent)>>,
@ -29,21 +31,23 @@ pub struct Canvas {
on_keyboard_release: Option<EventListenerHandle<dyn FnMut(KeyboardEvent)>>,
on_keyboard_press: Option<EventListenerHandle<dyn FnMut(KeyboardEvent)>>,
on_mouse_wheel: Option<EventListenerHandle<dyn FnMut(WheelEvent)>>,
on_fullscreen_change: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_dark_mode: Option<MediaQueryListHandle>,
pointer_handler: PointerHandler,
on_resize_scale: Option<ResizeScaleHandle>,
}
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<Cell<PhysicalSize<u32>>>,
old_size: Rc<Cell<PhysicalSize<u32>>>,
current_size: Rc<Cell<PhysicalSize<u32>>>,
wants_fullscreen: Rc<RefCell<bool>>,
}
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<u32> {
self.common.old_size.get()
}
pub fn size(&self) -> &Rc<Cell<PhysicalSize<u32>>> {
&self.common.size
pub fn inner_size(&self) -> PhysicalSize<u32> {
self.common.current_size.get()
}
pub fn set_old_size(&self, size: PhysicalSize<u32>) {
self.common.old_size.set(size)
}
pub fn set_current_size(&self, size: PhysicalSize<u32>) {
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<F>(&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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(bool),
@ -350,6 +348,19 @@ impl Canvas {
));
}
pub(crate) fn on_resize_scale<S, R>(&mut self, scale_handler: S, size_handler: R)
where
S: 'static + FnMut(PhysicalSize<u32>, f64),
R: 'static + FnMut(PhysicalSize<u32>),
{
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<T: 'static>(
&self,
runner: &super::super::event_loop::runner::Shared<T>,
event_handler: impl FnOnce(crate::event::Event<'_, T>),
current_size: PhysicalSize<u32>,
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;
}
}

View file

@ -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<f64> {
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<f64>,
) {
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::<f64>(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) {

View file

@ -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<RefCell<ResizeScaleInternal>>);
impl ResizeScaleHandle {
pub(crate) fn new<S, R>(
window: Window,
canvas: HtmlCanvasElement,
scale_handler: S,
resize_handler: R,
) -> Self
where
S: 'static + FnMut(PhysicalSize<u32>, f64),
R: 'static + FnMut(PhysicalSize<u32>),
{
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<dyn FnMut(Array, ResizeObserver)>,
scale_handler: Box<dyn FnMut(PhysicalSize<u32>, f64)>,
resize_handler: Box<dyn FnMut(PhysicalSize<u32>)>,
notify_scale: Cell<bool>,
}
impl ResizeScaleInternal {
fn new<S, R>(
window: Window,
canvas: HtmlCanvasElement,
scale_handler: S,
resize_handler: R,
) -> Rc<RefCell<Self>>
where
S: 'static + FnMut(PhysicalSize<u32>, f64),
R: 'static + FnMut(PhysicalSize<u32>),
{
Rc::<RefCell<ResizeScaleInternal>>::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<F>(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<RefCell<Self>>, 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:
// <https://github.com/rust-windowing/winit/issues/2597>.
// 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<u32> {
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 <https://bugs.webkit.org/show_bug.cgi?id=219005>.
pub fn has_device_pixel_support() -> bool {
thread_local! {
static DEVICE_PIXEL_SUPPORT: Lazy<bool> = 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)
}

View file

@ -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<RefCell<ScaleChangeDetectorInternal>>);
impl ScaleChangeDetector {
pub(crate) fn new<F>(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<dyn FnMut(ScaleChangeArgs)>,
mql: MediaQueryListHandle,
last_scale: f64,
}
impl ScaleChangeDetectorInternal {
fn new<F>(window: web_sys::Window, handler: F) -> Rc<RefCell<Self>>
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<F>(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<RefCell<Self>>, 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:
// <https://github.com/rust-windowing/winit/issues/2597>.
// 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;
}
}

View file

@ -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<RefCell<backend::Canvas>>,
previous_pointer: RefCell<&'static str>,
register_redraw_request: Box<dyn Fn()>,
resize_notify_fn: Box<dyn Fn(PhysicalSize<u32>)>,
destroy_fn: Option<Box<dyn FnOnce()>>,
}
@ -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<u32> {
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<u32> {
self.canvas.borrow().size().get()
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]