diff --git a/CHANGELOG.md b/CHANGELOG.md index d581bf7264..11dd8651db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ And please only add new entries to the top of this list, right below the `# Unre - **Breaking:** Move `Event::RedrawRequested` to `WindowEvent::RedrawRequested`. - On macOS, fix crash in `window.set_minimized(false)`. - On Web, enable event propagation and let `DeviceEvent`s appear after `WindowEvent`s. +- On Web, take all transient activations on the canvas and window into account to queue a fullscreen request. +- On Web, remove any fullscreen requests from the queue when an external fullscreen activation was detected. # 0.29.1-beta diff --git a/examples/web.rs b/examples/web.rs index b8ba1671dd..0520979bc9 100644 --- a/examples/web.rs +++ b/examples/web.rs @@ -65,7 +65,10 @@ mod wasm { use softbuffer::{Surface, SurfaceExtWeb}; use wasm_bindgen::prelude::*; - use winit::{event::Event, window::Window}; + use winit::{ + event::{Event, WindowEvent}, + window::Window, + }; #[wasm_bindgen(start)] pub fn run() { @@ -116,6 +119,10 @@ mod wasm { // So we implement this basic logging system into the page to give developers an easy alternative. // As a bonus its also kind of handy on desktop. let event = match event { + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => None, Event::WindowEvent { event, .. } => Some(format!("{event:?}")), Event::Resumed | Event::Suspended => Some(format!("{event:?}")), _ => None, diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index bea6b281ef..a9dd861f77 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -54,6 +54,7 @@ pub struct Execution { on_key_press: OnEventHandle, on_key_release: OnEventHandle, on_visibility_change: OnEventHandle, + on_touch_end: OnEventHandle, } enum RunnerEnum { @@ -167,6 +168,7 @@ impl Shared { on_key_press: RefCell::new(None), on_key_release: RefCell::new(None), on_visibility_change: RefCell::new(None), + on_touch_end: RefCell::new(None), })) } @@ -324,6 +326,8 @@ impl Shared { self.window().clone(), "pointerdown", Closure::new(move |event: PointerEvent| { + runner.transient_activation(); + if !runner.device_events() { return; } @@ -347,6 +351,8 @@ impl Shared { self.window().clone(), "pointerup", Closure::new(move |event: PointerEvent| { + runner.transient_activation(); + if !runner.device_events() { return; } @@ -370,6 +376,8 @@ impl Shared { self.window().clone(), "keydown", Closure::new(move |event: KeyboardEvent| { + runner.transient_activation(); + if !runner.device_events() { return; } @@ -428,6 +436,14 @@ impl Shared { } }), )); + let runner = self.clone(); + *self.0.on_touch_end.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "touchend", + Closure::new(move |_| { + runner.transient_activation(); + }), + )); } // Generate a strictly increasing ID @@ -736,7 +752,7 @@ impl Shared { self.0.device_events.set(allowed) } - pub fn device_events(&self) -> bool { + fn device_events(&self) -> bool { match self.0.device_events.get() { DeviceEvents::Always => true, DeviceEvents::WhenFocused => self.0.all_canvases.borrow().iter().any(|(_, canvas)| { @@ -750,6 +766,14 @@ impl Shared { } } + fn transient_activation(&self) { + self.0.all_canvases.borrow().iter().for_each(|(_, canvas)| { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().transient_activation(); + } + }); + } + pub fn event_loop_recreation(&self, allow: bool) { self.0.event_loop_recreation.set(allow) } diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index e0901a35c5..71522a1676 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -660,6 +660,8 @@ impl EventLoopWindowTarget { let runner = self.runner.clone(); canvas.on_animation_frame(move || runner.request_redraw(RootWindowId(id))); + + canvas.on_touch_end(); } pub fn available_monitors(&self) -> VecDequeIter { diff --git a/src/platform_impl/web/web_sys/animation_frame.rs b/src/platform_impl/web/web_sys/animation_frame.rs index 75a0955138..b761f9a9a7 100644 --- a/src/platform_impl/web/web_sys/animation_frame.rs +++ b/src/platform_impl/web/web_sys/animation_frame.rs @@ -49,6 +49,14 @@ impl AnimationFrameHandler { self.handle.set(Some(handle)); } + + pub fn cancel(&mut self) { + if let Some(handle) = self.handle.take() { + self.window + .cancel_animation_frame(handle) + .expect("Failed to cancel animation frame"); + } + } } impl Drop for AnimationFrameHandler { diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 2cb0ec9aea..3be75daef3 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -1,12 +1,10 @@ -use std::cell::{Cell, RefCell}; -use std::rc::Rc; +use std::cell::Cell; +use std::rc::{Rc, Weak}; use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; -use js_sys::Promise; use smol_str::SmolStr; use wasm_bindgen::{closure::Closure, JsCast}; -use wasm_bindgen_futures::JsFuture; use web_sys::{ CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent, }; @@ -21,10 +19,11 @@ use crate::window::{WindowAttributes, WindowId as RootWindowId}; use super::super::WindowId; use super::animation_frame::AnimationFrameHandler; use super::event_handle::EventListenerHandle; +use super::fullscreen::FullscreenHandler; use super::intersection_handle::IntersectionObserverHandle; use super::media_query_handle::MediaQueryListHandle; use super::pointer::PointerHandler; -use super::{event, fullscreen, ButtonsState, ResizeScaleHandle}; +use super::{event, ButtonsState, ResizeScaleHandle}; #[allow(dead_code)] pub struct Canvas { @@ -33,7 +32,6 @@ pub struct Canvas { pub has_focus: Arc, pub is_intersecting: Option, on_touch_start: Option>, - on_touch_end: Option>, on_focus: Option>, on_blur: Option>, on_keyboard_release: Option>, @@ -44,6 +42,7 @@ pub struct Canvas { on_resize_scale: Option, on_intersect: Option, animation_frame_handler: AnimationFrameHandler, + on_touch_end: Option>, } pub struct Common { @@ -54,7 +53,7 @@ pub struct Common { style: CssStyleDeclaration, old_size: Rc>>, current_size: Rc>>, - wants_fullscreen: Rc>, + fullscreen_handler: Rc, } impl Canvas { @@ -101,12 +100,12 @@ impl Canvas { let common = Common { window: window.clone(), - document, - raw: canvas, + document: document.clone(), + raw: canvas.clone(), style, old_size: Rc::default(), current_size: Rc::default(), - wants_fullscreen: Rc::new(RefCell::new(false)), + fullscreen_handler: Rc::new(FullscreenHandler::new(document.clone(), canvas.clone())), }; if let Some(size) = attr.inner_size { @@ -130,7 +129,7 @@ impl Canvas { } if attr.fullscreen.is_some() { - common.request_fullscreen(); + common.fullscreen_handler.request_fullscreen(); } if attr.active { @@ -143,7 +142,6 @@ impl Canvas { has_focus: Arc::new(AtomicBool::new(false)), is_intersecting: None, on_touch_start: None, - on_touch_end: None, on_blur: None, on_focus: None, on_keyboard_release: None, @@ -154,6 +152,7 @@ impl Canvas { on_resize_scale: None, on_intersect: None, animation_frame_handler: AnimationFrameHandler::new(window), + on_touch_end: None, }) } @@ -262,9 +261,8 @@ impl Canvas { where F: 'static + FnMut(KeyCode, Key, Option, KeyLocation, bool, ModifiersState), { - self.on_keyboard_release = Some(self.common.add_user_event( - "keyup", - move |event: KeyboardEvent| { + self.on_keyboard_release = + Some(self.common.add_event("keyup", move |event: KeyboardEvent| { if prevent_default { event.prevent_default(); } @@ -278,15 +276,14 @@ impl Canvas { event.repeat(), modifiers, ); - }, - )); + })); } pub fn on_keyboard_press(&mut self, mut handler: F, prevent_default: bool) where F: 'static + FnMut(KeyCode, Key, Option, KeyLocation, bool, ModifiersState), { - self.on_keyboard_press = Some(self.common.add_user_event( + self.on_keyboard_press = Some(self.common.add_transient_event( "keydown", move |event: KeyboardEvent| { if prevent_default { @@ -446,12 +443,20 @@ impl Canvas { self.animation_frame_handler.on_animation_frame(f) } + pub(crate) fn on_touch_end(&mut self) { + self.on_touch_end = Some(self.common.add_transient_event("touchend", |_| {})); + } + pub fn request_fullscreen(&self) { - self.common.request_fullscreen() + self.common.fullscreen_handler.request_fullscreen() + } + + pub fn exit_fullscreen(&self) { + self.common.fullscreen_handler.exit_fullscreen() } pub fn is_fullscreen(&self) -> bool { - self.common.is_fullscreen() + self.common.fullscreen_handler.is_fullscreen() } pub fn request_animation_frame(&self) { @@ -502,7 +507,12 @@ impl Canvas { } } + pub(crate) fn transient_activation(&self) { + self.common.fullscreen_handler.transient_activation() + } + pub fn remove_listeners(&mut self) { + self.on_touch_start = None; self.on_focus = None; self.on_blur = None; self.on_keyboard_release = None; @@ -512,6 +522,9 @@ impl Canvas { self.pointer_handler.remove_listeners(); self.on_resize_scale = None; self.on_intersect = None; + self.animation_frame_handler.cancel(); + self.on_touch_end = None; + self.common.fullscreen_handler.cancel(); } } @@ -531,7 +544,7 @@ impl Common { // The difference between add_event and add_user_event is that the latter has a special meaning // for browser security. A user event is a deliberate action by the user (like a mouse or key // press) and is the only time things like a fullscreen request may be successfully completed.) - pub fn add_user_event( + pub fn add_transient_event( &self, event_name: &'static str, mut handler: F, @@ -540,37 +553,14 @@ impl Common { E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, F: 'static + FnMut(E), { - let wants_fullscreen = self.wants_fullscreen.clone(); - let canvas = self.raw.clone(); + let fullscreen_handler = Rc::downgrade(&self.fullscreen_handler); self.add_event(event_name, move |event: E| { handler(event); - if *wants_fullscreen.borrow() { - fullscreen::request_fullscreen(&canvas).expect("Failed to enter fullscreen"); - *wants_fullscreen.borrow_mut() = false; + if let Some(fullscreen_handler) = Weak::upgrade(&fullscreen_handler) { + fullscreen_handler.transient_activation() } }) } - - pub fn request_fullscreen(&self) { - // This should return a `Promise`, but Safari v<16.4 is not up-to-date with the spec. - match fullscreen::request_fullscreen(&self.raw) { - Ok(value) if !value.is_undefined() => { - let promise: Promise = value.unchecked_into(); - let wants_fullscreen = self.wants_fullscreen.clone(); - wasm_bindgen_futures::spawn_local(async move { - if JsFuture::from(promise).await.is_err() { - *wants_fullscreen.borrow_mut() = true - } - }); - } - // We are on Safari v<16.4, let's try again on the next transient activation. - _ => *self.wants_fullscreen.borrow_mut() = true, - } - } - - pub fn is_fullscreen(&self) -> bool { - super::is_fullscreen(&self.document, &self.raw) - } } diff --git a/src/platform_impl/web/web_sys/fullscreen.rs b/src/platform_impl/web/web_sys/fullscreen.rs index 00e2df6a13..7a5b4f5aad 100644 --- a/src/platform_impl/web/web_sys/fullscreen.rs +++ b/src/platform_impl/web/web_sys/fullscreen.rs @@ -1,99 +1,157 @@ -use once_cell::unsync::OnceCell; +use std::cell::Cell; +use std::rc::Rc; + +use js_sys::Promise; +use once_cell::unsync::{Lazy, OnceCell}; +use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{Document, Element, HtmlCanvasElement}; +use super::EventListenerHandle; + thread_local! { static FULLSCREEN_API_SUPPORT: OnceCell = OnceCell::new(); } -fn canvas_has_fullscreen_api_support(canvas: &HtmlCanvasElement) -> bool { - FULLSCREEN_API_SUPPORT.with(|support| { - *support.get_or_init(|| { - #[wasm_bindgen] - extern "C" { - type CanvasFullScreenApiSupport; - - #[wasm_bindgen(method, getter, js_name = requestFullscreen)] - fn has_request_fullscreen(this: &CanvasFullScreenApiSupport) -> JsValue; - } - - let support: &CanvasFullScreenApiSupport = canvas.unchecked_ref(); - !support.has_request_fullscreen().is_undefined() - }) - }) +pub struct FullscreenHandler { + document: Document, + canvas: HtmlCanvasElement, + fullscreen_requested: Rc>, + _fullscreen_change: EventListenerHandle, } -fn document_has_fullscreen_api_support(document: &Document) -> bool { - FULLSCREEN_API_SUPPORT.with(|support| { - *support.get_or_init(|| { - #[wasm_bindgen] - extern "C" { - type DocumentFullScreenApiSupport; +impl FullscreenHandler { + pub fn new(document: Document, canvas: HtmlCanvasElement) -> Self { + let fullscreen_requested = Rc::new(Cell::new(false)); + let fullscreen_change = EventListenerHandle::new( + canvas.clone(), + if has_fullscreen_api_support(&canvas) { + "fullscreenchange" + } else { + "webkitfullscreenchange" + }, + Closure::new({ + let fullscreen_requested = fullscreen_requested.clone(); + move || { + // It doesn't matter if the canvas entered or exitted fullscreen mode, + // we don't want to request it again later. + fullscreen_requested.set(false); + } + }), + ); + + Self { + document, + canvas, + fullscreen_requested, + _fullscreen_change: fullscreen_change, + } + } - #[wasm_bindgen(method, getter, js_name = exitFullscreen)] - fn has_exit_fullscreen(this: &DocumentFullScreenApiSupport) -> JsValue; - } + fn internal_request_fullscreen(&self) { + #[wasm_bindgen] + extern "C" { + type RequestFullscreen; - let support: &DocumentFullScreenApiSupport = document.unchecked_ref(); - !support.has_exit_fullscreen().is_undefined() - }) - }) -} + #[wasm_bindgen(method, js_name = requestFullscreen)] + fn request_fullscreen(this: &RequestFullscreen) -> Promise; -pub fn request_fullscreen(canvas: &HtmlCanvasElement) -> Result { - #[wasm_bindgen] - extern "C" { - type RequestFullscreen; + #[wasm_bindgen(method, js_name = webkitRequestFullscreen)] + fn webkit_request_fullscreen(this: &RequestFullscreen); + } - #[wasm_bindgen(catch, method, js_name = requestFullscreen)] - fn request_fullscreen(this: &RequestFullscreen) -> Result; + let canvas: &RequestFullscreen = self.canvas.unchecked_ref(); - #[wasm_bindgen(catch, method, js_name = webkitRequestFullscreen)] - fn webkit_request_fullscreen(this: &RequestFullscreen) -> Result; + if has_fullscreen_api_support(&self.canvas) { + thread_local! { + static REJECT_HANDLER: Lazy> = Lazy::new(|| Closure::new(|_| ())); + } + REJECT_HANDLER.with(|handler| { + let _ = canvas.request_fullscreen().catch(handler); + }); + } else { + canvas.webkit_request_fullscreen(); + } } - let element: &RequestFullscreen = canvas.unchecked_ref(); + pub fn request_fullscreen(&self) { + if !self.is_fullscreen() { + self.internal_request_fullscreen(); + self.fullscreen_requested.set(true); + } + } - if canvas_has_fullscreen_api_support(canvas) { - element.request_fullscreen() - } else { - element.webkit_request_fullscreen() + pub fn transient_activation(&self) { + if self.fullscreen_requested.get() { + self.internal_request_fullscreen() + } } -} -pub fn exit_fullscreen(document: &Document) { - #[wasm_bindgen] - extern "C" { - type ExitFullscreen; + pub fn is_fullscreen(&self) -> bool { + #[wasm_bindgen] + extern "C" { + type FullscreenElement; + + #[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)] + fn webkit_fullscreen_element(this: &FullscreenElement) -> Option; + } + + let element = if has_fullscreen_api_support(&self.canvas) { + #[allow(clippy::disallowed_methods)] + self.document.fullscreen_element() + } else { + let document: &FullscreenElement = self.document.unchecked_ref(); + document.webkit_fullscreen_element() + }; + + match element { + Some(element) => { + let canvas: &Element = &self.canvas; + canvas == &element + } + None => false, + } + } + + pub fn exit_fullscreen(&self) { + #[wasm_bindgen] + extern "C" { + type ExitFullscreen; + + #[wasm_bindgen(method, js_name = webkitExitFullscreen)] + fn webkit_exit_fullscreen(this: &ExitFullscreen); + } + + if has_fullscreen_api_support(&self.canvas) { + #[allow(clippy::disallowed_methods)] + self.document.exit_fullscreen() + } else { + let document: &ExitFullscreen = self.document.unchecked_ref(); + document.webkit_exit_fullscreen() + } - #[wasm_bindgen(method, js_name = webkitExitFullscreen)] - fn webkit_exit_fullscreen(this: &ExitFullscreen); + self.fullscreen_requested.set(false); } - if document_has_fullscreen_api_support(document) { - #[allow(clippy::disallowed_methods)] - document.exit_fullscreen() - } else { - let document: &ExitFullscreen = document.unchecked_ref(); - document.webkit_exit_fullscreen() + pub fn cancel(&self) { + self.fullscreen_requested.set(false); } } -pub fn fullscreen_element(document: &Document) -> Option { - #[wasm_bindgen] - extern "C" { - type FullscreenElement; +fn has_fullscreen_api_support(canvas: &HtmlCanvasElement) -> bool { + FULLSCREEN_API_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type CanvasFullScreenApiSupport; - #[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)] - fn webkit_fullscreen_element(this: &FullscreenElement) -> Option; - } + #[wasm_bindgen(method, getter, js_name = requestFullscreen)] + fn has_request_fullscreen(this: &CanvasFullScreenApiSupport) -> JsValue; + } - if document_has_fullscreen_api_support(document) { - #[allow(clippy::disallowed_methods)] - document.fullscreen_element() - } else { - let document: &FullscreenElement = document.unchecked_ref(); - document.webkit_fullscreen_element() - } + let support: &CanvasFullScreenApiSupport = canvas.unchecked_ref(); + !support.has_request_fullscreen().is_undefined() + }) + }) } diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 3d961e4897..45dd7bf487 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -18,17 +18,13 @@ pub use self::schedule::Schedule; use crate::dpi::{LogicalPosition, LogicalSize}; use wasm_bindgen::closure::Closure; use web_sys::{ - CssStyleDeclaration, Document, Element, HtmlCanvasElement, PageTransitionEvent, VisibilityState, + CssStyleDeclaration, Document, HtmlCanvasElement, PageTransitionEvent, VisibilityState, }; pub fn throw(msg: &str) { wasm_bindgen::throw_str(msg); } -pub fn exit_fullscreen(document: &Document) { - fullscreen::exit_fullscreen(document); -} - pub struct PageTransitionEventHandle { _show_listener: event_handle::EventListenerHandle, _hide_listener: event_handle::EventListenerHandle, @@ -175,16 +171,6 @@ pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: .unwrap_or_else(|err| panic!("error: {err:?}\nFailed to set {property}")) } -pub fn is_fullscreen(document: &Document, canvas: &HtmlCanvasElement) -> bool { - match fullscreen::fullscreen_element(document) { - Some(elem) => { - let canvas: &Element = canvas; - canvas == &elem - } - None => false, - } -} - pub fn is_dark_mode(window: &web_sys::Window) -> Option { window .match_media("(prefers-color-scheme: dark)") diff --git a/src/platform_impl/web/web_sys/pointer.rs b/src/platform_impl/web/web_sys/pointer.rs index b0c52b58c2..4605d0aca0 100644 --- a/src/platform_impl/web/web_sys/pointer.rs +++ b/src/platform_impl/web/web_sys/pointer.rs @@ -80,7 +80,7 @@ impl PointerHandler { T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), { let window = canvas_common.window.clone(); - self.on_pointer_release = Some(canvas_common.add_user_event( + self.on_pointer_release = Some(canvas_common.add_transient_event( "pointerup", move |event: PointerEvent| { let modifiers = event::mouse_modifiers(&event); @@ -118,7 +118,7 @@ impl PointerHandler { { let window = canvas_common.window.clone(); let canvas = canvas_common.raw.clone(); - self.on_pointer_press = Some(canvas_common.add_user_event( + self.on_pointer_press = Some(canvas_common.add_transient_event( "pointerdown", move |event: PointerEvent| { if prevent_default { diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index dc9768ec6c..97fa7ef260 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -7,7 +7,7 @@ use crate::window::{ }; use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle}; -use web_sys::{Document, HtmlCanvasElement}; +use web_sys::HtmlCanvasElement; use super::r#async::Dispatcher; use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen}; @@ -25,7 +25,6 @@ pub struct Window { pub struct Inner { id: WindowId, pub window: web_sys::Window, - document: Document, canvas: Rc>, previous_pointer: RefCell<&'static str>, destroy_fn: Option>, @@ -57,7 +56,6 @@ impl Window { let inner = Inner { id, window: window.clone(), - document: document.clone(), canvas, previous_pointer: RefCell::new("auto"), destroy_fn: Some(destroy_fn), @@ -282,10 +280,12 @@ impl Inner { #[inline] pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + let canvas = &self.canvas.borrow(); + if fullscreen.is_some() { - self.canvas.borrow().request_fullscreen(); - } else if self.canvas.borrow().is_fullscreen() { - backend::exit_fullscreen(&self.document); + canvas.request_fullscreen(); + } else { + canvas.exit_fullscreen() } }