diff --git a/CHANGELOG.md b/CHANGELOG.md index 8320f308..d8f3703f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,9 @@ And please only add new entries to the top of this list, right below the `# Unre - **Breaking:** On Web, dropped support for Safari versions below 13. - On Web, prevent clicks on the canvas to select text. - On Web, use high-frequency pointer input events when supported by the browser. +- On Web, `EventLoopProxy` now implements `Send`. +- On Web, `Window` now implements `Send` and `Sync`. +- **Breaking:** `WindowExtWebSys::canvas()` now returns an `Option`. # 0.28.6 diff --git a/Cargo.toml b/Cargo.toml index 537ab4a6..b13639da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -153,6 +153,7 @@ features = [ ] [target.'cfg(target_family = "wasm")'.dependencies] +atomic-waker = "1" js-sys = "0.3" wasm-bindgen = "0.2.45" wasm-bindgen-futures = "0.4" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..5c0bb339 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,3 @@ +disallowed-methods = [ + { path = "web_sys::window", reason = "is not available in every context" }, +] diff --git a/examples/web.rs b/examples/web.rs index 5be8af29..274c03b3 100644 --- a/examples/web.rs +++ b/examples/web.rs @@ -1,4 +1,4 @@ -#![allow(clippy::single_match)] +#![allow(clippy::disallowed_methods, clippy::single_match)] use winit::{ event::{Event, WindowEvent}, @@ -52,7 +52,7 @@ mod wasm { pub fn insert_canvas_and_create_log_list(window: &Window) -> web_sys::Element { use winit::platform::web::WindowExtWebSys; - let canvas = window.canvas(); + let canvas = window.canvas().unwrap(); let window = web_sys::window().unwrap(); let document = window.document().unwrap(); diff --git a/examples/web_aspect_ratio.rs b/examples/web_aspect_ratio.rs index a1983dfc..cd832af4 100644 --- a/examples/web_aspect_ratio.rs +++ b/examples/web_aspect_ratio.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_methods)] + pub fn main() { println!("This example must be run with cargo run-wasm --example web_aspect_ratio") } @@ -66,7 +68,7 @@ This example demonstrates the desired future functionality which will possibly b let body = document.body().unwrap(); // Set a background color for the canvas to make it easier to tell the where the canvas is for debugging purposes. - let canvas = window.canvas(); + let canvas = window.canvas().unwrap(); canvas .style() .set_css_text("display: block; background-color: crimson; margin: auto; width: 50%; aspect-ratio: 4 / 1;"); diff --git a/src/platform/web.rs b/src/platform/web.rs index 2ab468d8..51e7ce47 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -12,7 +12,8 @@ use crate::window::WindowBuilder; use web_sys::HtmlCanvasElement; pub trait WindowExtWebSys { - fn canvas(&self) -> HtmlCanvasElement; + /// Only returns the canvas if called from inside the window. + fn canvas(&self) -> Option; /// Whether the browser reports the preferred color scheme to be "dark". fn is_dark_mode(&self) -> bool; diff --git a/src/platform_impl/web/async.rs b/src/platform_impl/web/async.rs new file mode 100644 index 00000000..2fc4fe39 --- /dev/null +++ b/src/platform_impl/web/async.rs @@ -0,0 +1,314 @@ +use atomic_waker::AtomicWaker; +use once_cell::unsync::Lazy; +use std::future; +use std::marker::PhantomData; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, Receiver, RecvError, SendError, Sender, TryRecvError}; +use std::sync::{Arc, Condvar, Mutex, RwLock}; +use std::task::Poll; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; + +// Unsafe wrapper type that allows us to use `T` when it's not `Send` from other threads. +// `value` **must** only be accessed on the main thread. +pub struct MainThreadSafe { + // We wrap this in an `Arc` to allow it to be safely cloned without accessing the value. + // The `RwLock` lets us safely drop in any thread. + // The `Option` lets us safely drop `T` only in the main thread, while letting other threads drop `None`. + value: Arc>>, + handler: fn(&RwLock>, E), + sender: AsyncSender, + // Prevent's `Send` or `Sync` to be automatically implemented. + local: PhantomData<*const ()>, +} + +impl MainThreadSafe { + thread_local! { + static MAIN_THREAD: Lazy = Lazy::new(|| { + #[wasm_bindgen] + extern "C" { + #[derive(Clone)] + type Global; + + #[wasm_bindgen(method, getter, js_name = Window)] + fn window(this: &Global) -> JsValue; + } + + let global: Global = js_sys::global().unchecked_into(); + !global.window().is_undefined() + }); + } + + #[track_caller] + fn new(value: T, handler: fn(&RwLock>, E)) -> Option { + Self::MAIN_THREAD.with(|safe| { + if !*safe.deref() { + panic!("only callable from inside the `Window`") + } + }); + + let value = Arc::new(RwLock::new(Some(value))); + + let (sender, receiver) = channel::(); + + wasm_bindgen_futures::spawn_local({ + let value = Arc::clone(&value); + async move { + while let Ok(event) = receiver.next().await { + handler(&value, event) + } + + // An error was returned because the channel was closed, which + // happens when all senders are dropped. + value.write().unwrap().take().unwrap(); + } + }); + + Some(Self { + value, + handler, + sender, + local: PhantomData, + }) + } + + pub fn send(&self, event: E) { + Self::MAIN_THREAD.with(|is_main_thread| { + if *is_main_thread.deref() { + (self.handler)(&self.value, event) + } else { + self.sender.send(event).unwrap() + } + }) + } + + fn is_main_thread(&self) -> bool { + Self::MAIN_THREAD.with(|is_main_thread| *is_main_thread.deref()) + } + + pub fn with(&self, f: impl FnOnce(&T) -> R) -> Option { + Self::MAIN_THREAD.with(|is_main_thread| { + if *is_main_thread.deref() { + Some(f(self.value.read().unwrap().as_ref().unwrap())) + } else { + None + } + }) + } + + fn with_mut(&self, f: impl FnOnce(&mut T) -> R) -> Option { + Self::MAIN_THREAD.with(|is_main_thread| { + if *is_main_thread.deref() { + Some(f(self.value.write().unwrap().as_mut().unwrap())) + } else { + None + } + }) + } +} + +impl Clone for MainThreadSafe { + fn clone(&self) -> Self { + Self { + value: self.value.clone(), + handler: self.handler, + sender: self.sender.clone(), + local: PhantomData, + } + } +} + +unsafe impl Send for MainThreadSafe {} +unsafe impl Sync for MainThreadSafe {} + +fn channel() -> (AsyncSender, AsyncReceiver) { + let (sender, receiver) = mpsc::channel(); + let sender = Arc::new(Mutex::new(sender)); + let waker = Arc::new(AtomicWaker::new()); + let closed = Arc::new(AtomicBool::new(false)); + + let sender = AsyncSender { + sender, + closed: closed.clone(), + waker: Arc::clone(&waker), + }; + let receiver = AsyncReceiver { + receiver, + closed, + waker, + }; + + (sender, receiver) +} + +struct AsyncSender { + // We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't + // be accessed on the main thread, as it could block. Additionally we need + // to wrap it in an `Arc` to make it clonable on the main thread without + // having to block. + sender: Arc>>, + closed: Arc, + waker: Arc, +} + +impl AsyncSender { + pub fn send(&self, event: T) -> Result<(), SendError> { + self.sender.lock().unwrap().send(event)?; + self.waker.wake(); + + Ok(()) + } +} + +impl Clone for AsyncSender { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + waker: self.waker.clone(), + closed: self.closed.clone(), + } + } +} + +impl Drop for AsyncSender { + fn drop(&mut self) { + // If it's the last + the one held by the receiver make sure to wake it + // up and tell it that all receiver have dropped. + if Arc::strong_count(&self.closed) == 2 { + self.closed.store(true, Ordering::Relaxed); + self.waker.wake() + } + } +} + +struct AsyncReceiver { + receiver: Receiver, + closed: Arc, + waker: Arc, +} + +impl AsyncReceiver { + pub async fn next(&self) -> Result { + future::poll_fn(|cx| match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + if self.closed.load(Ordering::Relaxed) { + return Poll::Ready(Err(RecvError)); + } + + self.waker.register(cx.waker()); + + match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + if self.closed.load(Ordering::Relaxed) { + Poll::Ready(Err(RecvError)) + } else { + Poll::Pending + } + } + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + } + } + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + }) + .await + } +} + +pub struct Dispatcher(MainThreadSafe>); + +pub enum Closure { + Ref(Box), + RefMut(Box), +} + +impl Dispatcher { + #[track_caller] + pub fn new(value: T) -> Option { + MainThreadSafe::new(value, |value, closure| match closure { + Closure::Ref(f) => f(value.read().unwrap().as_ref().unwrap()), + Closure::RefMut(f) => f(value.write().unwrap().as_mut().unwrap()), + }) + .map(Self) + } + + pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) { + if self.is_main_thread() { + self.0.with(f).unwrap() + } else { + self.0.send(Closure::Ref(Box::new(f))) + } + } + + pub fn dispatch_mut(&self, f: impl 'static + FnOnce(&mut T) + Send) { + if self.is_main_thread() { + self.0.with_mut(f).unwrap() + } else { + self.0.send(Closure::RefMut(Box::new(f))) + } + } + + pub fn queue(&self, f: impl 'static + FnOnce(&T) -> R + Send) -> R { + if self.is_main_thread() { + self.0.with(f).unwrap() + } else { + let pair = Arc::new((Mutex::new(None), Condvar::new())); + let closure = Closure::Ref(Box::new({ + let pair = pair.clone(); + move |value| { + *pair.0.lock().unwrap() = Some(f(value)); + pair.1.notify_one(); + } + })); + + self.0.send(closure); + + let mut started = pair.0.lock().unwrap(); + + while started.is_none() { + started = pair.1.wait(started).unwrap(); + } + + started.take().unwrap() + } + } +} + +impl Deref for Dispatcher { + type Target = MainThreadSafe>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +type ChannelValue = MainThreadSafe; + +pub struct Channel(ChannelValue); + +impl Channel { + pub fn new(value: T, handler: fn(&T, E)) -> Option { + MainThreadSafe::new((value, handler), |runner, event| { + let lock = runner.read().unwrap(); + let (value, handler) = lock.as_ref().unwrap(); + handler(value, event); + }) + .map(Self) + } +} + +impl Clone for Channel { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Deref for Channel { + type Target = ChannelValue; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/src/platform_impl/web/event_loop/proxy.rs b/src/platform_impl/web/event_loop/proxy.rs index 1c709923..7094daaf 100644 --- a/src/platform_impl/web/event_loop/proxy.rs +++ b/src/platform_impl/web/event_loop/proxy.rs @@ -1,18 +1,24 @@ use super::runner; use crate::event::Event; use crate::event_loop::EventLoopClosed; +use crate::platform_impl::platform::r#async::Channel; pub struct EventLoopProxy { - runner: runner::Shared, + runner: Channel, T>, } impl EventLoopProxy { pub fn new(runner: runner::Shared) -> Self { - Self { runner } + Self { + runner: Channel::new(runner, |runner, event| { + runner.send_event(Event::UserEvent(event)) + }) + .unwrap(), + } } pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { - self.runner.send_event(Event::UserEvent(event)); + self.runner.send(event); Ok(()) } } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index 7134088f..b2682312 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -27,6 +27,7 @@ pub struct Execution { runner: RefCell>, events: RefCell>>, id: RefCell, + window: web_sys::Window, all_canvases: RefCell>)>>, redraw_pending: RefCell>, destroy_pending: RefCell>, @@ -102,6 +103,8 @@ impl Shared { Shared(Rc::new(Execution { runner: RefCell::new(RunnerEnum::Pending), events: RefCell::new(VecDeque::new()), + #[allow(clippy::disallowed_methods)] + window: web_sys::window().expect("only callable from inside the `Window`"), id: RefCell::new(0), all_canvases: RefCell::new(Vec::new()), redraw_pending: RefCell::new(HashSet::new()), @@ -111,6 +114,10 @@ impl Shared { })) } + pub fn window(&self) -> &web_sys::Window { + &self.0.window + } + pub fn add_canvas(&self, id: WindowId, canvas: &Rc>) { self.0 .all_canvases @@ -135,15 +142,19 @@ impl Shared { let close_instance = self.clone(); *self.0.unload_event_handle.borrow_mut() = - Some(backend::on_unload(move || close_instance.handle_unload())); + Some(backend::on_unload(self.window(), move || { + close_instance.handle_unload() + })); } pub(crate) fn set_on_scale_change(&self, handler: F) where F: 'static + FnMut(ScaleChangeArgs), { - *self.0.scale_change_detector.borrow_mut() = - Some(backend::ScaleChangeDetector::new(handler)); + *self.0.scale_change_detector.borrow_mut() = Some(backend::ScaleChangeDetector::new( + self.window().clone(), + handler, + )); } // Generate a strictly increasing ID @@ -340,7 +351,7 @@ impl Shared { ); // Then we resize the canvas to the new size and send a `Resized` event: - backend::set_canvas_size(&canvas, crate::dpi::Size::Physical(new_size)); + backend::set_canvas_size(self.window(), &canvas, crate::dpi::Size::Physical(new_size)); self.handle_single_event_sync( Event::WindowEvent { window_id: id, @@ -431,7 +442,10 @@ impl Shared { ControlFlow::Poll => { let cloned = self.clone(); State::Poll { - request: backend::AnimationFrameRequest::new(move || cloned.poll()), + request: backend::AnimationFrameRequest::new( + self.window().clone(), + move || cloned.poll(), + ), } } ControlFlow::Wait => State::Wait { @@ -452,6 +466,7 @@ impl Shared { start, end, timeout: backend::Timeout::new( + self.window().clone(), move || cloned.resume_time_reached(start, end), delay, ), diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index d1ba53e8..cf558af5 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -3,6 +3,8 @@ use std::clone::Clone; use std::collections::{vec_deque::IntoIter as VecDequeIter, VecDeque}; use std::iter; use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use raw_window_handle::{RawDisplayHandle, WebDisplayHandle}; @@ -84,7 +86,7 @@ impl EventLoopWindowTarget { canvas: &Rc>, id: WindowId, prevent_default: bool, - has_focus: Rc>, + has_focus: Arc, ) { self.runner.add_canvas(RootWindowId(id), canvas); let mut canvas = canvas.borrow_mut(); @@ -96,7 +98,7 @@ impl EventLoopWindowTarget { let has_focus_clone = has_focus.clone(); let modifiers = self.modifiers.clone(); canvas.on_blur(move || { - has_focus_clone.set(false); + has_focus_clone.store(false, Ordering::Relaxed); let clear_modifiers = (!modifiers.get().is_empty()).then(|| { modifiers.set(ModifiersState::empty()); @@ -119,7 +121,7 @@ impl EventLoopWindowTarget { let runner = self.runner.clone(); let has_focus_clone = has_focus.clone(); canvas.on_focus(move || { - if !has_focus_clone.replace(true) { + if !has_focus_clone.swap(true, Ordering::Relaxed) { runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::Focused(true), @@ -204,7 +206,7 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers| { - if has_focus.get() && modifiers.get() != active_modifiers { + if has_focus.load(Ordering::Relaxed) && modifiers.get() != active_modifiers { modifiers.set(active_modifiers); runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), @@ -234,7 +236,7 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers| { - if has_focus.get() && modifiers.get() != active_modifiers { + if has_focus.load(Ordering::Relaxed) && modifiers.get() != active_modifiers { modifiers.set(active_modifiers); runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), @@ -264,7 +266,7 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers| { - if has_focus.get() && modifiers.get() != active_modifiers { + if has_focus.load(Ordering::Relaxed) && modifiers.get() != active_modifiers { modifiers.set(active_modifiers); runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), @@ -419,7 +421,7 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); move |active_modifiers| { - if has_focus.get() && modifiers.get() != active_modifiers { + if has_focus.load(Ordering::Relaxed) && modifiers.get() != active_modifiers { modifiers.set(active_modifiers); runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), @@ -476,7 +478,8 @@ impl EventLoopWindowTarget { let modifiers = self.modifiers.clone(); canvas.on_mouse_wheel( move |pointer_id, delta, active_modifiers| { - let modifiers_changed = (has_focus.get() && modifiers.get() != active_modifiers) + let modifiers_changed = (has_focus.load(Ordering::Relaxed) + && modifiers.get() != active_modifiers) .then(|| { modifiers.set(active_modifiers); Event::WindowEvent { @@ -507,26 +510,30 @@ impl EventLoopWindowTarget { width: raw.width(), height: raw.height(), }; - canvas.on_fullscreen_change(move || { - // If the canvas is marked as fullscreen, it is moving *into* fullscreen - // If it is not, it is moving *out of* fullscreen - let new_size = if backend::is_fullscreen(&raw) { - intended_size = PhysicalSize { - width: raw.width(), - height: raw.height(), + + canvas.on_fullscreen_change({ + let window = self.runner.window().clone(); + move || { + // If the canvas is marked as fullscreen, it is moving *into* fullscreen + // If it is not, it is moving *out of* fullscreen + let new_size = if backend::is_fullscreen(&window, &raw) { + intended_size = PhysicalSize { + width: raw.width(), + height: raw.height(), + }; + + backend::window_size(&window).to_physical(backend::scale_factor(&window)) + } else { + intended_size }; - backend::window_size().to_physical(backend::scale_factor()) - } else { - intended_size - }; - - backend::set_canvas_size(&raw, Size::Physical(new_size)); - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::Resized(new_size), - }); - runner.request_redraw(RootWindowId(id)); + backend::set_canvas_size(&window, &raw, Size::Physical(new_size)); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized(new_size), + }); + runner.request_redraw(RootWindowId(id)); + } }); let runner = self.runner.clone(); diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 01a27e9c..bea9f0cf 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -17,6 +17,7 @@ // incoming events (from the registered handlers) and ensuring they are passed to the user in a // compliant way. +mod r#async; mod device; mod error; mod event_loop; diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 8d3d5a07..8979c7c0 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -36,19 +36,20 @@ pub struct Canvas { } pub struct Common { + pub window: web_sys::Window, /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained. pub raw: HtmlCanvasElement, wants_fullscreen: Rc>, } impl Canvas { - pub fn create(attr: PlatformSpecificWindowBuilderAttributes) -> Result { + pub fn create( + window: web_sys::Window, + attr: PlatformSpecificWindowBuilderAttributes, + ) -> Result { let canvas = match attr.canvas { Some(canvas) => canvas, None => { - let window = web_sys::window() - .ok_or_else(|| os_error!(OsError("Failed to obtain window".to_owned())))?; - let document = window .document() .ok_or_else(|| os_error!(OsError("Failed to obtain document".to_owned())))?; @@ -73,6 +74,7 @@ impl Canvas { Ok(Canvas { common: Common { + window, raw: canvas, wants_fullscreen: Rc::new(RefCell::new(false)), }, @@ -93,9 +95,9 @@ impl Canvas { if lock { self.raw().request_pointer_lock(); } else { - let window = web_sys::window() - .ok_or_else(|| os_error!(OsError("Failed to obtain window".to_owned())))?; - let document = window + let document = self + .common + .window .document() .ok_or_else(|| os_error!(OsError("Failed to obtain document".to_owned())))?; document.exit_pointer_lock(); @@ -294,12 +296,13 @@ impl Canvas { where F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), { + let window = self.common.window.clone(); self.on_mouse_wheel = Some(self.common.add_event("wheel", move |event: WheelEvent| { if prevent_default { event.prevent_default(); } - if let Some(delta) = event::mouse_scroll_delta(&event) { + if let Some(delta) = event::mouse_scroll_delta(&window, &event) { let modifiers = event::mouse_modifiers(&event); handler(0, delta, modifiers); } @@ -325,7 +328,8 @@ impl Canvas { Box::new(move |event: MediaQueryListEvent| handler(event.matches())) as Box, ); - self.on_dark_mode = MediaQueryListHandle::new("(prefers-color-scheme: dark)", closure); + self.on_dark_mode = + MediaQueryListHandle::new(&self.common.window, "(prefers-color-scheme: dark)", closure); } pub fn request_fullscreen(&self) { @@ -426,6 +430,6 @@ impl Common { } pub fn is_fullscreen(&self) -> bool { - super::is_fullscreen(&self.raw) + super::is_fullscreen(&self.window, &self.raw) } } diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs index af1d4acb..56618844 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/event.rs @@ -79,14 +79,17 @@ pub fn mouse_position_by_client( } } -pub fn mouse_scroll_delta(event: &WheelEvent) -> Option { +pub fn mouse_scroll_delta( + window: &web_sys::Window, + event: &WheelEvent, +) -> Option { let x = -event.delta_x(); let y = -event.delta_y(); match event.delta_mode() { WheelEvent::DOM_DELTA_LINE => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), WheelEvent::DOM_DELTA_PIXEL => { - let delta = LogicalPosition::new(x, y).to_physical(super::scale_factor()); + let delta = LogicalPosition::new(x, y).to_physical(super::scale_factor(window)); Some(MouseScrollDelta::PixelDelta(delta)) } _ => None, diff --git a/src/platform_impl/web/web_sys/media_query_handle.rs b/src/platform_impl/web/web_sys/media_query_handle.rs index dc959561..a09feebd 100644 --- a/src/platform_impl/web/web_sys/media_query_handle.rs +++ b/src/platform_impl/web/web_sys/media_query_handle.rs @@ -8,10 +8,10 @@ pub(super) struct MediaQueryListHandle { impl MediaQueryListHandle { pub fn new( + window: &web_sys::Window, media_query: &str, listener: Closure, ) -> Option { - let window = web_sys::window().expect("Failed to obtain window"); let mql = window .match_media(media_query) .ok() diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 6a1f556e..2b6e3745 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -15,14 +15,13 @@ use crate::dpi::{LogicalSize, Size}; use crate::platform::web::WindowExtWebSys; use crate::window::Window; use wasm_bindgen::closure::Closure; -use web_sys::{window, BeforeUnloadEvent, Element, HtmlCanvasElement}; +use web_sys::{BeforeUnloadEvent, Element, HtmlCanvasElement}; pub fn throw(msg: &str) { wasm_bindgen::throw_str(msg); } -pub fn exit_fullscreen() { - let window = web_sys::window().expect("Failed to obtain window"); +pub fn exit_fullscreen(window: &web_sys::Window) { let document = window.document().expect("Failed to obtain document"); document.exit_fullscreen(); @@ -32,38 +31,33 @@ pub struct UnloadEventHandle { _listener: event_handle::EventListenerHandle, } -pub fn on_unload(mut handler: impl FnMut() + 'static) -> UnloadEventHandle { - let window = web_sys::window().expect("Failed to obtain window"); - +pub fn on_unload( + window: &web_sys::Window, + mut handler: impl FnMut() + 'static, +) -> UnloadEventHandle { let closure = Closure::wrap( Box::new(move |_: BeforeUnloadEvent| handler()) as Box ); - let listener = event_handle::EventListenerHandle::new(&window, "beforeunload", closure); + let listener = event_handle::EventListenerHandle::new(window, "beforeunload", closure); UnloadEventHandle { _listener: listener, } } impl WindowExtWebSys for Window { - fn canvas(&self) -> HtmlCanvasElement { - self.window.canvas().raw().clone() + fn canvas(&self) -> Option { + self.window.canvas() } fn is_dark_mode(&self) -> bool { - let window = web_sys::window().expect("Failed to obtain window"); - - window - .match_media("(prefers-color-scheme: dark)") - .ok() - .flatten() - .map(|media| media.matches()) - .unwrap_or(false) + self.window + .inner + .queue(|inner| is_dark_mode(&inner.window).unwrap_or(false)) } } -pub fn window_size() -> LogicalSize { - let window = web_sys::window().expect("Failed to obtain window"); +pub fn window_size(window: &web_sys::Window) -> LogicalSize { let width = window .inner_width() .expect("Failed to get width") @@ -78,13 +72,12 @@ pub fn window_size() -> LogicalSize { LogicalSize { width, height } } -pub fn scale_factor() -> f64 { - let window = web_sys::window().expect("Failed to obtain window"); +pub fn scale_factor(window: &web_sys::Window) -> f64 { window.device_pixel_ratio() } -pub fn set_canvas_size(raw: &HtmlCanvasElement, size: Size) { - let scale_factor = scale_factor(); +pub fn set_canvas_size(window: &web_sys::Window, raw: &HtmlCanvasElement, size: Size) { + let scale_factor = scale_factor(window); let logical_size = size.to_logical::(scale_factor); set_canvas_style_property(raw, "width", &format!("{}px", logical_size.width)); @@ -98,8 +91,7 @@ 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(canvas: &HtmlCanvasElement) -> bool { - let window = window().expect("Failed to obtain window"); +pub fn is_fullscreen(window: &web_sys::Window, canvas: &HtmlCanvasElement) -> bool { let document = window.document().expect("Failed to obtain document"); match document.fullscreen_element() { @@ -111,4 +103,12 @@ pub fn is_fullscreen(canvas: &HtmlCanvasElement) -> bool { } } +pub fn is_dark_mode(window: &web_sys::Window) -> Option { + window + .match_media("(prefers-color-scheme: dark)") + .ok() + .flatten() + .map(|media| media.matches()) +} + pub type RawCanvasType = HtmlCanvasElement; diff --git a/src/platform_impl/web/web_sys/pointer.rs b/src/platform_impl/web/web_sys/pointer.rs index aab89d29..8ee514e3 100644 --- a/src/platform_impl/web/web_sys/pointer.rs +++ b/src/platform_impl/web/web_sys/pointer.rs @@ -96,6 +96,7 @@ impl PointerHandler { M: 'static + FnMut(i32, PhysicalPosition, MouseButton), T: 'static + FnMut(i32, PhysicalPosition, Force), { + let window = canvas_common.window.clone(); let canvas = canvas_common.raw.clone(); self.on_pointer_release = Some(canvas_common.add_user_event( "pointerup", @@ -105,12 +106,13 @@ impl PointerHandler { match event.pointer_type().as_str() { "touch" => touch_handler( event.pointer_id(), - event::touch_position(&event, &canvas).to_physical(super::scale_factor()), + event::touch_position(&event, &canvas) + .to_physical(super::scale_factor(&window)), Force::Normalized(event.pressure() as f64), ), "mouse" => mouse_handler( event.pointer_id(), - event::mouse_position(&event).to_physical(super::scale_factor()), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), event::mouse_button(&event).expect("no mouse button released"), ), _ => (), @@ -131,6 +133,7 @@ impl PointerHandler { M: 'static + FnMut(i32, PhysicalPosition, MouseButton), T: 'static + FnMut(i32, PhysicalPosition, Force), { + let window = canvas_common.window.clone(); let canvas = canvas_common.raw.clone(); self.on_pointer_press = Some(canvas_common.add_user_event( "pointerdown", @@ -149,14 +152,14 @@ impl PointerHandler { touch_handler( event.pointer_id(), event::touch_position(&event, &canvas) - .to_physical(super::scale_factor()), + .to_physical(super::scale_factor(&window)), Force::Normalized(event.pressure() as f64), ); } "mouse" => { mouse_handler( event.pointer_id(), - event::mouse_position(&event).to_physical(super::scale_factor()), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), event::mouse_button(&event).expect("no mouse button pressed"), ); @@ -185,6 +188,7 @@ impl PointerHandler { T: 'static + FnMut(i32, PhysicalPosition, Force), B: 'static + FnMut(i32, PhysicalPosition, ButtonsState, MouseButton), { + let window = canvas_common.window.clone(); let canvas = canvas_common.raw.clone(); self.on_cursor_move = Some(canvas_common.add_event( if has_pointer_raw_support(&canvas) { @@ -230,7 +234,7 @@ impl PointerHandler { button_handler( id, - event::mouse_position(&event).to_physical(super::scale_factor()), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), event::mouse_buttons(&event), button, ); @@ -261,13 +265,13 @@ impl PointerHandler { match pointer_type.as_str() { "mouse" => mouse_handler( id, - event::mouse_position(&event).to_physical(super::scale_factor()), - event::mouse_delta(&event).to_physical(super::scale_factor()), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + event::mouse_delta(&event).to_physical(super::scale_factor(&window)), ), "touch" => touch_handler( id, event::touch_position(&event, &canvas) - .to_physical(super::scale_factor()), + .to_physical(super::scale_factor(&window)), Force::Normalized(event.pressure() as f64), ), _ => unreachable!("didn't return early before"), @@ -281,6 +285,7 @@ impl PointerHandler { where F: 'static + FnMut(i32, PhysicalPosition, Force), { + let window = canvas_common.window.clone(); let canvas = canvas_common.raw.clone(); self.on_touch_cancel = Some(canvas_common.add_event( "pointercancel", @@ -288,7 +293,8 @@ impl PointerHandler { if event.pointer_type() == "touch" { handler( event.pointer_id(), - event::touch_position(&event, &canvas).to_physical(super::scale_factor()), + event::touch_position(&event, &canvas) + .to_physical(super::scale_factor(&window)), Force::Normalized(event.pressure() as f64), ); } diff --git a/src/platform_impl/web/web_sys/scaling.rs b/src/platform_impl/web/web_sys/scaling.rs index 651857a6..dabe9780 100644 --- a/src/platform_impl/web/web_sys/scaling.rs +++ b/src/platform_impl/web/web_sys/scaling.rs @@ -8,29 +8,31 @@ use web_sys::MediaQueryListEvent; pub struct ScaleChangeDetector(Rc>); impl ScaleChangeDetector { - pub(crate) fn new(handler: F) -> Self + pub(crate) fn new(window: web_sys::Window, handler: F) -> Self where F: 'static + FnMut(ScaleChangeArgs), { - Self(ScaleChangeDetectorInternal::new(handler)) + Self(ScaleChangeDetectorInternal::new(window, handler)) } } /// This is a helper type to help manage the `MediaQueryList` used for detecting /// changes of the `devicePixelRatio`. struct ScaleChangeDetectorInternal { + window: web_sys::Window, callback: Box, mql: Option, last_scale: f64, } impl ScaleChangeDetectorInternal { - fn new(handler: F) -> Rc> + fn new(window: web_sys::Window, handler: F) -> Rc> where F: 'static + FnMut(ScaleChangeArgs), { - let current_scale = super::scale_factor(); + let current_scale = super::scale_factor(&window); let new_self = Rc::new(RefCell::new(Self { + window: window.clone(), callback: Box::new(handler), mql: None, last_scale: current_scale, @@ -43,7 +45,7 @@ impl ScaleChangeDetectorInternal { } }) as Box); - let mql = Self::create_mql(closure); + let mql = Self::create_mql(&window, closure); { let mut borrowed_self = new_self.borrow_mut(); borrowed_self.mql = mql; @@ -52,9 +54,10 @@ impl ScaleChangeDetectorInternal { } fn create_mql( + window: &web_sys::Window, closure: Closure, ) -> Option { - let current_scale = super::scale_factor(); + let current_scale = super::scale_factor(window); // This media query initially matches the current `devicePixelRatio`. // We add 0.0001 to the lower and upper bounds such that it won't fail // due to floating point precision limitations. @@ -63,7 +66,7 @@ impl ScaleChangeDetectorInternal { (-webkit-min-device-pixel-ratio: {min_scale:.4}) and (-webkit-max-device-pixel-ratio: {max_scale:.4})", min_scale = current_scale - 0.0001, max_scale= current_scale + 0.0001, ); - let mql = MediaQueryListHandle::new(&media_query, closure); + let mql = MediaQueryListHandle::new(window, &media_query, closure); if let Some(mql) = &mql { assert!(mql.mql().matches()); } @@ -76,12 +79,12 @@ impl ScaleChangeDetectorInternal { .take() .expect("DevicePixelRatioChangeDetector::mql should not be None"); let closure = mql.remove(); - let new_scale = super::scale_factor(); + let new_scale = super::scale_factor(&self.window); (self.callback)(ScaleChangeArgs { old_scale: self.last_scale, new_scale, }); - let new_mql = Self::create_mql(closure); + let new_mql = Self::create_mql(&self.window, closure); self.mql = new_mql; self.last_scale = new_scale; } diff --git a/src/platform_impl/web/web_sys/timeout.rs b/src/platform_impl/web/web_sys/timeout.rs index 1978cbb3..8250cf63 100644 --- a/src/platform_impl/web/web_sys/timeout.rs +++ b/src/platform_impl/web/web_sys/timeout.rs @@ -6,17 +6,16 @@ use wasm_bindgen::JsCast; #[derive(Debug)] pub struct Timeout { + window: web_sys::Window, handle: i32, _closure: Closure, } impl Timeout { - pub fn new(f: F, duration: Duration) -> Timeout + pub fn new(window: web_sys::Window, f: F, duration: Duration) -> Timeout where F: 'static + FnMut(), { - let window = web_sys::window().expect("Failed to obtain window"); - let closure = Closure::wrap(Box::new(f) as Box); let handle = window @@ -27,6 +26,7 @@ impl Timeout { .expect("Failed to set timeout"); Timeout { + window, handle, _closure: closure, } @@ -35,14 +35,13 @@ impl Timeout { impl Drop for Timeout { fn drop(&mut self) { - let window = web_sys::window().expect("Failed to obtain window"); - - window.clear_timeout_with_handle(self.handle); + self.window.clear_timeout_with_handle(self.handle); } } #[derive(Debug)] pub struct AnimationFrameRequest { + window: web_sys::Window, handle: i32, // track callback state, because `cancelAnimationFrame` is slow fired: Rc>, @@ -50,12 +49,10 @@ pub struct AnimationFrameRequest { } impl AnimationFrameRequest { - pub fn new(mut f: F) -> AnimationFrameRequest + pub fn new(window: web_sys::Window, mut f: F) -> AnimationFrameRequest where F: 'static + FnMut(), { - let window = web_sys::window().expect("Failed to obtain window"); - let fired = Rc::new(Cell::new(false)); let c_fired = fired.clone(); let closure = Closure::wrap(Box::new(move || { @@ -68,6 +65,7 @@ impl AnimationFrameRequest { .expect("Failed to request animation frame"); AnimationFrameRequest { + window, handle, fired, _closure: closure, @@ -78,8 +76,7 @@ impl AnimationFrameRequest { impl Drop for AnimationFrameRequest { fn drop(&mut self) { if !(*self.fired).get() { - let window = web_sys::window().expect("Failed to obtain window"); - window + self.window .cancel_animation_frame(self.handle) .expect("Failed to cancel animation frame"); } diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 6401fc0f..7368e19f 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -8,22 +8,31 @@ use crate::window::{ }; use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle}; +use web_sys::HtmlCanvasElement; +use super::r#async::Dispatcher; use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen}; -use std::cell::{Cell, Ref, RefCell}; +use std::cell::RefCell; use std::collections::vec_deque::IntoIter as VecDequeIter; use std::collections::VecDeque; use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; pub struct Window { + id: WindowId, + has_focus: Arc, + pub inner: Dispatcher, +} + +pub struct Inner { + pub window: web_sys::Window, canvas: Rc>, previous_pointer: RefCell<&'static str>, - id: WindowId, register_redraw_request: Box, resize_notify_fn: Box)>, destroy_fn: Option>, - has_focus: Rc>, } impl Window { @@ -38,12 +47,13 @@ impl Window { let prevent_default = platform_attr.prevent_default; - let canvas = backend::Canvas::create(platform_attr)?; + let window = target.runner.window(); + let canvas = backend::Canvas::create(window.clone(), platform_attr)?; let canvas = Rc::new(RefCell::new(canvas)); let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); - let has_focus = Rc::new(Cell::new(false)); + let has_focus = Arc::new(AtomicBool::new(false)); target.register(&canvas, id, prevent_default, has_focus.clone()); let runner = target.runner.clone(); @@ -57,23 +67,29 @@ impl Window { let runner = target.runner.clone(); let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); - let window = Window { - canvas, - previous_pointer: RefCell::new("auto"), - id, - register_redraw_request, - resize_notify_fn, - destroy_fn: Some(destroy_fn), - has_focus, - }; - backend::set_canvas_size( - window.canvas.borrow().raw(), + window, + canvas.borrow().raw(), attr.inner_size.unwrap_or(Size::Logical(LogicalSize { width: 1024.0, height: 768.0, })), ); + + let window = Window { + id, + has_focus, + inner: Dispatcher::new(Inner { + window: window.clone(), + canvas, + previous_pointer: RefCell::new("auto"), + register_redraw_request, + resize_notify_fn, + destroy_fn: Some(destroy_fn), + }) + .unwrap(), + }; + window.set_title(&attr.title); window.set_maximized(attr.maximized); window.set_visible(attr.visible); @@ -82,12 +98,20 @@ impl Window { Ok(window) } - pub fn canvas(&self) -> Ref<'_, backend::Canvas> { - self.canvas.borrow() + pub fn canvas(&self) -> Option { + self.inner.with(|inner| inner.canvas.borrow().raw().clone()) } pub fn set_title(&self, title: &str) { - self.canvas.borrow().set_attribute("alt", title); + if self + .inner + .with(|inner| inner.canvas.borrow().set_attribute("alt", title)) + .is_none() + { + let title = title.to_owned(); + self.inner + .dispatch(move |inner| inner.canvas.borrow().set_attribute("alt", &title)); + } } pub fn set_transparent(&self, _transparent: bool) {} @@ -102,15 +126,18 @@ impl Window { } pub fn request_redraw(&self) { - (self.register_redraw_request)(); + self.inner + .dispatch(|inner| (inner.register_redraw_request)()); } pub fn outer_position(&self) -> Result, NotSupportedError> { - Ok(self - .canvas - .borrow() - .position() - .to_physical(self.scale_factor())) + self.inner.queue(|inner| { + Ok(inner + .canvas + .borrow() + .position() + .to_physical(inner.scale_factor())) + }) } pub fn inner_position(&self) -> Result, NotSupportedError> { @@ -119,17 +146,19 @@ impl Window { } pub fn set_outer_position(&self, position: Position) { - let position = position.to_logical::(self.scale_factor()); + self.inner.dispatch(move |inner| { + let position = position.to_logical::(inner.scale_factor()); - let canvas = self.canvas.borrow(); - canvas.set_attribute("position", "fixed"); - canvas.set_attribute("left", &position.x.to_string()); - canvas.set_attribute("top", &position.y.to_string()); + let canvas = inner.canvas.borrow(); + canvas.set_attribute("position", "fixed"); + canvas.set_attribute("left", &position.x.to_string()); + canvas.set_attribute("top", &position.y.to_string()); + }); } #[inline] pub fn inner_size(&self) -> PhysicalSize { - self.canvas.borrow().size() + self.inner.queue(|inner| inner.inner_size()) } #[inline] @@ -140,12 +169,14 @@ impl Window { #[inline] pub fn set_inner_size(&self, size: Size) { - let old_size = self.inner_size(); - backend::set_canvas_size(self.canvas.borrow().raw(), size); - let new_size = self.inner_size(); - if old_size != new_size { - (self.resize_notify_fn)(new_size); - } + self.inner.dispatch(move |inner| { + let old_size = inner.inner_size(); + backend::set_canvas_size(&inner.window, inner.canvas.borrow().raw(), size); + let new_size = inner.inner_size(); + if old_size != new_size { + (inner.resize_notify_fn)(new_size); + } + }); } #[inline] @@ -187,13 +218,19 @@ impl Window { #[inline] pub fn scale_factor(&self) -> f64 { - super::backend::scale_factor() + self.inner.queue(|inner| inner.scale_factor()) } #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { - *self.previous_pointer.borrow_mut() = cursor.name(); - backend::set_canvas_style_property(self.canvas.borrow().raw(), "cursor", cursor.name()); + self.inner.dispatch(move |inner| { + *inner.previous_pointer.borrow_mut() = cursor.name(); + backend::set_canvas_style_property( + inner.canvas.borrow().raw(), + "cursor", + cursor.name(), + ); + }); } #[inline] @@ -203,29 +240,35 @@ impl Window { #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { - let lock = match mode { - CursorGrabMode::None => false, - CursorGrabMode::Locked => true, - CursorGrabMode::Confined => { - return Err(ExternalError::NotSupported(NotSupportedError::new())) - } - }; + self.inner.queue(move |inner| { + let lock = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Locked => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; - self.canvas - .borrow() - .set_cursor_lock(lock) - .map_err(ExternalError::Os) + inner + .canvas + .borrow() + .set_cursor_lock(lock) + .map_err(ExternalError::Os) + }) } #[inline] pub fn set_cursor_visible(&self, visible: bool) { - if !visible { - self.canvas.borrow().set_attribute("cursor", "none"); - } else { - self.canvas - .borrow() - .set_attribute("cursor", &self.previous_pointer.borrow()); - } + self.inner.dispatch(move |inner| { + if !visible { + inner.canvas.borrow().set_attribute("cursor", "none"); + } else { + inner + .canvas + .borrow() + .set_attribute("cursor", &inner.previous_pointer.borrow()); + } + }); } #[inline] @@ -267,20 +310,24 @@ impl Window { #[inline] pub(crate) fn fullscreen(&self) -> Option { - if self.canvas.borrow().is_fullscreen() { - Some(Fullscreen::Borderless(Some(self.current_monitor_inner()))) - } else { - None - } + self.inner.queue(|inner| { + if inner.canvas.borrow().is_fullscreen() { + Some(Fullscreen::Borderless(Some(MonitorHandle))) + } else { + None + } + }) } #[inline] pub(crate) fn set_fullscreen(&self, fullscreen: Option) { - if fullscreen.is_some() { - self.canvas.borrow().request_fullscreen(); - } else if self.canvas.borrow().is_fullscreen() { - backend::exit_fullscreen(); - } + self.inner.dispatch(move |inner| { + if fullscreen.is_some() { + inner.canvas.borrow().request_fullscreen(); + } else if inner.canvas.borrow().is_fullscreen() { + backend::exit_fullscreen(&inner.window); + } + }); } #[inline] @@ -327,15 +374,9 @@ impl Window { // Currently an intentional no-op } - #[inline] - // Allow directly accessing the current monitor internally without unwrapping. - fn current_monitor_inner(&self) -> MonitorHandle { - MonitorHandle - } - #[inline] pub fn current_monitor(&self) -> Option { - Some(self.current_monitor_inner()) + Some(MonitorHandle) } #[inline] @@ -370,25 +411,20 @@ impl Window { #[inline] pub fn theme(&self) -> Option { - web_sys::window() - .and_then(|window| { - window - .match_media("(prefers-color-scheme: dark)") - .ok() - .flatten() - }) - .map(|media_query_list| { - if media_query_list.matches() { + self.inner.queue(|inner| { + backend::is_dark_mode(&inner.window).map(|is_dark_mode| { + if is_dark_mode { Theme::Dark } else { Theme::Light } }) + }) } #[inline] pub fn has_focus(&self) -> bool { - self.has_focus.get() + self.has_focus.load(Ordering::Relaxed) } pub fn title(&self) -> String { @@ -402,9 +438,23 @@ impl Window { impl Drop for Window { fn drop(&mut self) { - if let Some(destroy_fn) = self.destroy_fn.take() { - destroy_fn(); - } + self.inner.dispatch_mut(|inner| { + if let Some(destroy_fn) = inner.destroy_fn.take() { + destroy_fn(); + } + }); + } +} + +impl Inner { + #[inline] + pub fn scale_factor(&self) -> f64 { + super::backend::scale_factor(&self.window) + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.canvas.borrow().size() } } diff --git a/tests/send_objects.rs b/tests/send_objects.rs index 15fd8793..9462d073 100644 --- a/tests/send_objects.rs +++ b/tests/send_objects.rs @@ -1,7 +1,6 @@ #[allow(dead_code)] fn needs_send() {} -#[cfg(not(wasm_platform))] #[test] fn event_loop_proxy_send() { #[allow(dead_code)] @@ -11,7 +10,6 @@ fn event_loop_proxy_send() { } } -#[cfg(not(wasm_platform))] #[test] fn window_send() { // ensures that `winit::Window` implements `Send` diff --git a/tests/sync_object.rs b/tests/sync_object.rs index 80be1773..dad65202 100644 --- a/tests/sync_object.rs +++ b/tests/sync_object.rs @@ -1,7 +1,6 @@ #[allow(dead_code)] fn needs_sync() {} -#[cfg(not(wasm_platform))] #[test] fn window_sync() { // ensures that `winit::Window` implements `Sync`