From 777d9edeaa9d1648c07e2b48d4019791adfc0fc3 Mon Sep 17 00:00:00 2001 From: Michael Tang Date: Tue, 31 Dec 2019 14:39:33 -0800 Subject: [PATCH] Implement hidpi for web platform (#1233) * fix: use a 'static lifetime for the web backend's `Event` types * implement hidpi for stdweb (web-sys wip?) * fix: make all canvas resizes go through backend::set_canvas_size * update Window docs for web, make `inner/outer_position` return the position in the viewport --- Cargo.toml | 1 + src/platform_impl/web/event_loop/mod.rs | 3 +- src/platform_impl/web/event_loop/runner.rs | 21 ++++--- .../web/event_loop/window_target.rs | 24 ++++---- src/platform_impl/web/monitor.rs | 6 +- src/platform_impl/web/stdweb/canvas.rs | 31 +++++------ src/platform_impl/web/stdweb/mod.rs | 24 +++++++- src/platform_impl/web/web_sys/canvas.rs | 30 +++++----- src/platform_impl/web/web_sys/mod.rs | 25 ++++++++- src/platform_impl/web/window.rs | 55 ++++++++----------- src/window.rs | 8 +++ 11 files changed, 137 insertions(+), 91 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2f5c9a1..5e198f5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ version = "0.3.22" optional = true features = [ 'console', + 'CssStyleDeclaration', 'BeforeUnloadEvent', 'Document', 'DomRect', diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 7124cee7..50640a1f 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -37,7 +37,8 @@ impl EventLoop { pub fn run(self, mut event_handler: F) -> ! where - F: 'static + FnMut(Event, &root::EventLoopWindowTarget, &mut root::ControlFlow), + F: 'static + + FnMut(Event<'static, 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 da7d5bad..b480054c 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -11,7 +11,7 @@ use std::{ rc::Rc, }; -pub struct Shared(Rc>); +pub struct Shared(Rc>); impl Clone for Shared { fn clone(&self) -> Self { @@ -19,21 +19,21 @@ impl Clone for Shared { } } -pub struct Execution { +pub struct Execution { runner: RefCell>>, - events: RefCell>>, + events: RefCell>>, id: RefCell, redraw_pending: RefCell>, } -struct Runner { +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, @@ -55,7 +55,10 @@ impl Shared { // 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)>) { + pub fn set_listener( + &self, + event_handler: Box, &mut root::ControlFlow)>, + ) { self.0.runner.replace(Some(Runner::new(event_handler))); self.send_event(Event::NewEvents(StartCause::Init)); @@ -79,7 +82,7 @@ impl Shared { // Add an event 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_event(&self, event: Event) { + pub fn send_event(&self, event: Event<'static, T>) { // If the event loop is closed, it should discard any new events if self.is_closed() { return; @@ -153,7 +156,7 @@ impl Shared { // handle_event takes in events and either queues them or applies a callback // // It should only ever be called from send_event - fn handle_event(&self, event: Event, control: &mut root::ControlFlow) { + fn handle_event(&self, event: Event<'static, T>, control: &mut root::ControlFlow) { let is_closed = self.is_closed(); match *self.0.runner.borrow_mut() { diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 2e0f32db..abe7b8e3 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -1,5 +1,5 @@ use super::{backend, device, proxy::Proxy, runner, window}; -use crate::dpi::LogicalSize; +use crate::dpi::{PhysicalSize, Size}; use crate::event::{DeviceId, ElementState, Event, KeyboardInput, TouchPhase, WindowEvent}; use crate::event_loop::ControlFlow; use crate::window::WindowId; @@ -28,7 +28,7 @@ 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); } @@ -170,25 +170,27 @@ impl WindowTarget { let runner = self.runner.clone(); let raw = canvas.raw().clone(); - let mut intended_size = LogicalSize { - width: raw.width() as f64, - height: raw.height() as f64, + + // The size to restore to after exiting fullscreen. + let mut intended_size = PhysicalSize { + width: raw.width() as u32, + height: raw.height() as u32, }; canvas.on_fullscreen_change(move || { // 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(&raw) { - intended_size = LogicalSize { - width: raw.width() as f64, - height: raw.height() as f64, + intended_size = PhysicalSize { + width: raw.width() as u32, + height: raw.height() as u32, }; - backend::window_size() + backend::window_size().to_physical(backend::hidpi_factor()) } else { intended_size }; - raw.set_width(new_size.width as u32); - raw.set_height(new_size.height as u32); + + backend::set_canvas_size(&raw, Size::Physical(new_size)); runner.send_event(Event::WindowEvent { window_id: WindowId(id), event: WindowEvent::Resized(new_size), diff --git a/src/platform_impl/web/monitor.rs b/src/platform_impl/web/monitor.rs index 9442d3b8..ac06d0cb 100644 --- a/src/platform_impl/web/monitor.rs +++ b/src/platform_impl/web/monitor.rs @@ -10,7 +10,7 @@ impl Handle { } pub fn position(&self) -> PhysicalPosition { - PhysicalPosition { x: 0.0, y: 0.0 } + PhysicalPosition { x: 0, y: 0 } } pub fn name(&self) -> Option { @@ -19,8 +19,8 @@ impl Handle { pub fn size(&self) -> PhysicalSize { PhysicalSize { - width: 0.0, - height: 0.0, + width: 0, + height: 0, } } diff --git a/src/platform_impl/web/stdweb/canvas.rs b/src/platform_impl/web/stdweb/canvas.rs index 07ba42b5..99778aa6 100644 --- a/src/platform_impl/web/stdweb/canvas.rs +++ b/src/platform_impl/web/stdweb/canvas.rs @@ -1,5 +1,5 @@ use super::event; -use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; use crate::platform_impl::OsError; @@ -19,6 +19,7 @@ use stdweb::web::{ }; pub struct Canvas { + /// Note: resizing the CanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained. raw: CanvasElement, on_focus: Option, on_blur: Option, @@ -82,23 +83,20 @@ impl Canvas { .expect(&format!("Set attribute: {}", attribute)); } - pub fn position(&self) -> (f64, f64) { + pub fn position(&self) -> LogicalPosition { let bounds = self.raw.get_bounding_client_rect(); - (bounds.get_x(), bounds.get_y()) + LogicalPosition { + x: bounds.get_x(), + y: bounds.get_y(), + } } - pub fn width(&self) -> f64 { - self.raw.width() as f64 - } - - pub fn height(&self) -> f64 { - self.raw.height() as f64 - } - - pub fn set_size(&self, size: LogicalSize) { - self.raw.set_width(size.width as u32); - self.raw.set_height(size.height as u32); + pub fn size(&self) -> PhysicalSize { + PhysicalSize { + width: self.raw.width() as u32, + height: self.raw.height() as u32, + } } pub fn raw(&self) -> &CanvasElement { @@ -209,12 +207,13 @@ impl Canvas { pub fn on_cursor_move(&mut self, mut handler: F) where - F: 'static + FnMut(i32, LogicalPosition, ModifiersState), + F: 'static + FnMut(i32, PhysicalPosition, ModifiersState), { + // todo self.on_cursor_move = Some(self.add_event(move |event: PointerMoveEvent| { handler( event.pointer_id(), - event::mouse_position(&event), + event::mouse_position(&event).to_physical(super::hidpi_factor()), event::mouse_modifiers(&event), ); })); diff --git a/src/platform_impl/web/stdweb/mod.rs b/src/platform_impl/web/stdweb/mod.rs index 0ac4b172..b87a1b9f 100644 --- a/src/platform_impl/web/stdweb/mod.rs +++ b/src/platform_impl/web/stdweb/mod.rs @@ -5,7 +5,7 @@ mod timeout; pub use self::canvas::Canvas; pub use self::timeout::Timeout; -use crate::dpi::LogicalSize; +use crate::dpi::{LogicalSize, Size}; use crate::platform::web::WindowExtStdweb; use crate::window::Window; @@ -41,6 +41,28 @@ pub fn window_size() -> LogicalSize { LogicalSize { width, height } } +pub fn hidpi_factor() -> f64 { + let window = window(); + window.device_pixel_ratio() +} + +pub fn set_canvas_size(raw: &CanvasElement, size: Size) { + use stdweb::*; + + let hidpi_factor = hidpi_factor(); + + let physical_size = size.to_physical::(hidpi_factor); + let logical_size = size.to_logical::(hidpi_factor); + + raw.set_width(physical_size.width); + raw.set_height(physical_size.height); + + js! { + @{raw.as_ref()}.style.width = @{logical_size.width} + "px"; + @{raw.as_ref()}.style.height = @{logical_size.height} + "px"; + } +} + pub fn is_fullscreen(canvas: &CanvasElement) -> bool { match document().fullscreen_element() { Some(elem) => { diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index c21b8cbd..7d8d292a 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -1,5 +1,5 @@ use super::event; -use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; use crate::platform_impl::OsError; @@ -11,6 +11,7 @@ use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, PointerEvent, WheelEvent}; pub struct Canvas { + /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained. raw: HtmlCanvasElement, on_focus: Option>, on_blur: Option>, @@ -80,23 +81,20 @@ impl Canvas { .expect(&format!("Set attribute: {}", attribute)); } - pub fn position(&self) -> (f64, f64) { + pub fn position(&self) -> LogicalPosition { let bounds = self.raw.get_bounding_client_rect(); - (bounds.x(), bounds.y()) + LogicalPosition { + x: bounds.x(), + y: bounds.y(), + } } - pub fn width(&self) -> f64 { - self.raw.width() as f64 - } - - pub fn height(&self) -> f64 { - self.raw.height() as f64 - } - - pub fn set_size(&self, size: LogicalSize) { - self.raw.set_width(size.width as u32); - self.raw.set_height(size.height as u32); + pub fn size(&self) -> PhysicalSize { + PhysicalSize { + width: self.raw.width(), + height: self.raw.height(), + } } pub fn raw(&self) -> &HtmlCanvasElement { @@ -218,12 +216,12 @@ impl Canvas { pub fn on_cursor_move(&mut self, mut handler: F) where - F: 'static + FnMut(i32, LogicalPosition, ModifiersState), + F: 'static + FnMut(i32, PhysicalPosition, ModifiersState), { self.on_cursor_move = Some(self.add_event("pointermove", move |event: PointerEvent| { handler( event.pointer_id(), - event::mouse_position(&event), + event::mouse_position(&event).to_physical(super::hidpi_factor()), event::mouse_modifiers(&event), ); })); diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 4424a03b..bde099a7 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -5,7 +5,7 @@ mod timeout; pub use self::canvas::Canvas; pub use self::timeout::Timeout; -use crate::dpi::LogicalSize; +use crate::dpi::{LogicalSize, Size}; use crate::platform::web::WindowExtWebSys; use crate::window::Window; use wasm_bindgen::{closure::Closure, JsCast}; @@ -56,6 +56,29 @@ pub fn window_size() -> LogicalSize { LogicalSize { width, height } } +pub fn hidpi_factor() -> f64 { + let window = web_sys::window().expect("Failed to obtain window"); + window.device_pixel_ratio() +} + +pub fn set_canvas_size(raw: &HtmlCanvasElement, size: Size) { + let hidpi_factor = hidpi_factor(); + + let physical_size = size.to_physical::(hidpi_factor); + let logical_size = size.to_logical::(hidpi_factor); + + raw.set_width(physical_size.width); + raw.set_height(physical_size.height); + + let style = raw.style(); + style + .set_property("width", &format!("{}px", logical_size.width)) + .expect("Failed to set canvas width"); + style + .set_property("height", &format!("{}px", logical_size.height)) + .expect("Failed to set canvas height"); +} + pub fn is_fullscreen(canvas: &HtmlCanvasElement) -> bool { let window = window().expect("Failed to obtain window"); let document = window.document().expect("Failed to obtain document"); diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index d63086aa..2d201784 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -1,4 +1,4 @@ -use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; use crate::icon::Icon; use crate::monitor::MonitorHandle as RootMH; @@ -15,7 +15,6 @@ use std::collections::VecDeque; pub struct Window { canvas: backend::Canvas, previous_pointer: RefCell<&'static str>, - position: RefCell>, id: Id, register_redraw_request: Box, } @@ -39,15 +38,14 @@ impl Window { let window = Window { canvas, previous_pointer: RefCell::new("auto"), - position: RefCell::new(LogicalPosition { x: 0.0, y: 0.0 }), id, register_redraw_request, }; - window.set_inner_size(attr.inner_size.unwrap_or(LogicalSize { + window.set_inner_size(attr.inner_size.unwrap_or(Size::Logical(LogicalSize { width: 1024.0, height: 768.0, - })); + }))); window.set_title(&attr.title); window.set_maximized(attr.maximized); window.set_visible(attr.visible); @@ -72,18 +70,17 @@ impl Window { (self.register_redraw_request)(); } - pub fn outer_position(&self) -> Result, NotSupportedError> { - let (x, y) = self.canvas.position(); - - Ok(LogicalPosition { x, y }) + pub fn outer_position(&self) -> Result, NotSupportedError> { + Ok(self.canvas.position().to_physical(self.hidpi_factor())) } - pub fn inner_position(&self) -> Result, NotSupportedError> { - Ok(*self.position.borrow()) + pub fn inner_position(&self) -> Result, NotSupportedError> { + // Note: the canvas element has no window decorations, so this is equal to `outer_position`. + self.outer_position() } - pub fn set_outer_position(&self, position: LogicalPosition) { - *self.position.borrow_mut() = position; + pub fn set_outer_position(&self, position: Position) { + let position = position.to_logical::(self.hidpi_factor()); self.canvas.set_attribute("position", "fixed"); self.canvas.set_attribute("left", &position.x.to_string()); @@ -91,33 +88,28 @@ impl Window { } #[inline] - pub fn inner_size(&self) -> LogicalSize { - LogicalSize { - width: self.canvas.width() as f64, - height: self.canvas.height() as f64, - } + pub fn inner_size(&self) -> PhysicalSize { + self.canvas.size() } #[inline] - pub fn outer_size(&self) -> LogicalSize { - LogicalSize { - width: self.canvas.width() as f64, - height: self.canvas.height() as f64, - } + pub fn outer_size(&self) -> PhysicalSize { + // Note: the canvas element has no window decorations, so this is equal to `inner_size`. + self.inner_size() } #[inline] - pub fn set_inner_size(&self, size: LogicalSize) { - self.canvas.set_size(size); + pub fn set_inner_size(&self, size: Size) { + backend::set_canvas_size(self.canvas.raw(), size); } #[inline] - pub fn set_min_inner_size(&self, _dimensions: Option>) { + pub fn set_min_inner_size(&self, _dimensions: Option) { // Intentionally a no-op: users can't resize canvas elements } #[inline] - pub fn set_max_inner_size(&self, _dimensions: Option>) { + pub fn set_max_inner_size(&self, _dimensions: Option) { // Intentionally a no-op: users can't resize canvas elements } @@ -128,7 +120,7 @@ impl Window { #[inline] pub fn hidpi_factor(&self) -> f64 { - 1.0 + super::backend::hidpi_factor() } #[inline] @@ -178,10 +170,7 @@ impl Window { } #[inline] - pub fn set_cursor_position( - &self, - _position: LogicalPosition, - ) -> Result<(), ExternalError> { + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { // Intentionally a no-op, as the web does not support setting cursor positions Ok(()) } @@ -246,7 +235,7 @@ impl Window { } #[inline] - pub fn set_ime_position(&self, _position: LogicalPosition) { + pub fn set_ime_position(&self, _position: Position) { // Currently a no-op as it does not seem there is good support for this on web } diff --git a/src/window.rs b/src/window.rs index b961f410..ae9c41c9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -419,6 +419,8 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the /// window's [safe area] in the screen space coordinate system. + /// - **Web:** Returns the top-left coordinates relative to the viewport. _Note: this returns the + /// same value as `outer_position`._ /// /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[inline] @@ -440,6 +442,7 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the /// window in the screen space coordinate system. + /// - **Web:** Returns the top-left coordinates relative to the viewport. #[inline] pub fn outer_position(&self) -> Result, NotSupportedError> { self.window.outer_position() @@ -454,6 +457,7 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Sets the top left coordinates of the /// window in the screen space coordinate system. + /// - **Web:** Sets the top-left coordinates relative to the viewport. #[inline] pub fn set_outer_position>(&self, position: P) { self.window.set_outer_position(position.into()) @@ -467,6 +471,7 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window's /// [safe area] in screen space coordinates. + /// - **Web:** Returns the size of the canvas element. /// /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[inline] @@ -483,6 +488,7 @@ impl Window { /// /// - **iOS:** Unimplemented. Currently this panics, as it's not clear what `set_inner_size` /// would mean for iOS. + /// - **Web:** Sets the size of the canvas element. #[inline] pub fn set_inner_size>(&self, size: S) { self.window.set_inner_size(size.into()) @@ -497,6 +503,8 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window in /// screen space coordinates. + /// - **Web:** Returns the size of the canvas element. _Note: this returns the same value as + /// `inner_size`._ #[inline] pub fn outer_size(&self) -> PhysicalSize { self.window.outer_size()