rio_window/platform/
web.rs

1//! # Web
2//!
3//! The officially supported browsers are Chrome, Firefox and Safari 13.1+,
4//! though forks of these should work fine.
5//!
6//! Winit supports compiling to the `wasm32-unknown-unknown` target with
7//! `web-sys`.
8//!
9//! On the web platform, a Winit window is backed by a `<canvas>` element. You
10//! can either [provide Winit with a `<canvas>` element][with_canvas], or
11//! [let Winit create a `<canvas>` element which you can then retrieve][get]
12//! and insert it into the DOM yourself.
13//!
14//! Currently, there is no example code using Winit on Web, see [#3473]. For
15//! information on using Rust on WebAssembly, check out the [Rust and
16//! WebAssembly book].
17//!
18//! [with_canvas]: WindowAttributesExtWebSys::with_canvas
19//! [get]: WindowExtWebSys::canvas
20//! [#3473]: https://github.com/rust-windowing/winit/issues/3473
21//! [Rust and WebAssembly book]: https://rustwasm.github.io/book/
22//!
23//! ## CSS properties
24//!
25//! It is recommended **not** to apply certain CSS properties to the canvas:
26//! - [`transform`](https://developer.mozilla.org/en-US/docs/Web/CSS/transform)
27//! - [`border`](https://developer.mozilla.org/en-US/docs/Web/CSS/border)
28//! - [`padding`](https://developer.mozilla.org/en-US/docs/Web/CSS/padding)
29//!
30//! The following APIs can't take them into account and will therefore provide inaccurate results:
31//! - [`WindowEvent::Resized`] and [`Window::(set_)inner_size()`]
32//! - [`WindowEvent::Occluded`]
33//! - [`WindowEvent::CursorMoved`], [`WindowEvent::CursorEntered`], [`WindowEvent::CursorLeft`], and
34//!   [`WindowEvent::Touch`].
35//! - [`Window::set_outer_position()`]
36//!
37//! [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized
38//! [`Window::(set_)inner_size()`]: crate::window::Window::inner_size
39//! [`WindowEvent::Occluded`]: crate::event::WindowEvent::Occluded
40//! [`WindowEvent::CursorMoved`]: crate::event::WindowEvent::CursorMoved
41//! [`WindowEvent::CursorEntered`]: crate::event::WindowEvent::CursorEntered
42//! [`WindowEvent::CursorLeft`]: crate::event::WindowEvent::CursorLeft
43//! [`WindowEvent::Touch`]: crate::event::WindowEvent::Touch
44//! [`Window::set_outer_position()`]: crate::window::Window::set_outer_position
45
46use std::error::Error;
47use std::fmt::{self, Display, Formatter};
48use std::future::Future;
49use std::pin::Pin;
50use std::task::{Context, Poll};
51use std::time::Duration;
52
53#[cfg(web_platform)]
54use web_sys::HtmlCanvasElement;
55
56use crate::application::ApplicationHandler;
57use crate::cursor::CustomCursorSource;
58use crate::event::Event;
59use crate::event_loop::{self, ActiveEventLoop, EventLoop};
60#[cfg(web_platform)]
61use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture;
62use crate::platform_impl::PlatformCustomCursorSource;
63use crate::window::{CustomCursor, Window, WindowAttributes};
64
65#[cfg(not(web_platform))]
66#[doc(hidden)]
67pub struct HtmlCanvasElement;
68
69pub trait WindowExtWebSys {
70    /// Only returns the canvas if called from inside the window context (the
71    /// main thread).
72    fn canvas(&self) -> Option<HtmlCanvasElement>;
73
74    /// Returns [`true`] if calling `event.preventDefault()` is enabled.
75    ///
76    /// See [`Window::set_prevent_default()`] for more details.
77    fn prevent_default(&self) -> bool;
78
79    /// Sets whether `event.preventDefault()` should be called on events on the
80    /// canvas that have side effects.
81    ///
82    /// For example, by default using the mouse wheel would cause the page to scroll, enabling this
83    /// would prevent that.
84    ///
85    /// Some events are impossible to prevent. E.g. Firefox allows to access the native browser
86    /// context menu with Shift+Rightclick.
87    fn set_prevent_default(&self, prevent_default: bool);
88}
89
90impl WindowExtWebSys for Window {
91    #[inline]
92    fn canvas(&self) -> Option<HtmlCanvasElement> {
93        self.window.canvas()
94    }
95
96    fn prevent_default(&self) -> bool {
97        self.window.prevent_default()
98    }
99
100    fn set_prevent_default(&self, prevent_default: bool) {
101        self.window.set_prevent_default(prevent_default)
102    }
103}
104
105pub trait WindowAttributesExtWebSys {
106    /// Pass an [`HtmlCanvasElement`] to be used for this [`Window`]. If [`None`],
107    /// [`WindowAttributes::default()`] will create one.
108    ///
109    /// In any case, the canvas won't be automatically inserted into the web page.
110    ///
111    /// [`None`] by default.
112    #[cfg_attr(
113        not(web_platform),
114        doc = "",
115        doc = "[`HtmlCanvasElement`]: #only-available-on-wasm"
116    )]
117    fn with_canvas(self, canvas: Option<HtmlCanvasElement>) -> Self;
118
119    /// Sets whether `event.preventDefault()` should be called on events on the
120    /// canvas that have side effects.
121    ///
122    /// See [`Window::set_prevent_default()`] for more details.
123    ///
124    /// Enabled by default.
125    fn with_prevent_default(self, prevent_default: bool) -> Self;
126
127    /// Whether the canvas should be focusable using the tab key. This is necessary to capture
128    /// canvas keyboard events.
129    ///
130    /// Enabled by default.
131    fn with_focusable(self, focusable: bool) -> Self;
132
133    /// On window creation, append the canvas element to the web page if it isn't already.
134    ///
135    /// Disabled by default.
136    fn with_append(self, append: bool) -> Self;
137}
138
139impl WindowAttributesExtWebSys for WindowAttributes {
140    fn with_canvas(mut self, canvas: Option<HtmlCanvasElement>) -> Self {
141        self.platform_specific.set_canvas(canvas);
142        self
143    }
144
145    fn with_prevent_default(mut self, prevent_default: bool) -> Self {
146        self.platform_specific.prevent_default = prevent_default;
147        self
148    }
149
150    fn with_focusable(mut self, focusable: bool) -> Self {
151        self.platform_specific.focusable = focusable;
152        self
153    }
154
155    fn with_append(mut self, append: bool) -> Self {
156        self.platform_specific.append = append;
157        self
158    }
159}
160
161/// Additional methods on `EventLoop` that are specific to the web.
162pub trait EventLoopExtWebSys {
163    /// A type provided by the user that can be passed through `Event::UserEvent`.
164    type UserEvent: 'static;
165
166    /// Initializes the winit event loop.
167    ///
168    /// Unlike
169    #[cfg_attr(all(web_platform, target_feature = "exception-handling"), doc = "`run_app()`")]
170    #[cfg_attr(
171        not(all(web_platform, target_feature = "exception-handling")),
172        doc = "[`run_app()`]"
173    )]
174    /// [^1], this returns immediately, and doesn't throw an exception in order to
175    /// satisfy its [`!`] return type.
176    ///
177    /// Once the event loop has been destroyed, it's possible to reinitialize another event loop
178    /// by calling this function again. This can be useful if you want to recreate the event loop
179    /// while the WebAssembly module is still loaded. For example, this can be used to recreate the
180    /// event loop when switching between tabs on a single page application.
181    #[rustfmt::skip]
182    ///
183    #[cfg_attr(
184        not(all(web_platform, target_feature = "exception-handling")),
185        doc = "[`run_app()`]: EventLoop::run_app()"
186    )]
187    /// [^1]: `run_app()` is _not_ available on WASM when the target supports `exception-handling`.
188    fn spawn_app<A: ApplicationHandler<Self::UserEvent> + 'static>(self, app: A);
189
190    /// See [`spawn_app`].
191    ///
192    /// [`spawn_app`]: Self::spawn_app
193    #[deprecated = "use EventLoopExtWebSys::spawn_app"]
194    fn spawn<F>(self, event_handler: F)
195    where
196        F: 'static + FnMut(Event<Self::UserEvent>, &ActiveEventLoop);
197}
198
199impl<T> EventLoopExtWebSys for EventLoop<T> {
200    type UserEvent = T;
201
202    fn spawn_app<A: ApplicationHandler<Self::UserEvent> + 'static>(self, mut app: A) {
203        self.event_loop.spawn(move |event, event_loop| {
204            event_loop::dispatch_event_for_app(&mut app, event_loop, event)
205        });
206    }
207
208    fn spawn<F>(self, event_handler: F)
209    where
210        F: 'static + FnMut(Event<Self::UserEvent>, &ActiveEventLoop),
211    {
212        self.event_loop.spawn(event_handler)
213    }
214}
215
216pub trait ActiveEventLoopExtWebSys {
217    /// Sets the strategy for [`ControlFlow::Poll`].
218    ///
219    /// See [`PollStrategy`].
220    ///
221    /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
222    fn set_poll_strategy(&self, strategy: PollStrategy);
223
224    /// Gets the strategy for [`ControlFlow::Poll`].
225    ///
226    /// See [`PollStrategy`].
227    ///
228    /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
229    fn poll_strategy(&self) -> PollStrategy;
230
231    /// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the
232    /// cursor has completely finished loading.
233    fn create_custom_cursor_async(
234        &self,
235        source: CustomCursorSource,
236    ) -> CustomCursorFuture;
237}
238
239impl ActiveEventLoopExtWebSys for ActiveEventLoop {
240    #[inline]
241    fn create_custom_cursor_async(
242        &self,
243        source: CustomCursorSource,
244    ) -> CustomCursorFuture {
245        self.p.create_custom_cursor_async(source)
246    }
247
248    #[inline]
249    fn set_poll_strategy(&self, strategy: PollStrategy) {
250        self.p.set_poll_strategy(strategy);
251    }
252
253    #[inline]
254    fn poll_strategy(&self) -> PollStrategy {
255        self.p.poll_strategy()
256    }
257}
258
259/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll].
260#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
261pub enum PollStrategy {
262    /// Uses [`Window.requestIdleCallback()`] to queue the next event loop. If not available
263    /// this will fallback to [`setTimeout()`].
264    ///
265    /// This strategy will wait for the browser to enter an idle period before running and might
266    /// be affected by browser throttling.
267    ///
268    /// [`Window.requestIdleCallback()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
269    /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
270    IdleCallback,
271    /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available
272    /// this will fallback to [`setTimeout()`].
273    ///
274    /// This strategy will run as fast as possible without disturbing users from interacting with
275    /// the page and is not affected by browser throttling.
276    ///
277    /// This is the default strategy.
278    ///
279    /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
280    /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
281    #[default]
282    Scheduler,
283}
284
285pub trait CustomCursorExtWebSys {
286    /// Returns if this cursor is an animation.
287    fn is_animation(&self) -> bool;
288
289    /// Creates a new cursor from a URL pointing to an image.
290    /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
291    /// but browser support for image formats is inconsistent. Using [PNG] is recommended.
292    ///
293    /// [PNG]: https://en.wikipedia.org/wiki/PNG
294    fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource;
295
296    /// Crates a new animated cursor from multiple [`CustomCursor`]s.
297    /// Supplied `cursors` can't be empty or other animations.
298    fn from_animation(
299        duration: Duration,
300        cursors: Vec<CustomCursor>,
301    ) -> Result<CustomCursorSource, BadAnimation>;
302}
303
304impl CustomCursorExtWebSys for CustomCursor {
305    fn is_animation(&self) -> bool {
306        self.inner.animation
307    }
308
309    fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource {
310        CustomCursorSource {
311            inner: PlatformCustomCursorSource::Url {
312                url,
313                hotspot_x,
314                hotspot_y,
315            },
316        }
317    }
318
319    fn from_animation(
320        duration: Duration,
321        cursors: Vec<CustomCursor>,
322    ) -> Result<CustomCursorSource, BadAnimation> {
323        if cursors.is_empty() {
324            return Err(BadAnimation::Empty);
325        }
326
327        if cursors.iter().any(CustomCursor::is_animation) {
328            return Err(BadAnimation::Animation);
329        }
330
331        Ok(CustomCursorSource {
332            inner: PlatformCustomCursorSource::Animation { duration, cursors },
333        })
334    }
335}
336
337/// An error produced when using [`CustomCursor::from_animation`] with invalid arguments.
338#[derive(Debug, Clone)]
339pub enum BadAnimation {
340    /// Produced when no cursors were supplied.
341    Empty,
342    /// Produced when a supplied cursor is an animation.
343    Animation,
344}
345
346impl fmt::Display for BadAnimation {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        match self {
349            Self::Empty => write!(f, "No cursors supplied"),
350            Self::Animation => write!(f, "A supplied cursor is an animtion"),
351        }
352    }
353}
354
355impl Error for BadAnimation {}
356
357#[cfg(not(web_platform))]
358struct PlatformCustomCursorFuture;
359
360#[derive(Debug)]
361pub struct CustomCursorFuture(pub(crate) PlatformCustomCursorFuture);
362
363impl Future for CustomCursorFuture {
364    type Output = Result<CustomCursor, CustomCursorError>;
365
366    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
367        Pin::new(&mut self.0)
368            .poll(cx)
369            .map_ok(|cursor| CustomCursor { inner: cursor })
370    }
371}
372
373#[derive(Clone, Debug)]
374pub enum CustomCursorError {
375    Blob,
376    Decode(String),
377    Animation,
378}
379
380impl Display for CustomCursorError {
381    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
382        match self {
383            Self::Blob => write!(f, "failed to create `Blob`"),
384            Self::Decode(error) => write!(f, "failed to decode image: {error}"),
385            Self::Animation => {
386                write!(f, "found `CustomCursor` that is an animation when building an animation")
387            }
388        }
389    }
390}