winit/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(not(web_platform), doc = "", doc = "[`HtmlCanvasElement`]: #only-available-on-wasm")]
113 fn with_canvas(self, canvas: Option<HtmlCanvasElement>) -> Self;
114
115 /// Sets whether `event.preventDefault()` should be called on events on the
116 /// canvas that have side effects.
117 ///
118 /// See [`Window::set_prevent_default()`] for more details.
119 ///
120 /// Enabled by default.
121 fn with_prevent_default(self, prevent_default: bool) -> Self;
122
123 /// Whether the canvas should be focusable using the tab key. This is necessary to capture
124 /// canvas keyboard events.
125 ///
126 /// Enabled by default.
127 fn with_focusable(self, focusable: bool) -> Self;
128
129 /// On window creation, append the canvas element to the web page if it isn't already.
130 ///
131 /// Disabled by default.
132 fn with_append(self, append: bool) -> Self;
133}
134
135impl WindowAttributesExtWebSys for WindowAttributes {
136 fn with_canvas(mut self, canvas: Option<HtmlCanvasElement>) -> Self {
137 self.platform_specific.set_canvas(canvas);
138 self
139 }
140
141 fn with_prevent_default(mut self, prevent_default: bool) -> Self {
142 self.platform_specific.prevent_default = prevent_default;
143 self
144 }
145
146 fn with_focusable(mut self, focusable: bool) -> Self {
147 self.platform_specific.focusable = focusable;
148 self
149 }
150
151 fn with_append(mut self, append: bool) -> Self {
152 self.platform_specific.append = append;
153 self
154 }
155}
156
157/// Additional methods on `EventLoop` that are specific to the web.
158pub trait EventLoopExtWebSys {
159 /// A type provided by the user that can be passed through `Event::UserEvent`.
160 type UserEvent: 'static;
161
162 /// Initializes the winit event loop.
163 ///
164 /// Unlike
165 #[cfg_attr(all(web_platform, target_feature = "exception-handling"), doc = "`run_app()`")]
166 #[cfg_attr(
167 not(all(web_platform, target_feature = "exception-handling")),
168 doc = "[`run_app()`]"
169 )]
170 /// [^1], this returns immediately, and doesn't throw an exception in order to
171 /// satisfy its [`!`] return type.
172 ///
173 /// Once the event loop has been destroyed, it's possible to reinitialize another event loop
174 /// by calling this function again. This can be useful if you want to recreate the event loop
175 /// while the WebAssembly module is still loaded. For example, this can be used to recreate the
176 /// event loop when switching between tabs on a single page application.
177 #[rustfmt::skip]
178 ///
179 #[cfg_attr(
180 not(all(web_platform, target_feature = "exception-handling")),
181 doc = "[`run_app()`]: EventLoop::run_app()"
182 )]
183 /// [^1]: `run_app()` is _not_ available on WASM when the target supports `exception-handling`.
184 fn spawn_app<A: ApplicationHandler<Self::UserEvent> + 'static>(self, app: A);
185
186 /// See [`spawn_app`].
187 ///
188 /// [`spawn_app`]: Self::spawn_app
189 #[deprecated = "use EventLoopExtWebSys::spawn_app"]
190 fn spawn<F>(self, event_handler: F)
191 where
192 F: 'static + FnMut(Event<Self::UserEvent>, &ActiveEventLoop);
193
194 /// Sets the strategy for [`ControlFlow::Poll`].
195 ///
196 /// See [`PollStrategy`].
197 ///
198 /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
199 fn set_poll_strategy(&self, strategy: PollStrategy);
200
201 /// Gets the strategy for [`ControlFlow::Poll`].
202 ///
203 /// See [`PollStrategy`].
204 ///
205 /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
206 fn poll_strategy(&self) -> PollStrategy;
207
208 /// Sets the strategy for [`ControlFlow::WaitUntil`].
209 ///
210 /// See [`WaitUntilStrategy`].
211 ///
212 /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
213 fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy);
214
215 /// Gets the strategy for [`ControlFlow::WaitUntil`].
216 ///
217 /// See [`WaitUntilStrategy`].
218 ///
219 /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
220 fn wait_until_strategy(&self) -> WaitUntilStrategy;
221}
222
223impl<T> EventLoopExtWebSys for EventLoop<T> {
224 type UserEvent = T;
225
226 fn spawn_app<A: ApplicationHandler<Self::UserEvent> + 'static>(self, mut app: A) {
227 self.event_loop.spawn(move |event, event_loop| {
228 event_loop::dispatch_event_for_app(&mut app, event_loop, event)
229 });
230 }
231
232 fn spawn<F>(self, event_handler: F)
233 where
234 F: 'static + FnMut(Event<Self::UserEvent>, &ActiveEventLoop),
235 {
236 self.event_loop.spawn(event_handler)
237 }
238
239 fn set_poll_strategy(&self, strategy: PollStrategy) {
240 self.event_loop.set_poll_strategy(strategy);
241 }
242
243 fn poll_strategy(&self) -> PollStrategy {
244 self.event_loop.poll_strategy()
245 }
246
247 fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
248 self.event_loop.set_wait_until_strategy(strategy);
249 }
250
251 fn wait_until_strategy(&self) -> WaitUntilStrategy {
252 self.event_loop.wait_until_strategy()
253 }
254}
255
256pub trait ActiveEventLoopExtWebSys {
257 /// Sets the strategy for [`ControlFlow::Poll`].
258 ///
259 /// See [`PollStrategy`].
260 ///
261 /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
262 fn set_poll_strategy(&self, strategy: PollStrategy);
263
264 /// Gets the strategy for [`ControlFlow::Poll`].
265 ///
266 /// See [`PollStrategy`].
267 ///
268 /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
269 fn poll_strategy(&self) -> PollStrategy;
270
271 /// Sets the strategy for [`ControlFlow::WaitUntil`].
272 ///
273 /// See [`WaitUntilStrategy`].
274 ///
275 /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
276 fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy);
277
278 /// Gets the strategy for [`ControlFlow::WaitUntil`].
279 ///
280 /// See [`WaitUntilStrategy`].
281 ///
282 /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
283 fn wait_until_strategy(&self) -> WaitUntilStrategy;
284
285 /// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the
286 /// cursor has completely finished loading.
287 fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture;
288}
289
290impl ActiveEventLoopExtWebSys for ActiveEventLoop {
291 #[inline]
292 fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture {
293 self.p.create_custom_cursor_async(source)
294 }
295
296 #[inline]
297 fn set_poll_strategy(&self, strategy: PollStrategy) {
298 self.p.set_poll_strategy(strategy);
299 }
300
301 #[inline]
302 fn poll_strategy(&self) -> PollStrategy {
303 self.p.poll_strategy()
304 }
305
306 #[inline]
307 fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
308 self.p.set_wait_until_strategy(strategy);
309 }
310
311 #[inline]
312 fn wait_until_strategy(&self) -> WaitUntilStrategy {
313 self.p.wait_until_strategy()
314 }
315}
316
317/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll].
318#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
319pub enum PollStrategy {
320 /// Uses [`Window.requestIdleCallback()`] to queue the next event loop. If not available
321 /// this will fallback to [`setTimeout()`].
322 ///
323 /// This strategy will wait for the browser to enter an idle period before running and might
324 /// be affected by browser throttling.
325 ///
326 /// [`Window.requestIdleCallback()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
327 /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
328 IdleCallback,
329 /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available
330 /// this will fallback to [`setTimeout()`].
331 ///
332 /// This strategy will run as fast as possible without disturbing users from interacting with
333 /// the page and is not affected by browser throttling.
334 ///
335 /// This is the default strategy.
336 ///
337 /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
338 /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
339 #[default]
340 Scheduler,
341}
342
343/// Strategy used for [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil].
344#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
345pub enum WaitUntilStrategy {
346 /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available
347 /// this will fallback to [`setTimeout()`].
348 ///
349 /// This strategy is commonly not affected by browser throttling unless the window is not
350 /// focused.
351 ///
352 /// This is the default strategy.
353 ///
354 /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
355 /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
356 #[default]
357 Scheduler,
358 /// Equal to [`Scheduler`][Self::Scheduler] but wakes up the event loop from a [worker].
359 ///
360 /// This strategy is commonly not affected by browser throttling regardless of window focus.
361 ///
362 /// [worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
363 Worker,
364}
365
366pub trait CustomCursorExtWebSys {
367 /// Returns if this cursor is an animation.
368 fn is_animation(&self) -> bool;
369
370 /// Creates a new cursor from a URL pointing to an image.
371 /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
372 /// but browser support for image formats is inconsistent. Using [PNG] is recommended.
373 ///
374 /// [PNG]: https://en.wikipedia.org/wiki/PNG
375 fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource;
376
377 /// Crates a new animated cursor from multiple [`CustomCursor`]s.
378 /// Supplied `cursors` can't be empty or other animations.
379 fn from_animation(
380 duration: Duration,
381 cursors: Vec<CustomCursor>,
382 ) -> Result<CustomCursorSource, BadAnimation>;
383}
384
385impl CustomCursorExtWebSys for CustomCursor {
386 fn is_animation(&self) -> bool {
387 self.inner.animation
388 }
389
390 fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource {
391 CustomCursorSource { inner: PlatformCustomCursorSource::Url { url, hotspot_x, hotspot_y } }
392 }
393
394 fn from_animation(
395 duration: Duration,
396 cursors: Vec<CustomCursor>,
397 ) -> Result<CustomCursorSource, BadAnimation> {
398 if cursors.is_empty() {
399 return Err(BadAnimation::Empty);
400 }
401
402 if cursors.iter().any(CustomCursor::is_animation) {
403 return Err(BadAnimation::Animation);
404 }
405
406 Ok(CustomCursorSource {
407 inner: PlatformCustomCursorSource::Animation { duration, cursors },
408 })
409 }
410}
411
412/// An error produced when using [`CustomCursor::from_animation`] with invalid arguments.
413#[derive(Debug, Clone)]
414pub enum BadAnimation {
415 /// Produced when no cursors were supplied.
416 Empty,
417 /// Produced when a supplied cursor is an animation.
418 Animation,
419}
420
421impl fmt::Display for BadAnimation {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 match self {
424 Self::Empty => write!(f, "No cursors supplied"),
425 Self::Animation => write!(f, "A supplied cursor is an animation"),
426 }
427 }
428}
429
430impl Error for BadAnimation {}
431
432#[cfg(not(web_platform))]
433struct PlatformCustomCursorFuture;
434
435#[derive(Debug)]
436pub struct CustomCursorFuture(pub(crate) PlatformCustomCursorFuture);
437
438impl Future for CustomCursorFuture {
439 type Output = Result<CustomCursor, CustomCursorError>;
440
441 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
442 Pin::new(&mut self.0).poll(cx).map_ok(|cursor| CustomCursor { inner: cursor })
443 }
444}
445
446#[derive(Clone, Debug)]
447pub enum CustomCursorError {
448 Blob,
449 Decode(String),
450 Animation,
451}
452
453impl Display for CustomCursorError {
454 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
455 match self {
456 Self::Blob => write!(f, "failed to create `Blob`"),
457 Self::Decode(error) => write!(f, "failed to decode image: {error}"),
458 Self::Animation => {
459 write!(f, "found `CustomCursor` that is an animation when building an animation")
460 },
461 }
462 }
463}
464
465impl Error for CustomCursorError {}