diff --git a/CHANGELOG.md b/CHANGELOG.md index 76cb48e3..5625e22c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On X11, fix false positive flagging of key repeats when pressing different keys with no release between presses. - Implement `PartialOrd` and `Ord` for `KeyCode` and `NativeKeyCode`. +- On Web, implement `WindowEvent::Occluded`. # 0.29.0-beta.0 diff --git a/Cargo.toml b/Cargo.toml index 3d13d549..46b3fcf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,6 +145,8 @@ features = [ 'FocusEvent', 'HtmlCanvasElement', 'HtmlElement', + 'IntersectionObserver', + 'IntersectionObserverEntry', 'KeyboardEvent', 'MediaQueryList', 'Node', @@ -155,6 +157,7 @@ features = [ 'ResizeObserverEntry', 'ResizeObserverOptions', 'ResizeObserverSize', + 'VisibilityState', 'Window', 'WheelEvent' ] diff --git a/examples/web.rs b/examples/web.rs index c1df6987..cda83b86 100644 --- a/examples/web.rs +++ b/examples/web.rs @@ -58,6 +58,9 @@ pub fn main() { #[cfg(wasm_platform)] mod wasm { + use std::num::NonZeroU32; + + use softbuffer::{Surface, SurfaceExtWeb}; use wasm_bindgen::prelude::*; use winit::{event::Event, window::Window}; @@ -73,16 +76,26 @@ mod wasm { use winit::platform::web::WindowExtWebSys; let canvas = window.canvas().unwrap(); + let mut surface = Surface::from_canvas(canvas.clone()).unwrap(); + surface + .resize( + NonZeroU32::new(canvas.width()).unwrap(), + NonZeroU32::new(canvas.height()).unwrap(), + ) + .unwrap(); + let mut buffer = surface.buffer_mut().unwrap(); + buffer.fill(0xFFF0000); + buffer.present().unwrap(); let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let body = document.body().unwrap(); - // Set a background color for the canvas to make it easier to tell where the canvas is for debugging purposes. - canvas - .style() - .set_property("background-color", "crimson") - .unwrap(); + let style = &canvas.style(); + style.set_property("margin", "50px").unwrap(); + // Use to test interactions with border and padding. + //style.set_property("border", "50px solid black").unwrap(); + //style.set_property("padding", "50px").unwrap(); body.append_child(&canvas).unwrap(); let log_header = document.create_element("h2").unwrap(); @@ -100,11 +113,25 @@ mod wasm { // Getting access to browser logs requires a lot of setup on mobile devices. // 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. - if let Event::WindowEvent { event, .. } = &event { + let event = match event { + Event::WindowEvent { event, .. } => Some(format!("{event:?}")), + Event::Resumed | Event::Suspended => Some(format!("{event:?}")), + _ => None, + }; + if let Some(event) = event { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let log = document.create_element("li").unwrap(); - log.set_text_content(Some(&format!("{event:?}"))); + + let date = js_sys::Date::new_0(); + log.set_text_content(Some(&format!( + "{:02}:{:02}:{:02}.{:03}: {event}", + date.get_hours(), + date.get_minutes(), + date.get_seconds(), + date.get_milliseconds(), + ))); + log_list .insert_before(&log, log_list.first_child().as_ref()) .unwrap(); diff --git a/src/event.rs b/src/event.rs index 8ee4e452..8d06cd7f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -565,7 +565,12 @@ pub enum WindowEvent<'a> { /// minimised, set invisible, or fully occluded by another window. /// /// Platform-specific behavior: - /// - **iOS / Android / Web / Wayland / Windows / Orbital:** Unsupported. + /// + /// - **Web:** Doesn't take into account CSS [`border`] or [`padding`]. + /// - **iOS / Android / Wayland / Windows / Orbital:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding Occluded(bool), } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index a47dc8c5..ac72dc6e 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -3,6 +3,7 @@ use super::{backend, state::State}; use crate::dpi::PhysicalSize; use crate::event::{ DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, RawKeyEvent, StartCause, + WindowEvent, }; use crate::event_loop::{ControlFlow, DeviceEvents}; use crate::platform_impl::platform::backend::EventListenerHandle; @@ -35,6 +36,7 @@ type OnEventHandle = RefCell>>; pub struct Execution { runner: RefCell>, + suspended: Cell, event_loop_recreation: Cell, events: RefCell>>, id: RefCell, @@ -50,6 +52,7 @@ pub struct Execution { on_mouse_release: OnEventHandle, on_key_press: OnEventHandle, on_key_release: OnEventHandle, + on_visibility_change: OnEventHandle, } enum RunnerEnum { @@ -140,6 +143,7 @@ impl Shared { pub fn new() -> Self { Shared(Rc::new(Execution { runner: RefCell::new(RunnerEnum::Pending), + suspended: Cell::new(false), event_loop_recreation: Cell::new(false), events: RefCell::new(VecDeque::new()), #[allow(clippy::disallowed_methods)] @@ -156,6 +160,7 @@ impl Shared { on_mouse_release: RefCell::new(None), on_key_press: RefCell::new(None), on_key_release: RefCell::new(None), + on_visibility_change: RefCell::new(None), })) } @@ -191,6 +196,7 @@ impl Shared { let runner = self.clone(); move |event: PageTransitionEvent| { if event.persisted() { + runner.0.suspended.set(false); runner.send_event(Event::Resumed); } } @@ -198,6 +204,7 @@ impl Shared { { let runner = self.clone(); move |event: PageTransitionEvent| { + runner.0.suspended.set(true); if event.persisted() { runner.send_event(Event::Suspended); } else { @@ -384,6 +391,28 @@ impl Shared { }); }), )); + let runner = self.clone(); + *self.0.on_visibility_change.borrow_mut() = Some(EventListenerHandle::new( + // Safari <14 doesn't support the `visibilitychange` event on `Window`. + &self.window().document().expect("Failed to obtain document"), + "visibilitychange", + Closure::new(move |_| { + if !runner.0.suspended.get() { + for (id, canvas) in &*runner.0.all_canvases.borrow() { + if let Some(canvas) = canvas.upgrade() { + if backend::is_intersecting(runner.window(), canvas.borrow().raw()) { + runner.send_event(Event::WindowEvent { + window_id: *id, + event: WindowEvent::Occluded(!backend::is_visible( + runner.window(), + )), + }); + } + } + } + } + }), + )); } // Generate a strictly increasing ID @@ -630,6 +659,7 @@ impl Shared { *self.0.on_mouse_release.borrow_mut() = None; *self.0.on_key_press.borrow_mut() = None; *self.0.on_key_release.borrow_mut() = None; + *self.0.on_visibility_change.borrow_mut() = None; // Dropping the `Runner` drops the event handler closure, which will in // turn drop all `Window`s moved into the closure. *self.0.runner.borrow_mut() = RunnerEnum::Destroyed; diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 7534aa7b..fbb686d4 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -720,6 +720,16 @@ impl EventLoopWindowTarget { } }, ); + + let runner = self.runner.clone(); + canvas.on_intersection(move |is_intersecting| { + if backend::is_visible(runner.window()) { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Occluded(!is_intersecting), + }); + } + }) } pub fn available_monitors(&self) -> VecDequeIter { diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 67a383e8..4b8a2200 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -1,5 +1,6 @@ use super::super::WindowId; use super::event_handle::EventListenerHandle; +use super::intersection_handle::IntersectionObserverHandle; use super::media_query_handle::MediaQueryListHandle; use super::pointer::PointerHandler; use super::{event, ButtonsState, ResizeScaleHandle}; @@ -37,6 +38,7 @@ pub struct Canvas { on_dark_mode: Option, pointer_handler: PointerHandler, on_resize_scale: Option, + on_intersect: Option, } pub struct Common { @@ -105,6 +107,7 @@ impl Canvas { on_dark_mode: None, pointer_handler: PointerHandler::new(), on_resize_scale: None, + on_intersect: None, }) } @@ -365,6 +368,13 @@ impl Canvas { )); } + pub(crate) fn on_intersection(&mut self, handler: F) + where + F: 'static + FnMut(bool), + { + self.on_intersect = Some(IntersectionObserverHandle::new(self.raw(), handler)); + } + pub fn request_fullscreen(&self) { self.common.request_fullscreen() } @@ -421,6 +431,7 @@ impl Canvas { self.on_dark_mode = None; self.pointer_handler.remove_listeners(); self.on_resize_scale = None; + self.on_intersect = None; } } diff --git a/src/platform_impl/web/web_sys/intersection_handle.rs b/src/platform_impl/web/web_sys/intersection_handle.rs new file mode 100644 index 00000000..2b694a56 --- /dev/null +++ b/src/platform_impl/web/web_sys/intersection_handle.rs @@ -0,0 +1,45 @@ +use js_sys::Array; +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::{Element, IntersectionObserver, IntersectionObserverEntry}; + +pub(super) struct IntersectionObserverHandle { + observer: IntersectionObserver, + _closure: Closure, +} + +impl IntersectionObserverHandle { + pub fn new(element: &Element, mut callback: F) -> Self + where + F: 'static + FnMut(bool), + { + let mut skip = true; + let closure = Closure::new(move |entries: Array| { + let entry: IntersectionObserverEntry = entries.get(0).unchecked_into(); + + let is_intersecting = entry.is_intersecting(); + + // skip first intersection + if skip && is_intersecting { + skip = false; + return; + } + + callback(is_intersecting); + }); + let observer = IntersectionObserver::new(closure.as_ref().unchecked_ref()) + // we don't provide any `options` + .expect("Invalid `options`"); + observer.observe(element); + + Self { + observer, + _closure: closure, + } + } +} + +impl Drop for IntersectionObserverHandle { + fn drop(&mut self) { + self.observer.disconnect() + } +} diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 92d7fd42..882ff095 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -1,6 +1,7 @@ mod canvas; pub mod event; mod event_handle; +mod intersection_handle; mod media_query_handle; mod pointer; mod resize_scaling; @@ -16,7 +17,9 @@ use crate::dpi::LogicalSize; use crate::platform::web::WindowExtWebSys; use crate::window::Window; use wasm_bindgen::closure::Closure; -use web_sys::{CssStyleDeclaration, Element, HtmlCanvasElement, PageTransitionEvent}; +use web_sys::{ + CssStyleDeclaration, Element, HtmlCanvasElement, PageTransitionEvent, VisibilityState, +}; pub fn throw(msg: &str) { wasm_bindgen::throw_str(msg); @@ -136,4 +139,25 @@ pub fn is_dark_mode(window: &web_sys::Window) -> Option { .map(|media| media.matches()) } +pub fn is_visible(window: &web_sys::Window) -> bool { + let document = window.document().expect("Failed to obtain document"); + document.visibility_state() == VisibilityState::Visible +} + +pub fn is_intersecting(window: &web_sys::Window, canvas: &HtmlCanvasElement) -> bool { + let rect = canvas.get_bounding_client_rect(); + // This should never panic. + let window_width = window.inner_width().unwrap().as_f64().unwrap() as i32; + let window_height = window.inner_height().unwrap().as_f64().unwrap() as i32; + let left = rect.left() as i32; + let width = rect.width() as i32; + let top = rect.top() as i32; + let height = rect.height() as i32; + + let horizontal = left <= window_width && left + width >= 0; + let vertical = top <= window_height && top + height >= 0; + + horizontal && vertical +} + pub type RawCanvasType = HtmlCanvasElement;