From a2739ebfeb6e7edd6e90408f02ffc0b97e94f517 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Fri, 19 Jul 2024 01:59:04 +0200 Subject: [PATCH] Web: Implement `MonitorHandle` --- Cargo.toml | 7 + clippy.toml | 1 + examples/window.rs | 104 ++- src/changelog/unreleased.md | 7 + src/event_loop.rs | 17 +- src/monitor.rs | 37 +- src/platform/web.rs | 260 +++++- src/platform_impl/web/async/dispatcher.rs | 41 + src/platform_impl/web/async/wrapper.rs | 28 + src/platform_impl/web/event_loop/mod.rs | 16 +- src/platform_impl/web/event_loop/runner.rs | 80 +- .../web/event_loop/window_target.rs | 27 +- src/platform_impl/web/lock.rs | 10 +- src/platform_impl/web/mod.rs | 5 +- src/platform_impl/web/monitor.rs | 777 +++++++++++++++++- src/platform_impl/web/web_sys/canvas.rs | 32 +- src/platform_impl/web/web_sys/event.rs | 6 +- src/platform_impl/web/web_sys/fullscreen.rs | 85 +- src/platform_impl/web/web_sys/mod.rs | 16 +- src/platform_impl/web/window.rs | 30 +- src/window.rs | 28 +- 21 files changed, 1493 insertions(+), 121 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b265cf4f75..b4c5d74ec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -305,7 +305,12 @@ web_sys = { package = "web-sys", version = "0.3.64", features = [ 'MessagePort', 'Navigator', 'Node', + 'OrientationType', + 'OrientationLockType', 'PageTransitionEvent', + 'Permissions', + 'PermissionState', + 'PermissionStatus', 'PointerEvent', 'PremultiplyAlpha', 'ResizeObserver', @@ -313,6 +318,8 @@ web_sys = { package = "web-sys", version = "0.3.64", features = [ 'ResizeObserverEntry', 'ResizeObserverOptions', 'ResizeObserverSize', + 'Screen', + 'ScreenOrientation', 'VisibilityState', 'Window', 'WheelEvent', diff --git a/clippy.toml b/clippy.toml index 84bf0d5e01..c822ee4c3e 100644 --- a/clippy.toml +++ b/clippy.toml @@ -4,6 +4,7 @@ disallowed-methods = [ { path = "web_sys::HtmlCanvasElement::height", reason = "Winit shouldn't touch the internal canvas size" }, { path = "web_sys::HtmlCanvasElement::set_width", reason = "Winit shouldn't touch the internal canvas size" }, { path = "web_sys::HtmlCanvasElement::set_height", reason = "Winit shouldn't touch the internal canvas size" }, + { path = "web_sys::Window::navigator", reason = "cache this to reduce calls to JS" }, { path = "web_sys::Window::document", reason = "cache this to reduce calls to JS" }, { path = "web_sys::Window::get_computed_style", reason = "cache this to reduce calls to JS" }, { path = "web_sys::HtmlElement::style", reason = "cache this to reduce calls to JS" }, diff --git a/examples/window.rs b/examples/window.rs index daf1a610f9..629ef7ec3f 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -5,6 +5,7 @@ use std::error::Error; use std::fmt::Debug; #[cfg(not(any(android_platform, ios_platform)))] use std::num::NonZeroU32; +use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::Arc; use std::{fmt, mem}; @@ -25,6 +26,8 @@ use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMac use winit::platform::startup_notify::{ self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify, }; +#[cfg(web_platform)] +use winit::platform::web::{ActiveEventLoopExtWeb, CustomCursorExtWeb, WindowAttributesExtWeb}; use winit::window::{ Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, Fullscreen, Icon, ResizeDirection, Theme, Window, WindowId, @@ -43,26 +46,34 @@ fn main() -> Result<(), Box> { tracing::init(); let event_loop = EventLoop::new()?; - let _event_loop_proxy = event_loop.create_proxy(); + let (sender, receiver) = mpsc::channel(); // Wire the user event from another thread. #[cfg(not(web_platform))] - std::thread::spawn(move || { - // Wake up the `event_loop` once every second and dispatch a custom event - // from a different thread. - info!("Starting to send user event every second"); - loop { - _event_loop_proxy.wake_up(); - std::thread::sleep(std::time::Duration::from_secs(1)); - } - }); + { + let event_loop_proxy = event_loop.create_proxy(); + let sender = sender.clone(); + std::thread::spawn(move || { + // Wake up the `event_loop` once every second and dispatch a custom event + // from a different thread. + info!("Starting to send user event every second"); + loop { + let _ = sender.send(Action::Message); + event_loop_proxy.wake_up(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + }); + } - let app = Application::new(&event_loop); + let app = Application::new(&event_loop, receiver, sender); Ok(event_loop.run_app(app)?) } /// Application state and event handling. struct Application { + /// Trigger actions through proxy wake up. + receiver: Receiver, + sender: Sender, /// Custom cursors assets. custom_cursors: Vec, /// Application icon. @@ -76,7 +87,7 @@ struct Application { } impl Application { - fn new(event_loop: &EventLoop) -> Self { + fn new(event_loop: &EventLoop, receiver: Receiver, sender: Sender) -> Self { // SAFETY: we drop the context right before the event loop is stopped, thus making it safe. #[cfg(not(any(android_platform, ios_platform)))] let context = Some( @@ -103,6 +114,8 @@ impl Application { ]; Self { + receiver, + sender, #[cfg(not(any(android_platform, ios_platform)))] context, custom_cursors, @@ -138,7 +151,6 @@ impl Application { #[cfg(web_platform)] { - use winit::platform::web::WindowAttributesExtWeb; window_attributes = window_attributes.with_append(true); } @@ -160,7 +172,23 @@ impl Application { Ok(window_id) } - fn handle_action(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, action: Action) { + fn handle_action(&mut self, event_loop: &ActiveEventLoop, action: Action) { + match action { + Action::PrintHelp => self.print_help(), + Action::DumpMonitors => self.dump_monitors(event_loop), + Action::Message => { + info!("User wake up"); + }, + _ => unreachable!("Tried to execute invalid action without `WindowId`"), + } + } + + fn handle_action_with_window( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + action: Action, + ) { // let cursor_position = self.cursor_position; let window = self.windows.get_mut(&window_id).unwrap(); info!("Executing action: {action:?}"); @@ -200,7 +228,6 @@ impl Application { Action::DragWindow => window.drag_window(), Action::DragResizeWindow => window.drag_resize_window(), Action::ShowWindowMenu => window.show_menu(), - Action::PrintHelp => self.print_help(), #[cfg(macos_platform)] Action::CycleOptionAsAlt => window.cycle_option_as_alt(), Action::SetTheme(theme) => { @@ -217,6 +244,27 @@ impl Application { } }, Action::RequestResize => window.swap_dimensions(), + #[cfg(web_platform)] + Action::DumpMonitors => { + let future = event_loop.request_detailed_monitor_permission(); + let proxy = event_loop.create_proxy(); + let sender = self.sender.clone(); + wasm_bindgen_futures::spawn_local(async move { + if let Err(error) = future.await { + error!("{error}") + } + + let _ = sender.send(Action::DumpMonitors); + proxy.wake_up(); + }); + }, + #[cfg(not(web_platform))] + Action::DumpMonitors => self.dump_monitors(event_loop), + Action::Message => { + self.sender.send(Action::Message).unwrap(); + event_loop.create_proxy().wake_up(); + }, + _ => self.handle_action(event_loop, action), } } @@ -300,8 +348,10 @@ impl Application { } impl ApplicationHandler for Application { - fn proxy_wake_up(&mut self, _event_loop: &ActiveEventLoop) { - info!("User wake up"); + fn proxy_wake_up(&mut self, event_loop: &ActiveEventLoop) { + while let Ok(action) = self.receiver.try_recv() { + self.handle_action(event_loop, action) + } } fn window_event( @@ -369,7 +419,7 @@ impl ApplicationHandler for Application { }; if let Some(action) = action { - self.handle_action(event_loop, window_id, action); + self.handle_action_with_window(event_loop, window_id, action); } } }, @@ -378,7 +428,7 @@ impl ApplicationHandler for Application { if let Some(action) = state.is_pressed().then(|| Self::process_mouse_binding(button, &mods)).flatten() { - self.handle_action(event_loop, window_id, action); + self.handle_action_with_window(event_loop, window_id, action); } }, WindowEvent::CursorLeft { .. } => { @@ -703,8 +753,6 @@ impl WindowState { ) { use std::time::Duration; - use winit::platform::web::CustomCursorExtWeb; - let cursors = vec![ custom_cursors[0].clone(), custom_cursors[1].clone(), @@ -886,6 +934,8 @@ enum Action { #[cfg(macos_platform)] CreateNewTab, RequestResize, + DumpMonitors, + Message, } impl Action { @@ -920,6 +970,14 @@ impl Action { #[cfg(macos_platform)] Action::CreateNewTab => "Create new tab", Action::RequestResize => "Request a resize", + #[cfg(not(web_platform))] + Action::DumpMonitors => "Dump monitor information", + #[cfg(web_platform)] + Action::DumpMonitors => { + "Request permission to query detailed monitor information and dump monitor \ + information" + }, + Action::Message => "Prints a message through a user wake up", } } } @@ -942,8 +1000,6 @@ fn decode_cursor(bytes: &[u8]) -> CustomCursorSource { fn url_custom_cursor() -> CustomCursorSource { use std::sync::atomic::{AtomicU64, Ordering}; - use winit::platform::web::CustomCursorExtWeb; - static URL_COUNTER: AtomicU64 = AtomicU64::new(0); CustomCursor::from_url( @@ -1041,6 +1097,7 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[ Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable), Binding::new("R", ModifiersState::ALT, Action::RequestResize), // M. + Binding::new("M", ModifiersState::CONTROL.union(ModifiersState::ALT), Action::DumpMonitors), Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize), Binding::new("M", ModifiersState::ALT, Action::Minimize), // N. @@ -1069,6 +1126,7 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[ Binding::new("T", ModifiersState::SUPER, Action::CreateNewTab), #[cfg(macos_platform)] Binding::new("O", ModifiersState::CONTROL, Action::CycleOptionAsAlt), + Binding::new("S", ModifiersState::CONTROL, Action::Message), ]; const MOUSE_BINDINGS: &[Binding] = &[ diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 06f794cbc5..08e2fe9179 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -47,6 +47,13 @@ changelog entry. - On Web, add `ActiveEventLoopExtWeb::is_cursor_lock_raw()` to determine if `DeviceEvent::MouseMotion` is returning raw data, not OS accelerated, when using `CursorGrabMode::Locked`. +- On Web, implement `MonitorHandle` and `VideoModeHandle`. + + Without prompting the user for permission, only the current monitor is returned. But when + prompting and being granted permission through + `ActiveEventLoop::request_detailed_monitor_permission()`, access to all monitors and their + information is available. This "detailed monitors" can be used in `Window::set_fullscreen()` as + well. ### Changed diff --git a/src/event_loop.rs b/src/event_loop.rs index 404acca59d..843adea744 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -380,6 +380,15 @@ impl ActiveEventLoop { } /// Returns the list of all the monitors available on the system. + /// + /// ## Platform-specific + /// + /// **Web:** Only returns the current monitor without + #[cfg_attr( + any(web_platform, docsrs), + doc = "[detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]." + )] + #[cfg_attr(not(any(web_platform, docsrs)), doc = "detailed monitor permissions.")] #[inline] pub fn available_monitors(&self) -> impl Iterator { let _span = tracing::debug_span!("winit::ActiveEventLoop::available_monitors",).entered(); @@ -394,7 +403,13 @@ impl ActiveEventLoop { /// /// ## Platform-specific /// - /// **Wayland / Web:** Always returns `None`. + /// - **Wayland:** Always returns `None`. + /// - **Web:** Always returns `None` without + #[cfg_attr( + any(web_platform, docsrs), + doc = " [detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]." + )] + #[cfg_attr(not(any(web_platform, docsrs)), doc = " detailed monitor permissions.")] #[inline] pub fn primary_monitor(&self) -> Option { let _span = tracing::debug_span!("winit::ActiveEventLoop::primary_monitor",).entered(); diff --git a/src/monitor.rs b/src/monitor.rs index ed987e25d1..f4743fdd37 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -68,6 +68,10 @@ impl VideoModeHandle { } /// Returns the refresh rate of this video mode in mHz. + /// + /// ## Platform-specific + /// + /// **Web:** Always returns `0`. #[inline] pub fn refresh_rate_millihertz(&self) -> u32 { self.video_mode.refresh_rate_millihertz() @@ -108,6 +112,15 @@ impl MonitorHandle { /// Returns a human-readable name of the monitor. /// /// Returns `None` if the monitor doesn't exist anymore. + /// + /// ## Platform-specific + /// + /// **Web:** Always returns [`None`] without + #[cfg_attr( + any(web_platform, docsrs), + doc = "[detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]." + )] + #[cfg_attr(not(any(web_platform, docsrs)), doc = "detailed monitor permissions.")] #[inline] pub fn name(&self) -> Option { self.inner.name() @@ -121,6 +134,15 @@ impl MonitorHandle { /// Returns the top-left corner position of the monitor relative to the larger full /// screen area. + /// + /// ## Platform-specific + /// + /// **Web:** Always returns [`Default`] without + #[cfg_attr( + any(web_platform, docsrs), + doc = "[detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]." + )] + #[cfg_attr(not(any(web_platform, docsrs)), doc = "detailed monitor permissions.")] #[inline] pub fn position(&self) -> PhysicalPosition { self.inner.position() @@ -133,6 +155,10 @@ impl MonitorHandle { /// /// When using exclusive fullscreen, the refresh rate of the [`VideoModeHandle`] that was /// used to enter fullscreen should be used instead. + /// + /// ## Platform-specific + /// + /// **Web:** Always returns [`None`]. #[inline] pub fn refresh_rate_millihertz(&self) -> Option { self.inner.refresh_rate_millihertz() @@ -148,7 +174,14 @@ impl MonitorHandle { /// - **X11:** Can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable. /// - **Wayland:** May differ from [`Window::scale_factor`]. /// - **Android:** Always returns 1.0. + /// - **Web:** Always returns `0.0` without + #[cfg_attr( + any(web_platform, docsrs), + doc = " [detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]." + )] + #[cfg_attr(not(any(web_platform, docsrs)), doc = " detailed monitor permissions.")] /// + #[rustfmt::skip] /// [`Window::scale_factor`]: crate::window::Window::scale_factor #[inline] pub fn scale_factor(&self) -> f64 { @@ -156,10 +189,6 @@ impl MonitorHandle { } /// Returns all fullscreen video modes supported by this monitor. - /// - /// ## Platform-specific - /// - /// - **Web:** Always returns an empty iterator #[inline] pub fn video_modes(&self) -> impl Iterator { self.inner.video_modes().map(|video_mode| VideoModeHandle { video_mode }) diff --git a/src/platform/web.rs b/src/platform/web.rs index 2974ba3d66..61faf39bd1 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -55,10 +55,17 @@ use web_sys::HtmlCanvasElement; use crate::application::ApplicationHandler; use crate::cursor::CustomCursorSource; +use crate::error::NotSupportedError; use crate::event_loop::{ActiveEventLoop, EventLoop}; -#[cfg(web_platform)] -use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture; +use crate::monitor::MonitorHandle; use crate::platform_impl::PlatformCustomCursorSource; +#[cfg(web_platform)] +use crate::platform_impl::{ + CustomCursorFuture as PlatformCustomCursorFuture, + HasMonitorPermissionFuture as PlatformHasMonitorPermissionFuture, + MonitorPermissionFuture as PlatformMonitorPermissionFuture, + OrientationLockFuture as PlatformOrientationLockFuture, +}; use crate::window::{CustomCursor, Window, WindowAttributes}; #[cfg(not(web_platform))] @@ -218,6 +225,21 @@ pub trait EventLoopExtWeb { /// /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil fn wait_until_strategy(&self) -> WaitUntilStrategy; + + /// Returns if the users device has multiple screens. + /// + /// Browsers might always return [`false`] to reduce fingerprinting. + fn has_multiple_screens(&self) -> Result; + + /// Prompts the user for permission to query detailed information about available monitors. The + /// returned [`MonitorPermissionFuture`] can be dropped without aborting the request. + /// + /// [`MonitorHandle`]s don't automatically make use of this after permission is granted. New + /// [`MonitorHandle`]s have to be created instead. + fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture; + + /// Returns whether the user has given permission to access detailed monitor information. + fn has_detailed_monitor_permission(&self) -> HasMonitorPermissionFuture; } impl EventLoopExtWeb for EventLoop { @@ -240,6 +262,18 @@ impl EventLoopExtWeb for EventLoop { fn wait_until_strategy(&self) -> WaitUntilStrategy { self.event_loop.wait_until_strategy() } + + fn has_multiple_screens(&self) -> Result { + self.event_loop.has_multiple_screens() + } + + fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture { + MonitorPermissionFuture(self.event_loop.request_detailed_monitor_permission()) + } + + fn has_detailed_monitor_permission(&self) -> HasMonitorPermissionFuture { + HasMonitorPermissionFuture(self.event_loop.has_detailed_monitor_permission()) + } } pub trait ActiveEventLoopExtWeb { @@ -279,6 +313,24 @@ pub trait ActiveEventLoopExtWeb { /// /// [`CursorGrabMode::Locked`]: crate::window::CursorGrabMode::Locked fn is_cursor_lock_raw(&self) -> bool; + + /// Returns if the users device has multiple screens. + /// + /// Browsers might always return [`false`] to reduce fingerprinting. + fn has_multiple_screens(&self) -> Result; + + /// Prompts the user for permission to query detailed information about available monitors. The + /// returned [`MonitorPermissionFuture`] can be dropped without aborting the request. + /// + /// [`MonitorHandle`]s don't automatically make use of this after permission is granted. New + /// [`MonitorHandle`]s have to be created instead. + fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture; + + /// Returns whether the user has given permission to access detailed monitor information. + /// + /// [`MonitorHandle`]s don't automatically make use of this after permission is granted. New + /// [`MonitorHandle`]s have to be created instead. + fn has_detailed_monitor_permission(&self) -> bool; } impl ActiveEventLoopExtWeb for ActiveEventLoop { @@ -311,6 +363,21 @@ impl ActiveEventLoopExtWeb for ActiveEventLoop { fn is_cursor_lock_raw(&self) -> bool { self.p.is_cursor_lock_raw() } + + #[inline] + fn has_multiple_screens(&self) -> Result { + self.p.has_multiple_screens() + } + + #[inline] + fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture { + MonitorPermissionFuture(self.p.request_detailed_monitor_permission()) + } + + #[inline] + fn has_detailed_monitor_permission(&self) -> bool { + self.p.has_detailed_monitor_permission() + } } /// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll]. @@ -458,3 +525,192 @@ impl Display for CustomCursorError { } impl Error for CustomCursorError {} + +#[cfg(not(web_platform))] +struct PlatformMonitorPermissionFuture; + +/// Can be dropped without aborting the request for detailed monitor permissions. +#[derive(Debug)] +pub struct MonitorPermissionFuture(pub(crate) PlatformMonitorPermissionFuture); + +impl Future for MonitorPermissionFuture { + type Output = Result<(), MonitorPermissionError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0).poll(cx) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum MonitorPermissionError { + /// User has explicitly denied permission to query detailed monitor information. + Denied, + /// User has not decided to give permission to query detailed monitor information. + Prompt, + /// Browser does not support detailed monitor information. + Unsupported, +} + +impl Display for MonitorPermissionError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + MonitorPermissionError::Denied => write!( + f, + "User has explicitly denied permission to query detailed monitor information" + ), + MonitorPermissionError::Prompt => write!( + f, + "User has not decided to give permission to query detailed monitor information" + ), + MonitorPermissionError::Unsupported => { + write!(f, "Browser does not support detailed monitor information") + }, + } + } +} + +impl Error for MonitorPermissionError {} + +#[cfg(not(web_platform))] +struct PlatformHasMonitorPermissionFuture; + +#[derive(Debug)] +pub struct HasMonitorPermissionFuture(PlatformHasMonitorPermissionFuture); + +impl Future for HasMonitorPermissionFuture { + type Output = bool; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0).poll(cx) + } +} + +/// Additional methods on [`MonitorHandle`] that are specific to the Web. +pub trait MonitorHandleExtWeb { + /// Returns whether the screen is internal to the device or external. + /// + /// External devices are generally manufactured separately from the device they are attached to + /// and can be connected and disconnected as needed, whereas internal screens are part of + /// the device and not intended to be disconnected. + fn is_internal(&self) -> Option; + + /// Returns screen orientation data for this monitor. + fn orientation(&self) -> OrientationData; + + /// Lock the screen orientation. The returned [`OrientationLockFuture`] can be dropped without + /// aborting the request. + /// + /// Will fail if another locking call is in progress. + fn request_lock(&self, orientation: OrientationLock) -> OrientationLockFuture; + + /// Unlock the screen orientation. + /// + /// Will fail if a locking call is in progress. + fn unlock(&self) -> Result<(), OrientationLockError>; + + /// Returns whether this [`MonitorHandle`] was created using detailed monitor permissions. + /// + /// See [`ActiveEventLoop::request_detailed_monitor_permission()`]. + fn is_detailed(&self) -> bool; +} + +impl MonitorHandleExtWeb for MonitorHandle { + fn is_internal(&self) -> Option { + self.inner.is_internal() + } + + fn orientation(&self) -> OrientationData { + self.inner.orientation() + } + + fn request_lock(&self, orientation_lock: OrientationLock) -> OrientationLockFuture { + OrientationLockFuture(self.inner.request_lock(orientation_lock)) + } + + fn unlock(&self) -> Result<(), OrientationLockError> { + self.inner.unlock() + } + + fn is_detailed(&self) -> bool { + self.inner.is_detailed() + } +} + +/// Screen orientation data. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct OrientationData { + /// The orientation. + pub orientation: Orientation, + /// [`true`] if the [`orientation`](Self::orientation) is flipped upside down. + pub flipped: bool, + /// [`true`] if the [`Orientation`] is the most natural one for the screen regardless of being + /// flipped. Computer monitors are commonly naturally landscape mode, while mobile phones + /// are commonly naturally portrait mode. + pub natural: bool, +} + +/// Screen orientation. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Orientation { + /// The screen's aspect ratio has a width greater than the height. + Landscape, + /// The screen's aspect ratio has a height greater than the width. + Portrait, +} + +/// Screen orientation lock options. Reoresents which orientations a user can use. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum OrientationLock { + /// User is free to use any orientation. + Any, + /// User is locked to the most upright natural orientation for the screen. Computer monitors + /// are commonly naturally landscape mode, while mobile phones are commonly + /// naturally portrait mode. + Natural, + /// User is locked to landscape mode. + Landscape { + /// - [`None`]: User is locked to both upright or upside down landscape mode. + /// - [`false`]: User is locked to upright landscape mode. + /// - [`false`]: User is locked to upside down landscape mode. + flipped: Option, + }, + /// User is locked to portrait mode. + Portrait { + /// - [`None`]: User is locked to both upright or upside down portrait mode. + /// - [`false`]: User is locked to upright portrait mode. + /// - [`false`]: User is locked to upside down portrait mode. + flipped: Option, + }, +} + +#[cfg(not(web_platform))] +struct PlatformOrientationLockFuture; + +/// Can be dropped without aborting the request to lock the screen. +#[derive(Debug)] +pub struct OrientationLockFuture(PlatformOrientationLockFuture); + +impl Future for OrientationLockFuture { + type Output = Result<(), OrientationLockError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0).poll(cx) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum OrientationLockError { + Unsupported, + Busy, +} + +impl Display for OrientationLockError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Unsupported => write!(f, "Locking the screen orientation is not supported"), + Self::Busy => write!(f, "Another locking call is in progress"), + } + } +} + +impl Error for OrientationLockError {} diff --git a/src/platform_impl/web/async/dispatcher.rs b/src/platform_impl/web/async/dispatcher.rs index beac64873a..0e85944e24 100644 --- a/src/platform_impl/web/async/dispatcher.rs +++ b/src/platform_impl/web/async/dispatcher.rs @@ -1,4 +1,7 @@ use std::cell::Ref; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::rc::Rc; use std::sync::{Arc, Condvar, Mutex}; @@ -9,6 +12,44 @@ pub struct Dispatcher(Wrapper>>, Closure struct Closure(Box); +impl Clone for Dispatcher { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Debug for Dispatcher { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Dispatcher").finish_non_exhaustive() + } +} + +impl Eq for Dispatcher {} + +impl Hash for Dispatcher { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl Ord for Dispatcher { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } +} + +impl PartialEq for Dispatcher { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) + } +} + +impl PartialOrd for Dispatcher { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Dispatcher { pub fn new(main_thread: MainThreadMarker, value: T) -> (Self, DispatchRunner) { let (sender, receiver) = channel::>(); diff --git a/src/platform_impl/web/async/wrapper.rs b/src/platform_impl/web/async/wrapper.rs index 35524555f8..c26df39c79 100644 --- a/src/platform_impl/web/async/wrapper.rs +++ b/src/platform_impl/web/async/wrapper.rs @@ -1,5 +1,7 @@ use std::cell::{Ref, RefCell}; +use std::cmp; use std::future::Future; +use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::sync::Arc; @@ -81,3 +83,29 @@ impl Clone for Wrapper { } } } + +impl Eq for Wrapper {} + +impl Hash for Wrapper { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.value.value).hash(state) + } +} + +impl Ord for Wrapper { + fn cmp(&self, other: &Self) -> cmp::Ordering { + Arc::as_ptr(&self.value.value).cmp(&Arc::as_ptr(&other.value.value)) + } +} + +impl PartialOrd for Wrapper { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Wrapper { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.value.value, &other.value.value) + } +} diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 6daa220b20..53050f65dc 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -1,8 +1,8 @@ use std::marker::PhantomData; -use super::{backend, device, window}; +use super::{backend, device, window, HasMonitorPermissionFuture, MonitorPermissionFuture}; use crate::application::ApplicationHandler; -use crate::error::EventLoopError; +use crate::error::{EventLoopError, NotSupportedError}; use crate::event::Event; use crate::event_loop::ActiveEventLoop as RootActiveEventLoop; use crate::platform::web::{ActiveEventLoopExtWeb, PollStrategy, WaitUntilStrategy}; @@ -77,6 +77,18 @@ impl EventLoop { pub fn wait_until_strategy(&self) -> WaitUntilStrategy { self.elw.wait_until_strategy() } + + pub fn has_multiple_screens(&self) -> Result { + self.elw.has_multiple_screens() + } + + pub(crate) fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture { + self.elw.request_detailed_monitor_permission().0 + } + + pub fn has_detailed_monitor_permission(&self) -> HasMonitorPermissionFuture { + self.elw.p.runner.monitor().has_detailed_monitor_permission_async() + } } fn handle_event(app: &mut A, target: &RootActiveEventLoop, event: Event) { diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index 2739ced89c..9990ee6bc9 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -7,10 +7,11 @@ use std::rc::{Rc, Weak}; use js_sys::Function; use wasm_bindgen::prelude::{wasm_bindgen, Closure}; use wasm_bindgen::JsCast; -use web_sys::{Document, KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent}; +use web_sys::{Document, KeyboardEvent, Navigator, PageTransitionEvent, PointerEvent, WheelEvent}; use web_time::{Duration, Instant}; use super::super::main_thread::MainThreadMarker; +use super::super::monitor::MonitorHandler; use super::super::DeviceId; use super::backend; use super::state::State; @@ -51,11 +52,13 @@ struct Execution { events: RefCell>, id: RefCell, window: web_sys::Window, + navigator: Navigator, document: Document, #[allow(clippy::type_complexity)] all_canvases: RefCell, DispatchRunner)>>, redraw_pending: RefCell>, destroy_pending: RefCell>, + pub(crate) monitor: Rc, page_transition_event_handle: RefCell>, device_events: Cell, on_mouse_move: OnEventHandle, @@ -70,6 +73,8 @@ struct Execution { enum RunnerEnum { /// The `EventLoop` is created but not being run. Pending, + /// The `EventLoop` is running some async initialization and is waiting to be started. + Initializing(Runner), /// The `EventLoop` is being run. Running(Runner), /// The `EventLoop` is exited after being started with `EventLoop::run_app`. Since @@ -134,6 +139,8 @@ impl Shared { #[allow(clippy::disallowed_methods)] let window = web_sys::window().expect("only callable from inside the `Window`"); #[allow(clippy::disallowed_methods)] + let navigator = window.navigator(); + #[allow(clippy::disallowed_methods)] let document = window.document().expect("Failed to obtain document"); Shared(Rc::::new_cyclic(|weak| { @@ -144,6 +151,13 @@ impl Shared { } }); + let monitor = MonitorHandler::new( + main_thread, + window.clone(), + &navigator, + WeakShared(weak.clone()), + ); + Execution { main_thread, proxy_spawner, @@ -156,11 +170,13 @@ impl Shared { event_loop_recreation: Cell::new(false), events: RefCell::new(VecDeque::new()), window, + navigator, document, id: RefCell::new(0), all_canvases: RefCell::new(Vec::new()), redraw_pending: RefCell::new(HashSet::new()), destroy_pending: RefCell::new(VecDeque::new()), + monitor: Rc::new(monitor), page_transition_event_handle: RefCell::new(None), device_events: Cell::default(), on_mouse_move: RefCell::new(None), @@ -182,6 +198,10 @@ impl Shared { &self.0.window } + pub fn navigator(&self) -> &Navigator { + &self.0.navigator + } + pub fn document(&self) -> &Document { &self.0.document } @@ -199,17 +219,42 @@ impl Shared { self.0.destroy_pending.borrow_mut().push_back(id); } - // 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 RootActiveEventLoop reference - pub(crate) fn set_listener(&self, event_handler: Box) { - { + pub(crate) fn start(&self, event_handler: Box) { + let start = { let mut runner = self.0.runner.borrow_mut(); assert!(matches!(*runner, RunnerEnum::Pending)); - *runner = RunnerEnum::Running(Runner::new(event_handler)); + if self.0.monitor.is_initializing() { + *runner = RunnerEnum::Initializing(Runner::new(event_handler)); + false + } else { + *runner = RunnerEnum::Running(Runner::new(event_handler)); + true + } + }; + + if start { + self.init(); + self.set_listener(); } + } + + pub(crate) fn start_delayed(&self) { + let event_handler = match self.0.runner.replace(RunnerEnum::Pending) { + RunnerEnum::Initializing(event_handler) => event_handler, + // The event loop wasn't started yet. + RunnerEnum::Pending => return, + _ => unreachable!("event loop already started before waiting for initialization"), + }; + *self.0.runner.borrow_mut() = RunnerEnum::Running(event_handler); + self.init(); + self.set_listener(); + } + // 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 RootActiveEventLoop reference + fn set_listener(&self) { *self.0.page_transition_event_handle.borrow_mut() = Some(backend::on_page_transition( self.window().clone(), { @@ -236,6 +281,7 @@ impl Shared { let runner = self.clone(); let window = self.window().clone(); + let navigator = self.navigator().clone(); *self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new( self.window().clone(), "pointermove", @@ -263,7 +309,7 @@ impl Shared { } // pointer move event - let mut delta = backend::event::MouseDelta::init(&window, &event); + let mut delta = backend::event::MouseDelta::init(&navigator, &event); runner.send_events(backend::event::pointer_move_event(event).flat_map(|event| { let delta = delta.delta(&event).to_physical(backend::scale_factor(&window)); @@ -419,7 +465,7 @@ impl Shared { self.send_events::(iter::empty()); } - pub fn init(&self) { + fn init(&self) { // NB: For consistency all platforms must call `can_create_surfaces` even though Web // applications don't themselves have a formal surface destroy/create lifecycle. self.run_until_cleared( @@ -501,8 +547,8 @@ impl Shared { match self.0.runner.try_borrow().as_ref().map(Deref::deref) { // If the runner is attached but not running, we always wake it up. Ok(RunnerEnum::Running(_)) => (), - Ok(RunnerEnum::Pending) => { - // The runner still hasn't been attached: queue this event and wait for it to be + // The runner still hasn't been attached: queue this event and wait for it to be + Ok(RunnerEnum::Pending | RunnerEnum::Initializing(_)) => { process_immediately = false; }, // Some other code is mutating the runner, which most likely means @@ -605,6 +651,8 @@ impl Shared { 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, + // This function should never be called if we are still waiting for something. + RunnerEnum::Initializing(_) => unreachable!(), } let is_closed = self.exiting(); @@ -730,6 +778,8 @@ impl Shared { Ok(RunnerEnum::Pending) => false, // The event loop is closed since it has been destroyed. Ok(RunnerEnum::Destroyed) => true, + // The event loop is not closed since its still waiting to be started. + Ok(RunnerEnum::Initializing(_)) => false, // Some other code is mutating the runner, which most likely means // the event loop is running and busy. Err(_) => false, @@ -795,6 +845,14 @@ impl Shared { pub(crate) fn waker(&self) -> Waker { self.0.proxy_spawner.waker() } + + pub(crate) fn weak(&self) -> WeakShared { + WeakShared(Rc::downgrade(&self.0)) + } + + pub(crate) fn monitor(&self) -> &Rc { + &self.0.monitor + } } #[derive(Clone, Debug)] diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 4cca979432..823e040335 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -1,18 +1,17 @@ use std::cell::Cell; use std::clone::Clone; -use std::collections::vec_deque::IntoIter as VecDequeIter; -use std::collections::VecDeque; use std::iter; use std::rc::Rc; use web_sys::Element; -use super::super::monitor::MonitorHandle; +use super::super::monitor::{MonitorHandle, MonitorPermissionFuture}; use super::super::{lock, KeyEventExtra}; use super::device::DeviceId; use super::runner::{EventWrapper, WeakShared}; use super::window::WindowId; use super::{backend, runner, EventLoopProxy}; +use crate::error::NotSupportedError; use crate::event::{ DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, WindowEvent, }; @@ -61,7 +60,7 @@ impl ActiveEventLoop { event_loop_recreation: bool, ) { self.runner.event_loop_recreation(event_loop_recreation); - self.runner.set_listener(event_handler); + self.runner.start(event_handler); } pub fn generate_id(&self) -> WindowId { @@ -594,12 +593,12 @@ impl ActiveEventLoop { canvas.on_context_menu(); } - pub fn available_monitors(&self) -> VecDequeIter { - VecDeque::new().into_iter() + pub fn available_monitors(&self) -> Vec { + self.runner.monitor().available_monitors() } pub fn primary_monitor(&self) -> Option { - None + self.runner.monitor().primary_monitor() } #[cfg(feature = "rwh_05")] @@ -653,7 +652,19 @@ impl ActiveEventLoop { } pub(crate) fn is_cursor_lock_raw(&self) -> bool { - lock::is_cursor_lock_raw(self.runner.window(), self.runner.document()) + lock::is_cursor_lock_raw(self.runner.navigator(), self.runner.document()) + } + + pub(crate) fn has_multiple_screens(&self) -> Result { + self.runner.monitor().is_extended().ok_or(NotSupportedError::new()) + } + + pub(crate) fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture { + self.runner.monitor().request_detailed_monitor_permission(self.runner.weak()) + } + + pub(crate) fn has_detailed_monitor_permission(&self) -> bool { + self.runner.monitor().has_detailed_monitor_permission() } pub(crate) fn waker(&self) -> Waker { diff --git a/src/platform_impl/web/lock.rs b/src/platform_impl/web/lock.rs index b4c752219b..2afd2f37a2 100644 --- a/src/platform_impl/web/lock.rs +++ b/src/platform_impl/web/lock.rs @@ -5,9 +5,9 @@ use tracing::error; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{console, Document, DomException, Element, Window}; +use web_sys::{console, Document, DomException, Element, Navigator}; -pub(crate) fn is_cursor_lock_raw(window: &Window, document: &Document) -> bool { +pub(crate) fn is_cursor_lock_raw(navigator: &Navigator, document: &Document) -> bool { thread_local! { static IS_CURSOR_LOCK_RAW: OnceCell = const { OnceCell::new() }; } @@ -17,7 +17,7 @@ pub(crate) fn is_cursor_lock_raw(window: &Window, document: &Document) -> bool { // TODO: Remove when Chrome can better advertise that they don't support unaccelerated // movement on Linux. // See . - if super::web_sys::chrome_linux(window) { + if super::web_sys::chrome_linux(navigator) { return false; } @@ -39,8 +39,8 @@ pub(crate) fn is_cursor_lock_raw(window: &Window, document: &Document) -> bool { }) } -pub(crate) fn request_pointer_lock(window: &Window, document: &Document, element: &Element) { - if is_cursor_lock_raw(window, document) { +pub(crate) fn request_pointer_lock(navigator: &Navigator, document: &Document, element: &Element) { + if is_cursor_lock_raw(navigator, document) { thread_local! { static REJECT_HANDLER: Closure = Closure::new(|error: JsValue| { if let Some(error) = error.dyn_ref::() { diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 7e42a9fd02..325ab478aa 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -44,7 +44,10 @@ pub(crate) use self::event_loop::{ PlatformSpecificEventLoopAttributes, }; pub(crate) use self::keyboard::KeyEventExtra; -pub use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::monitor::{ + HasMonitorPermissionFuture, MonitorHandle, MonitorPermissionFuture, OrientationLockFuture, + VideoModeHandle, +}; use self::web_sys as backend; pub use self::window::{PlatformSpecificWindowAttributes, Window, WindowId}; pub(crate) use crate::icon::NoIcon as PlatformIcon; diff --git a/src/platform_impl/web/monitor.rs b/src/platform_impl/web/monitor.rs index 1870284a89..daebbe672a 100644 --- a/src/platform_impl/web/monitor.rs +++ b/src/platform_impl/web/monitor.rs @@ -1,53 +1,798 @@ -use std::iter::Empty; +use std::cell::{OnceCell, Ref, RefCell}; +use std::future::Future; +use std::hash::Hash; +use std::iter::{self, Once}; +use std::mem; +use std::ops::Deref; +use std::pin::Pin; +use std::task::{ready, Context, Poll}; +use dpi::LogicalSize; +use js_sys::{Object, Promise}; +use tracing::error; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + console, DomException, Navigator, OrientationLockType, OrientationType, PermissionState, + PermissionStatus, ScreenOrientation, Window, +}; + +use super::event_loop::runner::WeakShared; +use super::main_thread::MainThreadMarker; +use super::r#async::{Dispatcher, Notified, Notifier}; +use super::web_sys::{Engine, EventListenerHandle}; use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::platform::web::{ + MonitorPermissionError, Orientation, OrientationData, OrientationLock, OrientationLockError, +}; -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct MonitorHandle; +#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct MonitorHandle(Dispatcher); impl MonitorHandle { + fn new(main_thread: MainThreadMarker, inner: Inner) -> Self { + Self(Dispatcher::new(main_thread, inner).0) + } + pub fn scale_factor(&self) -> f64 { - unreachable!() + self.0.queue(|inner| match &inner.screen { + Screen::Screen(_) => 0., + Screen::Detailed(screen) => screen.device_pixel_ratio(), + }) } pub fn position(&self) -> PhysicalPosition { - unreachable!() + self.0.queue(|inner| { + if let Screen::Detailed(screen) = &inner.screen { + PhysicalPosition::new(screen.left(), screen.top()) + } else { + PhysicalPosition::default() + } + }) } pub fn name(&self) -> Option { - unreachable!() + self.0.queue(|inner| { + if let Screen::Detailed(screen) = &inner.screen { + Some(screen.label()) + } else { + None + } + }) } pub fn refresh_rate_millihertz(&self) -> Option { - unreachable!() + None } pub fn size(&self) -> PhysicalSize { - unreachable!() + self.0.queue(|inner| { + let width = inner.screen.width().unwrap(); + let height = inner.screen.height().unwrap(); + + if let Some(Engine::Chromium) = inner.engine { + PhysicalSize::new(width, height).cast() + } else { + LogicalSize::new(width, height) + .to_physical(super::web_sys::scale_factor(&inner.window)) + } + }) + } + + pub fn video_modes(&self) -> Once { + iter::once(VideoModeHandle(self.clone())) + } + + pub fn orientation(&self) -> OrientationData { + self.0.queue(|inner| { + let orientation = + inner.orientation.get_or_init(|| inner.screen.orientation().unchecked_into()); + let angle = orientation.angle().unwrap(); + + match orientation.type_().unwrap() { + OrientationType::LandscapePrimary => OrientationData { + orientation: Orientation::Landscape, + flipped: false, + natural: angle == 0, + }, + OrientationType::LandscapeSecondary => OrientationData { + orientation: Orientation::Landscape, + flipped: true, + natural: angle == 180, + }, + OrientationType::PortraitPrimary => OrientationData { + orientation: Orientation::Portrait, + flipped: false, + natural: angle == 0, + }, + OrientationType::PortraitSecondary => OrientationData { + orientation: Orientation::Portrait, + flipped: true, + natural: angle == 180, + }, + _ => { + unreachable!("found unrecognized orientation: {}", orientation.type_string()) + }, + } + }) } - pub fn video_modes(&self) -> Empty { - unreachable!() + pub fn request_lock(&self, orientation_lock: OrientationLock) -> OrientationLockFuture { + // Short-circuit without blocking. + if let Some(support) = HAS_LOCK_SUPPORT.with(|support| support.get().cloned()) { + if !support { + return OrientationLockFuture::Ready(Some(Err(OrientationLockError::Unsupported))); + } + } + + self.0.queue(|inner| { + let orientation = + inner.orientation.get_or_init(|| inner.screen.orientation().unchecked_into()); + + if !HAS_LOCK_SUPPORT + .with(|support| *support.get_or_init(|| !orientation.has_lock().is_undefined())) + { + return OrientationLockFuture::Ready(Some(Err(OrientationLockError::Unsupported))); + } + + let future = JsFuture::from(orientation.lock(orientation_lock.to_js()).unwrap()); + let notifier = Notifier::new(); + let notified = notifier.notified(); + + wasm_bindgen_futures::spawn_local(async move { + notifier.notify(future.await.map(|_| ()).map_err(OrientationLockError::from_js)); + }); + + OrientationLockFuture::Future(notified) + }) } + + pub fn unlock(&self) -> Result<(), OrientationLockError> { + // Short-circuit without blocking. + if let Some(support) = HAS_LOCK_SUPPORT.with(|support| support.get().cloned()) { + if !support { + return Err(OrientationLockError::Unsupported); + } + } + + self.0.queue(|inner| { + let orientation = + inner.orientation.get_or_init(|| inner.screen.orientation().unchecked_into()); + + if !HAS_LOCK_SUPPORT + .with(|support| *support.get_or_init(|| !orientation.has_lock().is_undefined())) + { + return Err(OrientationLockError::Unsupported); + } + + orientation.unlock().map_err(OrientationLockError::from_js) + }) + } + + pub fn is_internal(&self) -> Option { + self.0.queue(|inner| { + if let Screen::Detailed(screen) = &inner.screen { + Some(screen.is_internal()) + } else { + None + } + }) + } + + pub fn is_detailed(&self) -> bool { + self.0.queue(|inner| matches!(inner.screen, Screen::Detailed(_))) + } + + pub(crate) fn detailed( + &self, + main_thread: MainThreadMarker, + ) -> Option> { + let inner = self.0.value(main_thread); + match &inner.screen { + Screen::Screen(_) => None, + Screen::Detailed(_) => Some(Ref::map(inner, |inner| { + if let Screen::Detailed(detailed) = &inner.screen { + detailed + } else { + unreachable!() + } + })), + } + } +} + +#[derive(Debug)] +pub enum OrientationLockFuture { + Future(Notified>), + Ready(Option>), } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct VideoModeHandle; +impl Future for OrientationLockFuture { + type Output = Result<(), OrientationLockError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.get_mut() { + Self::Future(notified) => Pin::new(notified).poll(cx).map(Option::unwrap), + Self::Ready(result) => { + Poll::Ready(result.take().expect("`OrientationLockFuture` polled after completion")) + }, + } + } +} + +impl OrientationLock { + fn to_js(self) -> OrientationLockType { + match self { + OrientationLock::Any => OrientationLockType::Any, + OrientationLock::Natural => OrientationLockType::Natural, + OrientationLock::Landscape { flipped: None } => OrientationLockType::Landscape, + OrientationLock::Landscape { flipped: Some(flipped) } => { + if flipped { + OrientationLockType::LandscapeSecondary + } else { + OrientationLockType::LandscapePrimary + } + }, + OrientationLock::Portrait { flipped: None } => OrientationLockType::Portrait, + OrientationLock::Portrait { flipped: Some(flipped) } => { + if flipped { + OrientationLockType::PortraitSecondary + } else { + OrientationLockType::PortraitPrimary + } + }, + } + } +} + +impl OrientationLockError { + fn from_js(error: JsValue) -> Self { + debug_assert!(error.has_type::()); + let error: DomException = error.unchecked_into(); + + if let DomException::ABORT_ERR = error.code() { + OrientationLockError::Busy + } else { + OrientationLockError::Unsupported + } + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VideoModeHandle(pub(super) MonitorHandle); impl VideoModeHandle { pub fn size(&self) -> PhysicalSize { - unreachable!(); + self.0.size() } pub fn bit_depth(&self) -> u16 { - unreachable!(); + self.0 .0.queue(|inner| inner.screen.color_depth().unwrap()).try_into().unwrap() } pub fn refresh_rate_millihertz(&self) -> u32 { - unreachable!(); + 0 } pub fn monitor(&self) -> MonitorHandle { - unreachable!(); + self.0.clone() + } +} + +struct Inner { + window: WindowExt, + engine: Option, + screen: Screen, + orientation: OnceCell, +} + +impl Inner { + fn new(window: WindowExt, engine: Option, screen: Screen) -> Self { + Self { window, engine, screen, orientation: OnceCell::new() } + } +} + +enum Screen { + Screen(ScreenExt), + Detailed(ScreenDetailed), +} + +impl Deref for Screen { + type Target = ScreenExt; + + fn deref(&self) -> &Self::Target { + match self { + Screen::Screen(screen) => screen, + Screen::Detailed(screen) => screen, + } + } +} + +pub struct MonitorHandler { + state: RefCell, + main_thread: MainThreadMarker, + window: WindowExt, + engine: Option, + screen: ScreenExt, +} + +enum State { + Unsupported, + Initialize(Notified>), + Permission { permission: PermissionStatusExt, _handle: EventListenerHandle }, + Upgrade(Notified>), + Detailed(ScreenDetails), +} + +impl MonitorHandler { + pub fn new( + main_thread: MainThreadMarker, + window: Window, + navigator: &Navigator, + runner: WeakShared, + ) -> Self { + let window: WindowExt = window.unchecked_into(); + let engine = super::web_sys::engine(navigator); + let screen: ScreenExt = window.screen().unwrap().unchecked_into(); + + let state = if has_screen_details_support(&window) { + let permissions = navigator.permissions().expect( + "expected the Permissions API to be implemented if the Window Management API is \ + as well", + ); + let descriptor: PermissionDescriptor = Object::new().unchecked_into(); + descriptor.set_name("window-management"); + let future = JsFuture::from(permissions.query(&descriptor).unwrap()); + + let window = window.clone(); + let notifier = Notifier::new(); + let notified = notifier.notified(); + wasm_bindgen_futures::spawn_local(async move { + let permission: PermissionStatusExt = match future.await { + Ok(permission) => permission.unchecked_into(), + Err(error) => unreachable_error( + &error, + "retrieving permission for Window Management API failed even though its \ + implemented", + ), + }; + + let screen_details = match permission.state() { + PermissionState::Granted => { + let screen_details = match JsFuture::from(window.screen_details()).await { + Ok(screen_details) => screen_details.unchecked_into(), + Err(error) => unreachable_error( + &error, + "getting screen details failed even though permission was granted", + ), + }; + notifier.notify(Ok(())); + Some(screen_details) + }, + PermissionState::Denied => { + notifier.notify(Err(MonitorPermissionError::Denied)); + None + }, + PermissionState::Prompt => { + notifier.notify(Err(MonitorPermissionError::Prompt)); + None + }, + _ => { + error!( + "encountered unknown permission state: {}", + permission.state_string() + ); + notifier.notify(Err(MonitorPermissionError::Denied)); + None + }, + }; + + // Notifying `Future`s is not dependant on the lifetime of the runner, + // because they can outlive it. + if let Some(runner) = runner.upgrade() { + let state = if let Some(screen_details) = screen_details { + State::Detailed(screen_details) + } else { + // If permission is denied we listen for changes so we can catch external + // permission granting. + let handle = EventListenerHandle::new( + permission.clone(), + "change", + Closure::new({ + let runner = runner.weak(); + let permission = permission.clone(); + move || { + if let PermissionState::Granted = permission.state() { + let future = JsFuture::from(window.screen_details()); + + let runner = runner.clone(); + wasm_bindgen_futures::spawn_local(async move { + let screen_details = match future.await { + Ok(screen_details) => { + screen_details.unchecked_into() + }, + Err(error) => unreachable_error( + &error, + "getting screen details failed even though \ + permission was granted", + ), + }; + + if let Some(runner) = runner.upgrade() { + // We drop the event listener handle here, which + // doesn't drop it while we are running it, because + // we are in a `spawn_local()` context. + *runner.monitor().state.borrow_mut() = + State::Detailed(screen_details); + } + }); + } + } + }), + ); + State::Permission { permission, _handle: handle } + }; + + *runner.monitor().state.borrow_mut() = state; + runner.start_delayed(); + } + }); + + State::Initialize(notified) + } else { + State::Unsupported + }; + + Self { state: RefCell::new(state), main_thread, window, engine, screen } + } + + pub fn is_extended(&self) -> Option { + self.screen.is_extended() + } + + pub fn is_initializing(&self) -> bool { + matches!(self.state.borrow().deref(), State::Initialize(_)) } + + pub fn current_monitor(&self) -> MonitorHandle { + if let State::Detailed(details) = self.state.borrow().deref() { + MonitorHandle::new( + self.main_thread, + Inner::new( + self.window.clone(), + self.engine, + Screen::Detailed(details.current_screen()), + ), + ) + } else { + MonitorHandle::new( + self.main_thread, + Inner::new(self.window.clone(), self.engine, Screen::Screen(self.screen.clone())), + ) + } + } + + // Note: We have to return a `Vec` here because the iterator is otherwise not `Send` + `Sync`. + pub fn available_monitors(&self) -> Vec { + if let State::Detailed(details) = self.state.borrow().deref() { + details + .screens() + .into_iter() + .map(move |screen| { + MonitorHandle::new( + self.main_thread, + Inner::new(self.window.clone(), self.engine, Screen::Detailed(screen)), + ) + }) + .collect() + } else { + vec![self.current_monitor()] + } + } + + pub fn primary_monitor(&self) -> Option { + if let State::Detailed(details) = self.state.borrow().deref() { + details.screens().into_iter().find_map(|screen| { + screen.is_primary().then(|| { + MonitorHandle::new( + self.main_thread, + Inner::new(self.window.clone(), self.engine, Screen::Detailed(screen)), + ) + }) + }) + } else { + None + } + } + + pub(crate) fn request_detailed_monitor_permission( + &self, + shared: WeakShared, + ) -> MonitorPermissionFuture { + let state = self.state.borrow(); + let (notifier, notified) = match state.deref() { + State::Unsupported => { + return MonitorPermissionFuture::Ready(Some(Err( + MonitorPermissionError::Unsupported, + ))) + }, + State::Initialize(notified) => { + return MonitorPermissionFuture::Initialize { + runner: Dispatcher::new(self.main_thread, (shared, self.window.clone())).0, + notified: notified.clone(), + } + }, + State::Permission { permission, .. } => { + match permission.state() { + PermissionState::Granted | PermissionState::Prompt => (), + PermissionState::Denied => { + return MonitorPermissionFuture::Ready(Some(Err( + MonitorPermissionError::Denied, + ))) + }, + _ => { + error!( + "encountered unknown permission state: {}", + permission.state_string() + ); + + return MonitorPermissionFuture::Ready(Some(Err( + MonitorPermissionError::Denied, + ))); + }, + } + + drop(state); + + let notifier = Notifier::new(); + let notified = notifier.notified(); + *self.state.borrow_mut() = State::Upgrade(notified.clone()); + + (notifier, notified) + }, + // A request is already in progress. + State::Upgrade(notified) => return MonitorPermissionFuture::Upgrade(notified.clone()), + State::Detailed(_) => return MonitorPermissionFuture::Ready(Some(Ok(()))), + }; + + let future = JsFuture::from(self.window.screen_details()); + wasm_bindgen_futures::spawn_local(async move { + match future.await { + Ok(details) => { + // Notifying `Future`s is not dependant on the lifetime of the runner, because + // they can outlive it. + notifier.notify(Ok(())); + + if let Some(shared) = shared.upgrade() { + *shared.monitor().state.borrow_mut() = + State::Detailed(details.unchecked_into()) + } + }, + Err(error) => unreachable_error( + &error, + "getting screen details failed even though permission was granted", + ), + } + }); + + MonitorPermissionFuture::Upgrade(notified) + } + + pub fn has_detailed_monitor_permission_async(&self) -> HasMonitorPermissionFuture { + match self.state.borrow().deref() { + State::Unsupported | State::Permission { .. } | State::Upgrade(_) => { + HasMonitorPermissionFuture::Ready(Some(false)) + }, + State::Initialize(notified) => HasMonitorPermissionFuture::Future(notified.clone()), + State::Detailed(_) => HasMonitorPermissionFuture::Ready(Some(true)), + } + } + + pub fn has_detailed_monitor_permission(&self) -> bool { + match self.state.borrow().deref() { + State::Unsupported | State::Permission { .. } | State::Upgrade(_) => false, + State::Initialize(_) => { + unreachable!("called `has_detailed_monitor_permission()` while initializing") + }, + State::Detailed(_) => true, + } + } +} + +#[derive(Debug)] +pub(crate) enum MonitorPermissionFuture { + Initialize { + runner: Dispatcher<(WeakShared, WindowExt)>, + notified: Notified>, + }, + Upgrade(Notified>), + Ready(Option>), +} + +impl Future for MonitorPermissionFuture { + type Output = Result<(), MonitorPermissionError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + match this { + Self::Initialize { notified, .. } => { + if let Err(error) = ready!(Pin::new(notified).poll(cx).map(Option::unwrap)) { + match error { + MonitorPermissionError::Denied | MonitorPermissionError::Unsupported => { + Poll::Ready(Err(error)) + }, + MonitorPermissionError::Prompt => { + let notifier = Notifier::new(); + let notified = notifier.notified(); + let Self::Initialize { runner, .. } = + mem::replace(this, Self::Upgrade(notified.clone())) + else { + unreachable!() + }; + + runner.queue(|(shared, window)| { + let future = JsFuture::from(window.screen_details()); + + if let Some(shared) = shared.upgrade() { + *shared.monitor().state.borrow_mut() = State::Upgrade(notified); + } + + let shared = shared.clone(); + wasm_bindgen_futures::spawn_local(async move { + match future.await { + Ok(details) => { + // Notifying `Future`s is not dependant on the lifetime + // of + // the runner, because + // they can outlive it. + notifier.notify(Ok(())); + + if let Some(shared) = shared.upgrade() { + *shared.monitor().state.borrow_mut() = + State::Detailed(details.unchecked_into()) + } + }, + Err(error) => unreachable_error( + &error, + "getting screen details failed even though permission \ + was granted", + ), + } + }); + + Poll::Pending + }) + }, + } + } else { + Poll::Ready(Ok(())) + } + }, + Self::Upgrade(notified) => Pin::new(notified).poll(cx).map(Option::unwrap), + Self::Ready(result) => Poll::Ready( + result.take().expect("`MonitorPermissionFuture` polled after completion"), + ), + } + } +} + +#[derive(Debug)] +pub enum HasMonitorPermissionFuture { + Future(Notified>), + Ready(Option), +} + +impl Future for HasMonitorPermissionFuture { + type Output = bool; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.get_mut() { + Self::Future(notified) => { + Pin::new(notified).poll(cx).map(Option::unwrap).map(|result| result.is_ok()) + }, + Self::Ready(result) => Poll::Ready( + result.take().expect("`MonitorPermissionFuture` polled after completion"), + ), + } + } +} + +#[track_caller] +fn unreachable_error(error: &JsValue, message: &str) -> ! { + if let Some(error) = error.dyn_ref::() { + unreachable!("{message}. {}: {}", error.name(), error.message()); + } else { + console::error_1(error); + unreachable!("{message}"); + } +} + +thread_local! { + static HAS_LOCK_SUPPORT: OnceCell = const { OnceCell::new() }; +} + +pub fn has_screen_details_support(window: &Window) -> bool { + thread_local! { + static HAS_SCREEN_DETAILS: OnceCell = const { OnceCell::new() }; + } + + HAS_SCREEN_DETAILS.with(|support| { + *support.get_or_init(|| { + let window: &WindowExt = window.unchecked_ref(); + !window.has_screen_details().is_undefined() + }) + }) +} + +#[wasm_bindgen] +extern "C" { + #[derive(Clone)] + #[wasm_bindgen(extends = Window)] + pub(crate) type WindowExt; + + #[wasm_bindgen(method, getter, js_name = getScreenDetails)] + fn has_screen_details(this: &WindowExt) -> JsValue; + + #[wasm_bindgen(method, js_name = getScreenDetails)] + fn screen_details(this: &WindowExt) -> Promise; + + type ScreenDetails; + + #[wasm_bindgen(method, getter, js_name = currentScreen)] + fn current_screen(this: &ScreenDetails) -> ScreenDetailed; + + #[wasm_bindgen(method, getter)] + fn screens(this: &ScreenDetails) -> Vec; + + #[derive(Clone)] + #[wasm_bindgen(extends = web_sys::Screen)] + pub(crate) type ScreenExt; + + #[wasm_bindgen(method, getter, js_name = isExtended)] + fn is_extended(this: &ScreenExt) -> Option; + + #[wasm_bindgen(extends = ScreenOrientation)] + type ScreenOrientationExt; + + #[wasm_bindgen(method, getter, js_name = type)] + fn type_string(this: &ScreenOrientationExt) -> String; + + #[wasm_bindgen(method, getter, js_name = lock)] + fn has_lock(this: &ScreenOrientationExt) -> JsValue; + + #[wasm_bindgen(extends = ScreenExt)] + pub(crate) type ScreenDetailed; + + #[wasm_bindgen(method, getter, js_name = devicePixelRatio)] + fn device_pixel_ratio(this: &ScreenDetailed) -> f64; + + #[wasm_bindgen(method, getter, js_name = isInternal)] + fn is_internal(this: &ScreenDetailed) -> bool; + + #[wasm_bindgen(method, getter, js_name = isPrimary)] + fn is_primary(this: &ScreenDetailed) -> bool; + + #[wasm_bindgen(method, getter)] + fn label(this: &ScreenDetailed) -> String; + + #[wasm_bindgen(method, getter)] + fn left(this: &ScreenDetailed) -> i32; + + #[wasm_bindgen(method, getter)] + fn top(this: &ScreenDetailed) -> i32; + + #[wasm_bindgen(extends = Object)] + type PermissionDescriptor; + + #[wasm_bindgen(method, setter, js_name = name)] + fn set_name(this: &PermissionDescriptor, name: &str); + + #[wasm_bindgen(extends = PermissionStatus)] + type PermissionStatusExt; + + #[wasm_bindgen(method, getter, js_name = state)] + fn state_string(this: &PermissionStatusExt) -> String; } diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index d2a9947e8b..10fd654723 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -7,7 +7,7 @@ use smol_str::SmolStr; use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; use web_sys::{ - CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, + CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, Navigator, PointerEvent, WheelEvent, }; @@ -24,7 +24,7 @@ use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; use crate::event::{Force, InnerSizeWriter, MouseButton, MouseScrollDelta}; use crate::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey}; -use crate::platform_impl::OsError; +use crate::platform_impl::{Fullscreen, OsError}; use crate::window::{WindowAttributes, WindowId as RootWindowId}; #[allow(dead_code)] @@ -57,6 +57,7 @@ struct Handlers { pub struct Common { pub window: web_sys::Window, + navigator: Navigator, pub document: Document, /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure /// the DPI factor is maintained. Note: this is read-only because we use a pointer to this @@ -78,6 +79,7 @@ impl Canvas { main_thread: MainThreadMarker, id: WindowId, window: web_sys::Window, + navigator: Navigator, document: Document, attr: WindowAttributes, ) -> Result { @@ -116,6 +118,7 @@ impl Canvas { let common = Common { window: window.clone(), document: document.clone(), + navigator, raw: Rc::new(canvas.clone()), style, old_size: Rc::default(), @@ -142,8 +145,14 @@ impl Canvas { super::set_canvas_position(&common.document, &common.raw, &common.style, position); } - if attr.fullscreen.is_some() { - fullscreen::request_fullscreen(&document, &canvas); + if let Some(fullscreen) = attr.fullscreen { + fullscreen::request_fullscreen( + main_thread, + &window, + &document, + &canvas, + fullscreen.into(), + ); } if attr.active { @@ -222,6 +231,11 @@ impl Canvas { &self.common.window } + #[inline] + pub fn navigator(&self) -> &Navigator { + &self.common.navigator + } + #[inline] pub fn document(&self) -> &Document { &self.common.document @@ -445,8 +459,14 @@ impl Canvas { })); } - pub fn request_fullscreen(&self) { - fullscreen::request_fullscreen(self.document(), self.raw()); + pub(crate) fn request_fullscreen(&self, fullscreen: Fullscreen) { + fullscreen::request_fullscreen( + self.main_thread, + self.window(), + self.document(), + self.raw(), + fullscreen, + ); } pub fn exit_fullscreen(&self) { diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs index 7d38fabdd1..46456d075c 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/event.rs @@ -4,7 +4,7 @@ use dpi::{LogicalPosition, PhysicalPosition, Position}; use smol_str::SmolStr; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; +use web_sys::{KeyboardEvent, MouseEvent, Navigator, PointerEvent, WheelEvent}; use super::Engine; use crate::event::{MouseButton, MouseScrollDelta}; @@ -108,8 +108,8 @@ pub enum MouseDelta { } impl MouseDelta { - pub fn init(window: &web_sys::Window, event: &PointerEvent) -> Self { - match super::engine(window) { + pub fn init(navigator: &Navigator, event: &PointerEvent) -> Self { + match super::engine(navigator) { Some(Engine::Chromium) => Self::Chromium, // Firefox has wrong movement values in coalesced events. Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko { diff --git a/src/platform_impl/web/web_sys/fullscreen.rs b/src/platform_impl/web/web_sys/fullscreen.rs index 867d34a623..13ffbaa43d 100644 --- a/src/platform_impl/web/web_sys/fullscreen.rs +++ b/src/platform_impl/web/web_sys/fullscreen.rs @@ -1,16 +1,23 @@ use std::cell::OnceCell; -use js_sys::Promise; +use js_sys::{Object, Promise}; +use tracing::error; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{Document, Element, HtmlCanvasElement}; - -pub fn request_fullscreen(document: &Document, canvas: &HtmlCanvasElement) { - if is_fullscreen(document, canvas) { - return; - } - +use web_sys::{console, Document, Element, HtmlCanvasElement, Window}; + +use super::super::main_thread::MainThreadMarker; +use super::super::monitor::{self, ScreenDetailed}; +use crate::platform_impl::Fullscreen; + +pub(crate) fn request_fullscreen( + main_thread: MainThreadMarker, + window: &Window, + document: &Document, + canvas: &HtmlCanvasElement, + fullscreen: Fullscreen, +) { #[wasm_bindgen] extern "C" { #[wasm_bindgen(extends = HtmlCanvasElement)] @@ -19,21 +26,65 @@ pub fn request_fullscreen(document: &Document, canvas: &HtmlCanvasElement) { #[wasm_bindgen(method, js_name = requestFullscreen)] fn request_fullscreen(this: &RequestFullscreen) -> Promise; + #[wasm_bindgen(method, js_name = requestFullscreen)] + fn request_fullscreen_with_options( + this: &RequestFullscreen, + options: &FullscreenOptions, + ) -> Promise; + #[wasm_bindgen(method, js_name = webkitRequestFullscreen)] fn webkit_request_fullscreen(this: &RequestFullscreen); + + type FullscreenOptions; + + #[wasm_bindgen(method, setter, js_name = screen)] + fn set_screen(this: &FullscreenOptions, screen: &ScreenDetailed); + } + + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|error| { + console::error_1(&error); + error!("Failed to transition to full screen mode") + }); + } + + if is_fullscreen(document, canvas) { + return; } let canvas: &RequestFullscreen = canvas.unchecked_ref(); - if has_fullscreen_api_support(canvas) { - thread_local! { - static REJECT_HANDLER: Closure = Closure::new(|_| ()); - } - REJECT_HANDLER.with(|handler| { - let _ = canvas.request_fullscreen().catch(handler); - }); - } else { - canvas.webkit_request_fullscreen(); + match fullscreen { + Fullscreen::Exclusive(_) => error!("Exclusive full screen mode is not supported"), + Fullscreen::Borderless(Some(monitor)) => { + if monitor::has_screen_details_support(window) { + if let Some(monitor) = monitor.detailed(main_thread) { + let options: FullscreenOptions = Object::new().unchecked_into(); + options.set_screen(&monitor); + REJECT_HANDLER.with(|handler| { + let _ = canvas.request_fullscreen_with_options(&options).catch(handler); + }); + } else { + error!( + "Selecting a specific screen for fullscreen mode requires a detailed \ + screen. See `MonitorHandleExtWeb::is_detailed()`." + ) + } + } else { + error!( + "Fullscreen mode selecting a specific screen is not supported by this browser" + ) + } + }, + Fullscreen::Borderless(None) => { + if has_fullscreen_api_support(canvas) { + REJECT_HANDLER.with(|handler| { + let _ = canvas.request_fullscreen().catch(handler); + }); + } else { + canvas.webkit_request_fullscreen(); + } + }, } } diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 8d1c8cb66d..e6d19077a9 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -15,9 +15,7 @@ use js_sys::Array; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsCast; -use web_sys::{ - Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState, Window, -}; +use web_sys::{Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState}; pub use self::canvas::{Canvas, Style}; pub use self::event::ButtonsState; @@ -182,15 +180,15 @@ thread_local! { static USER_AGENT_DATA: OnceCell = const { OnceCell::new() }; } -pub fn chrome_linux(window: &Window) -> bool { - USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(window)).chrome_linux) +pub fn chrome_linux(navigator: &Navigator) -> bool { + USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).chrome_linux) } -pub fn engine(window: &Window) -> Option { - USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(window)).engine) +pub fn engine(navigator: &Navigator) -> Option { + USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).engine) } -fn user_agent(window: &Window) -> UserAgentData { +fn user_agent(navigator: &Navigator) -> UserAgentData { #[wasm_bindgen] extern "C" { #[wasm_bindgen(extends = Navigator)] @@ -213,7 +211,7 @@ fn user_agent(window: &Window) -> UserAgentData { fn brand(this: &NavigatorUaBrandVersion) -> String; } - let navigator: NavigatorExt = window.navigator().unchecked_into(); + let navigator: &NavigatorExt = navigator.unchecked_ref(); if let Some(data) = navigator.user_agent_data() { let engine = 'engine: { diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 3ac87aa3ad..8dfec166aa 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -1,12 +1,11 @@ use std::cell::Ref; -use std::collections::VecDeque; use std::rc::Rc; use std::sync::Arc; use web_sys::HtmlCanvasElement; use super::main_thread::{MainThreadMarker, MainThreadSafe}; -use super::monitor::MonitorHandle; +use super::monitor::{MonitorHandle, MonitorHandler}; use super::r#async::Dispatcher; use super::{backend, lock, ActiveEventLoop, Fullscreen}; use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; @@ -24,6 +23,7 @@ pub struct Window { pub struct Inner { id: WindowId, pub window: web_sys::Window, + monitor: Rc, canvas: Rc, destroy_fn: Option>, } @@ -33,11 +33,13 @@ impl Window { let id = target.generate_id(); let window = target.runner.window(); + let navigator = target.runner.navigator(); let document = target.runner.document(); let canvas = backend::Canvas::create( target.runner.main_thread(), id, window.clone(), + navigator.clone(), document.clone(), attr, )?; @@ -48,7 +50,13 @@ impl Window { let runner = target.runner.clone(); let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); - let inner = Inner { id, window: window.clone(), canvas, destroy_fn: Some(destroy_fn) }; + let inner = Inner { + id, + window: window.clone(), + monitor: Rc::clone(target.runner.monitor()), + canvas, + destroy_fn: Some(destroy_fn), + }; let canvas = Rc::downgrade(&inner.canvas); let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner); @@ -80,7 +88,7 @@ impl Window { pub(crate) fn is_cursor_lock_raw(&self) -> bool { self.inner.queue(move |inner| { - lock::is_cursor_lock_raw(inner.canvas.window(), inner.canvas.document()) + lock::is_cursor_lock_raw(inner.canvas.navigator(), inner.canvas.document()) }) } @@ -244,7 +252,7 @@ impl Inner { match mode { CursorGrabMode::None => self.canvas.document().exit_pointer_lock(), CursorGrabMode::Locked => lock::request_pointer_lock( - self.canvas.window(), + self.canvas.navigator(), self.canvas.document(), self.canvas.raw(), ), @@ -312,8 +320,8 @@ impl Inner { #[inline] pub(crate) fn set_fullscreen(&self, fullscreen: Option) { - if fullscreen.is_some() { - self.canvas.request_fullscreen(); + if let Some(fullscreen) = fullscreen { + self.canvas.request_fullscreen(fullscreen); } else { self.canvas.exit_fullscreen() } @@ -365,17 +373,17 @@ impl Inner { #[inline] pub fn current_monitor(&self) -> Option { - None + Some(self.monitor.current_monitor()) } #[inline] - pub fn available_monitors(&self) -> VecDeque { - VecDeque::new() + pub fn available_monitors(&self) -> Vec { + self.monitor.available_monitors() } #[inline] pub fn primary_monitor(&self) -> Option { - None + self.monitor.primary_monitor() } #[inline] diff --git a/src/window.rs b/src/window.rs index d95e868065..5c1516c65b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1120,7 +1120,13 @@ impl Window { /// - **Wayland:** Does not support exclusive fullscreen mode and will no-op a request. /// - **Windows:** Screen saver is disabled in fullscreen mode. /// - **Android / Orbital:** Unsupported. - /// - **Web:** Does nothing without a [transient activation]. + /// - **Web:** Passing a [`MonitorHandle`] or [`VideoModeHandle`] that was not created with + #[cfg_attr( + any(web_platform, docsrs), + doc = " [detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]" + )] + #[cfg_attr(not(any(web_platform, docsrs)), doc = " detailed monitor permissions")] + /// or calling without a [transient activation] does nothing. /// /// [transient activation]: https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation #[inline] @@ -1593,6 +1599,17 @@ impl Window { /// This is the same as [`ActiveEventLoop::available_monitors`], and is provided for /// convenience. /// + /// + /// ## Platform-specific + /// + /// **Web:** Only returns the current monitor without + #[cfg_attr( + any(web_platform, docsrs), + doc = "[detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]." + )] + #[cfg_attr(not(any(web_platform, docsrs)), doc = "detailed monitor permissions.")] + /// + #[rustfmt::skip] /// [`ActiveEventLoop::available_monitors`]: crate::event_loop::ActiveEventLoop::available_monitors #[inline] pub fn available_monitors(&self) -> impl Iterator { @@ -1610,8 +1627,15 @@ impl Window { /// /// ## Platform-specific /// - /// **Wayland / Web:** Always returns `None`. + /// - **Wayland:** Always returns `None`. + /// - **Web:** Always returns `None` without + #[cfg_attr( + any(web_platform, docsrs), + doc = " [detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]." + )] + #[cfg_attr(not(any(web_platform, docsrs)), doc = " detailed monitor permissions.")] /// + #[rustfmt::skip] /// [`ActiveEventLoop::primary_monitor`]: crate::event_loop::ActiveEventLoop::primary_monitor #[inline] pub fn primary_monitor(&self) -> Option {