smudge_platform/window.rs
1//! The window, its run loop and the frames it presents.
2
3use std::num::NonZeroU32;
4use std::rc::Rc;
5use std::sync::Arc;
6
7use smudge_core::app::{App, AppConfig, FrameOutput, FrameStats, RepaintMode};
8use smudge_core::clipboard::{Clipboard, MemoryClipboard};
9use smudge_core::element::IntoElement;
10use smudge_core::geometry::{Color, PixelRect, Rect, Size};
11use smudge_cpu::CpuRenderer;
12use softbuffer::{Context, Surface};
13use web_time::Instant;
14use winit::application::ApplicationHandler;
15use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
16use winit::event::WindowEvent;
17use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
18use winit::raw_window_handle::{
19 DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle,
20};
21use winit::window::{Window, WindowId};
22
23#[cfg(not(target_arch = "wasm32"))]
24use crate::clipboard::SystemClipboard;
25use crate::error::PlatformError;
26use crate::events::EventTranslator;
27use crate::present::{
28 DamageHistory, PresentStats, PresentStatsHandle, copy_rect, damage_region, present_skipped,
29 tint_rects,
30};
31use crate::renderer::RendererChoice;
32#[cfg(feature = "gpu")]
33use smudge_gpu::AdapterSummary;
34
35/// The user event the run loop carries, of which the accessibility adapter
36/// sends the only kind.
37///
38/// With the `a11y` feature off the run loop carries `()` and nothing is ever
39/// sent.
40#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
41pub(crate) enum PlatformEvent {
42 /// An event from the accessibility adapter: a request for the initial
43 /// tree, an action a user ran in an assistive tool, or a notice that the
44 /// platform stopped reading the tree.
45 A11y(accesskit_winit::Event),
46}
47
48#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
49impl From<accesskit_winit::Event> for PlatformEvent {
50 fn from(event: accesskit_winit::Event) -> PlatformEvent {
51 PlatformEvent::A11y(event)
52 }
53}
54
55/// The user event type the run loop is built with.
56#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
57type UserEvent = PlatformEvent;
58#[cfg(any(not(feature = "a11y"), target_arch = "wasm32"))]
59type UserEvent = ();
60
61/// The measurement hook that receives one pair of statistics per presented
62/// frame.
63///
64/// The platform calls it after each present with the frame's pipeline
65/// statistics and what the present did. Nothing in the library sets it: it
66/// exists so a benchmark can record one entry per frame while the frames run.
67pub type FrameHook = Rc<dyn Fn(FrameStats, PresentStats)>;
68
69/// The measurement hook that injects scripted input before a frame runs.
70///
71/// The platform calls it with the app before every frame but the first, which
72/// mounts the app and lays it out, and the hook delivers input through [`App::handle_event`], which is the path every real
73/// event takes. Nothing in the library sets it: it exists so a benchmark can
74/// script input on the window backend, where nothing else can reach the app
75/// between frames. The time from a call to the end of that frame's present is
76/// the event's latency. Like [`FrameHook`], the hook is shared, so it keeps
77/// any counter it needs behind a `Cell` or an `Rc`.
78pub type EventHook = Rc<dyn Fn(&mut App)>;
79
80/// The settings a window opens with.
81#[derive(Clone)]
82pub struct WindowOptions {
83 /// The text shown in the window's title bar.
84 pub title: String,
85 /// The size of the window's content area in logical pixels.
86 pub size: Size,
87 /// The color the renderer fills the surface with before it replays the
88 /// display list.
89 ///
90 /// Must be opaque, because presentation copies only the color channels of
91 /// the surface into the window. A run with a color that is not opaque
92 /// returns [`PlatformError::ClearColor`] before a window opens.
93 pub clear_color: Color,
94 /// How much of the surface each frame repaints.
95 pub repaint: RepaintMode,
96 /// Whether every present tints the pixels the frame repainted.
97 ///
98 /// A debugging aid for damage regressions: while it is on, every present
99 /// copies the whole pixmap and blends a translucent tint over the frame's
100 /// damage rectangles in the window buffer alone, so the window shows what
101 /// each frame paid for. Damage presentation is off while the overlay is
102 /// on, and neither the renderer's pixmap nor a later frame keeps a tint.
103 ///
104 /// The overlay exists on the CPU renderer only, so it makes
105 /// [`RendererChoice::Auto`] pick the CPU renderer, and it cannot be
106 /// combined with [`RendererChoice::Gpu`].
107 pub show_damage: bool,
108 /// Which renderer presents the frames.
109 ///
110 /// [`RendererChoice::Auto`] uses the GPU renderer only when the window's
111 /// adapter is a real GPU, and falls back to the CPU renderer on a software
112 /// rasterizer, without one, or on a window the GPU cannot present to. It
113 /// is the default in builds with the `gpu` feature. [`RendererChoice::Gpu`]
114 /// never falls back: it fails the run when the `gpu` feature is off or no
115 /// usable adapter exists.
116 ///
117 /// The [`crate::renderer::RENDERER_ENVIRONMENT`] variable, set to `cpu`,
118 /// `gpu` or `auto`, overrides the value here when the window opens, so a
119 /// machine can switch renderer without a rebuild.
120 pub renderer: RendererChoice,
121 /// Where the platform publishes presentation statistics, when given.
122 ///
123 /// Pass a [`PresentStatsHandle`] and keep the original to read the
124 /// [`PresentStats`] of the most recent present while the window runs.
125 /// Without one the platform skips publishing.
126 pub stats: Option<PresentStatsHandle>,
127 /// Measurement hook: runs after every presented frame.
128 ///
129 /// The hook receives the frame's [`FrameStats`] and the present's
130 /// [`PresentStats`], in that order. It runs on the main thread between
131 /// frames and must not touch the window. A frame rendered while the
132 /// surface has no pixel, for example while the window is minimized, is not
133 /// presented, so it neither calls the hook nor counts toward the frame
134 /// limit.
135 pub on_frame: Option<FrameHook>,
136 /// Measurement hook: injects input before every frame.
137 ///
138 /// The hook receives the app and may hand it input events through
139 /// [`App::handle_event`], which is the path every real event takes, so the
140 /// frame that follows shows the event and measures its latency. A run with
141 /// this hook draws frames back to back like a run with a tick. Like a
142 /// ticked frame, a frame whose input changed nothing presents nothing, so
143 /// it neither calls [`WindowOptions::on_frame`] nor counts toward the
144 /// frame limit. A hook that must reach its limit changes something in
145 /// every frame.
146 pub on_event: Option<EventHook>,
147 /// Measurement hook: closes the window after this many presented frames.
148 ///
149 /// A run that must last a fixed number of frames sets this instead of
150 /// waiting for the user to close the window. Until the limit is reached,
151 /// every presented frame asks for the next one, as if the app ran a tick,
152 /// so a window whose app has nothing more to draw still closes: such a
153 /// frame presents the last picture again, paced by the display. A frame
154 /// that presents nothing, because the surface has no pixels to show, asks
155 /// for none, so the run waits for the window to change instead of
156 /// spinning.
157 pub frame_limit: Option<u64>,
158}
159
160impl std::fmt::Debug for WindowOptions {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 f.debug_struct("WindowOptions")
163 .field("title", &self.title)
164 .field("size", &self.size)
165 .field("clear_color", &self.clear_color)
166 .field("repaint", &self.repaint)
167 .field("show_damage", &self.show_damage)
168 .field("renderer", &self.renderer)
169 .field("stats", &self.stats)
170 .field("on_frame", &self.on_frame.is_some())
171 .field("on_event", &self.on_event.is_some())
172 .field("frame_limit", &self.frame_limit)
173 .finish()
174 }
175}
176
177/// Opens a window and runs frames until the window closes.
178///
179/// `build` runs once, when the window first appears, and mounts the element it
180/// returns. A frame runs after a reactive signal wakes an effect, after a
181/// resize or a scale change, and once per frame while a tick is installed.
182/// Otherwise the window waits for the next event.
183///
184/// Call this from the main thread. The event loop takes over the calling
185/// thread until the window closes.
186///
187/// Platform-specific:
188///
189/// - **Web:** the loop runs on the browser's own event loop, so this returns
190/// `Ok(())` as soon as the loop is started, before the window opens. See
191/// [`run_with`] for the details.
192///
193/// # Errors
194///
195/// Returns [`PlatformError::ClearColor`] when [`WindowOptions::clear_color`]
196/// is not opaque, [`PlatformError::Renderer`] when the
197/// [`WindowOptions::renderer`] choice or its environment override cannot be
198/// used, [`PlatformError::EventLoop`] when the event loop cannot be
199/// created or run, [`PlatformError::Window`] when the window cannot be
200/// created, and [`PlatformError::Surface`] when the presentation surface
201/// cannot be created, resized or presented.
202///
203/// # Examples
204///
205/// ```no_run
206/// use smudge_core::app::RepaintMode;
207/// use smudge_core::geometry::{Color, Size};
208/// use smudge_core::style::px;
209/// use smudge_core::widget::panel;
210/// use smudge_platform::{RendererChoice, WindowOptions, run};
211///
212/// run(
213/// WindowOptions {
214/// title: String::from("Smudge"),
215/// size: Size {
216/// width: 320.0,
217/// height: 200.0,
218/// },
219/// clear_color: Color::WHITE,
220/// repaint: RepaintMode::Damage,
221/// show_damage: false,
222/// renderer: RendererChoice::default(),
223/// stats: None,
224/// on_frame: None,
225/// on_event: None,
226/// frame_limit: None,
227/// },
228/// || panel().width(px(120.0)).height(px(40.0)).background(Color::BLACK),
229/// )?;
230/// # Ok::<(), smudge_platform::PlatformError>(())
231/// ```
232pub fn run<E: IntoElement + 'static>(
233 options: WindowOptions,
234 build: impl FnOnce() -> E + 'static,
235) -> Result<(), PlatformError> {
236 run_with(options, build, |_| {})
237}
238
239/// Opens a window, hands the app to `configure`, and runs frames until the
240/// window closes.
241///
242/// Behaves like [`run`], and calls `configure` once after `build` has mounted
243/// its element and before the first frame. Use it to install a per-frame
244/// driver with [`App::set_tick`], or to set up measurement that must run
245/// alongside the frames.
246///
247/// Platform-specific:
248///
249/// - **Web:** the window is a canvas appended to the page's body, at
250/// [`WindowOptions::size`] unless the page's style sheet sizes it with
251/// `!important`, and it follows the size the page gives it. The loop runs
252/// on the browser's own event loop, so this returns `Ok(())` as soon as
253/// the loop is started, before the window opens. Only
254/// failures before that point, such as a clear color that is not opaque,
255/// come back as an error; a failure later in the run stops the loop
256/// without a way to report it.
257///
258/// # Errors
259///
260/// Returns [`PlatformError::ClearColor`] when [`WindowOptions::clear_color`]
261/// is not opaque, checked before the event loop is created,
262/// [`PlatformError::Renderer`] when the [`WindowOptions::renderer`] choice
263/// or its environment override cannot be used, and otherwise the same errors
264/// as [`run`].
265///
266/// # Examples
267///
268/// ```no_run
269/// use smudge_core::app::RepaintMode;
270/// use smudge_core::geometry::{Color, Size};
271/// use smudge_core::reactive::{self, prelude::*};
272/// use smudge_core::style::px;
273/// use smudge_core::widget::panel;
274/// use smudge_platform::{RendererChoice, WindowOptions, run_with};
275///
276/// let (color, set_color) = reactive::signal_local(Color::BLACK);
277/// run_with(
278/// WindowOptions {
279/// title: String::from("Smudge"),
280/// size: Size {
281/// width: 320.0,
282/// height: 200.0,
283/// },
284/// clear_color: Color::WHITE,
285/// repaint: RepaintMode::Damage,
286/// show_damage: false,
287/// renderer: RendererChoice::default(),
288/// stats: None,
289/// on_frame: None,
290/// on_event: None,
291/// frame_limit: None,
292/// },
293/// move || panel().width(px(120.0)).background(move || color.get().into()),
294/// move |app| {
295/// let mut dark = false;
296/// app.set_tick(move |_| {
297/// dark = !dark;
298/// set_color.set(if dark { Color::BLACK } else { Color::WHITE });
299/// });
300/// },
301/// )?;
302/// # Ok::<(), smudge_platform::PlatformError>(())
303/// ```
304pub fn run_with<E: IntoElement + 'static>(
305 options: WindowOptions,
306 build: impl FnOnce() -> E + 'static,
307 configure: impl FnOnce(&mut App) + 'static,
308) -> Result<(), PlatformError> {
309 if !options.clear_color.is_opaque() {
310 return Err(PlatformError::ClearColor);
311 }
312 let renderer = options.renderer.with_env_override()?;
313
314 let event_loop = EventLoop::<UserEvent>::with_user_event()
315 .build()
316 .map_err(PlatformError::EventLoop)?;
317 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
318 let proxy = event_loop.create_proxy();
319 // The web loop takes the state by value, so nothing mutates it here.
320 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
321 let mut platform = Platform {
322 renderer,
323 options,
324 build: Some(build),
325 configure: Some(Box::new(configure)),
326 state: None,
327 error: None,
328 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
329 proxy,
330 };
331 // The web loop runs on the browser's own event loop and never runs to
332 // completion, so it is started and can no longer return an error. A
333 // failure after it started only stops the run.
334 #[cfg(target_arch = "wasm32")]
335 {
336 use winit::platform::web::EventLoopExtWebSys;
337 event_loop.spawn_app(platform);
338 Ok(())
339 }
340 #[cfg(not(target_arch = "wasm32"))]
341 {
342 event_loop
343 .run_app(&mut platform)
344 .map_err(PlatformError::EventLoop)?;
345 match platform.error.take() {
346 Some(error) => Err(error),
347 None => Ok(()),
348 }
349 }
350}
351
352/// The state the event loop drives.
353struct Platform<E, B>
354where
355 E: IntoElement,
356 B: FnOnce() -> E,
357{
358 /// The renderer the window presents with, after the environment override.
359 renderer: RendererChoice,
360 /// The settings the window opens with.
361 options: WindowOptions,
362 /// The element to mount, until the window first appears.
363 build: Option<B>,
364 /// The caller's setup, run once the app exists.
365 configure: Option<Configure>,
366 /// The window, its app and its renderer, once the window exists.
367 state: Option<WindowState>,
368 /// The first error that stopped the run.
369 error: Option<PlatformError>,
370 /// The sender the accessibility adapter posts its events through.
371 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
372 proxy: winit::event_loop::EventLoopProxy<UserEvent>,
373}
374
375/// The setup a caller hands to [`run_with`], boxed so the platform state does
376/// not become generic over it.
377type Configure = Box<dyn FnOnce(&mut App)>;
378
379/// One live window and everything it presents.
380struct WindowState {
381 /// The window the surface presents to.
382 ///
383 /// The shared handle is an `Arc` because the wgpu surface target must be
384 /// sendable; everything else in the window stays on the main thread.
385 window: Arc<Window>,
386 /// The app whose frames the window runs.
387 app: App,
388 /// The translator that turns window events into input events.
389 translator: EventTranslator,
390 /// The renderer and presentation path the frames go through.
391 presenter: Presenter,
392 /// What the last present did.
393 stats: PresentStats,
394 /// Where to publish the statistics, when the caller asked for them.
395 stats_handle: Option<PresentStatsHandle>,
396 /// The measurement hook to call after every presented frame.
397 on_frame: Option<FrameHook>,
398 /// The measurement hook to call before every frame.
399 on_event: Option<EventHook>,
400 /// Whether the app has run a frame. The event hook waits for that, since
401 /// a real event cannot arrive before the app has drawn once, and a hit
402 /// test before the first layout has no rectangles to hit.
403 rendered: bool,
404 /// How many frames to present before the window closes.
405 frame_limit: Option<u64>,
406 /// How many frames the window has presented.
407 frames_presented: u64,
408 /// The number of physical pixels per logical pixel the app last used.
409 scale: f32,
410 /// The window's size in physical pixels when the app last used it.
411 physical: (u32, u32),
412 /// The IME area the window last reported, in logical pixels, or `None`
413 /// while no widget reports one.
414 ime_area: Option<Rect>,
415 /// The scale the IME area was last reported under. The window system
416 /// turns the area into physical pixels with the scale of the moment, so
417 /// an unchanged logical area is reported again after a scale change.
418 ime_scale: f32,
419 /// Whether the window currently accepts input method input.
420 ime_allowed: bool,
421 /// The bridge that maps the app's accessibility projection onto the
422 /// platform's, when the platform builds with accessibility.
423 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
424 adapter: accesskit_winit::Adapter,
425 /// The projection that builds the updates the adapter sends.
426 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
427 a11y: smudge_a11y::Projection,
428 /// Whether the accessibility backend has asked for the tree and has not
429 /// stopped reading it since. While it is false, no projection work runs.
430 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
431 a11y_active: bool,
432 /// Whether the next update must carry the whole tree, because the
433 /// backend asked for it or the scale moved every node's bounds.
434 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
435 a11y_full: bool,
436}
437
438/// The frame that was rendered and waits to be presented.
439#[derive(Default)]
440struct PendingFrame {
441 /// The rectangles the frame changed, in physical pixels.
442 damage: Vec<PixelRect>,
443 /// Whether the frame repainted the whole surface.
444 full: bool,
445}
446
447impl<E, B> Platform<E, B>
448where
449 E: IntoElement + 'static,
450 B: FnOnce() -> E + 'static,
451{
452 /// Creates the window, its app and its presentation path.
453 fn create(&mut self, event_loop: &ActiveEventLoop) -> Result<WindowState, PlatformError> {
454 let attributes = Window::default_attributes()
455 .with_title(self.options.title.clone())
456 .with_inner_size(LogicalSize::new(
457 f64::from(self.options.size.width),
458 f64::from(self.options.size.height),
459 ))
460 // The accessibility adapter must be attached before the window is
461 // shown, so the window starts hidden and is shown right after.
462 .with_visible(false);
463 // A web window draws into a canvas, which the page shows only once it
464 // is part of the document.
465 #[cfg(target_arch = "wasm32")]
466 let attributes = {
467 use winit::platform::web::WindowAttributesExtWebSys;
468 attributes.with_append(true)
469 };
470 let window = Arc::new(
471 event_loop
472 .create_window(attributes)
473 .map_err(PlatformError::Window)?,
474 );
475 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
476 let adapter = accesskit_winit::Adapter::with_event_loop_proxy(
477 event_loop,
478 &window,
479 self.proxy.clone(),
480 );
481 window.set_visible(true);
482
483 let scale = window.scale_factor() as f32;
484 let physical = window.inner_size();
485 let build = self
486 .build
487 .take()
488 .expect("the window is created only once, so its element is still waiting");
489 let mut app = App::new(
490 AppConfig {
491 viewport: logical_size(physical, scale),
492 scale,
493 clear_color: self.options.clear_color,
494 repaint: self.options.repaint,
495 },
496 build,
497 );
498 if let Some(configure) = self.configure.take() {
499 configure(&mut app);
500 }
501
502 let (width, height) = app.surface_size();
503 let presenter = create_presenter(
504 &window,
505 self.renderer,
506 width,
507 height,
508 self.options.show_damage,
509 )?;
510 #[cfg(feature = "gpu")]
511 if let (Some(handle), Some(summary)) =
512 (self.options.stats.as_ref(), presenter.adapter_summary())
513 {
514 handle.set_adapter(summary);
515 }
516
517 let weak = Arc::downgrade(&window);
518 app.set_wake_callback(move || {
519 if let Some(window) = weak.upgrade() {
520 window.request_redraw();
521 }
522 });
523 window.request_redraw();
524
525 // The app gets the system clipboard when it can be opened, and an
526 // in-memory one when it cannot, so cut, copy and paste keep working
527 // inside the app either way. The web page has no system clipboard to
528 // open, so it always gets the in-memory one.
529 #[cfg(not(target_arch = "wasm32"))]
530 let clipboard: Box<dyn Clipboard> = match SystemClipboard::new() {
531 Ok(clipboard) => Box::new(clipboard),
532 Err(_) => Box::new(MemoryClipboard::default()),
533 };
534 #[cfg(target_arch = "wasm32")]
535 let clipboard: Box<dyn Clipboard> = Box::new(MemoryClipboard::default());
536 app.set_clipboard(clipboard);
537
538 Ok(WindowState {
539 window,
540 app,
541 translator: EventTranslator::new(),
542 presenter,
543 stats: PresentStats::default(),
544 stats_handle: self.options.stats.clone(),
545 on_frame: self.options.on_frame.take(),
546 on_event: self.options.on_event.take(),
547 rendered: false,
548 frame_limit: self.options.frame_limit,
549 frames_presented: 0,
550 scale,
551 physical: (physical.width, physical.height),
552 ime_area: None,
553 ime_scale: scale,
554 ime_allowed: false,
555 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
556 adapter,
557 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
558 a11y: smudge_a11y::Projection::new(self.options.title.clone()),
559 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
560 a11y_active: false,
561 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
562 a11y_full: false,
563 })
564 }
565
566 /// Changes the size and the scale the app and the renderer work at.
567 fn resize(&mut self, physical: PhysicalSize<u32>, scale: f64) -> Result<(), PlatformError> {
568 let Some(state) = self.state.as_mut() else {
569 return Ok(());
570 };
571 let scale = scale as f32;
572 if !scale.is_finite() || scale <= 0.0 {
573 return Ok(());
574 }
575 if physical.width == 0 || physical.height == 0 {
576 return Ok(());
577 }
578 if state.scale == scale && state.physical == (physical.width, physical.height) {
579 return Ok(());
580 }
581
582 // The app learns the new viewport only after the presenter took the
583 // new size, so a failed resize leaves the presenter and the app
584 // consistent at the old size instead of an app whose next frame
585 // reaches past the surface it renders onto.
586 let (Some(width), Some(height)) = (
587 NonZeroU32::new(physical.width),
588 NonZeroU32::new(physical.height),
589 ) else {
590 return Ok(());
591 };
592 state.presenter.resize(width, height)?;
593 state.app.set_viewport(logical_size(physical, scale), scale);
594 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
595 if state.scale != scale {
596 state.a11y_full = true;
597 }
598 debug_assert_eq!(
599 state.app.surface_size(),
600 (width.get(), height.get()),
601 "the app's surface size must match the resized surface and renderer"
602 );
603 state.scale = scale;
604 state.physical = (physical.width, physical.height);
605 state.window.request_redraw();
606 Ok(())
607 }
608
609 /// Runs one frame and presents it.
610 ///
611 /// When an [`WindowOptions::on_event`] hook is set, it injects its input
612 /// first, so the frame shows it, and the run draws frames back to back
613 /// like a run whose app has a tick. The first frame gets no injection:
614 /// nothing real arrives before the app has drawn once, and a hit test
615 /// before the first layout has no rectangles to hit.
616 fn redraw(&mut self) -> Result<(), PlatformError> {
617 let Some(state) = self.state.as_mut() else {
618 return Ok(());
619 };
620 if state.rendered
621 && let Some(on_event) = &state.on_event
622 {
623 (on_event)(&mut state.app);
624 }
625 let driven = state.app.has_tick() || state.on_event.is_some();
626 let output = state.app.frame();
627 state.rendered = true;
628 let presented = state.presenter.render_and_present(&output, driven)?;
629 if let Some(stats) = presented {
630 state.stats = stats;
631 if let Some(handle) = &state.stats_handle {
632 handle.store(stats);
633 }
634 state.frames_presented += 1;
635 if let Some(on_frame) = &state.on_frame {
636 on_frame(state.app.stats().clone(), stats);
637 }
638 }
639 state.update_ime();
640 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
641 state.update_a11y();
642 if driven || (presented.is_some() && state.below_frame_limit()) {
643 state.window.request_redraw();
644 }
645 Ok(())
646 }
647
648 /// Runs the timers that are due and puts the event loop to sleep until
649 /// the next one.
650 ///
651 /// An app with a tick or an event hook keeps itself awake and needs
652 /// nothing here. Any
653 /// other app runs its due timers between frames: one that writes a
654 /// signal wakes the window through the app's wake callback, and one that
655 /// changes nothing draws no frame, so it never costs a present. The loop
656 /// then waits until the soonest timer, or for events when none is left.
657 /// A timer that is due again at once, such as one a callback just
658 /// scheduled with no delay, asks for a frame, which runs it.
659 fn wait_for_timers(&mut self, event_loop: &ActiveEventLoop) {
660 let Some(state) = self.state.as_mut() else {
661 return;
662 };
663 if state.app.has_tick() || state.on_event.is_some() {
664 return;
665 }
666 if wait_decision(state.app.next_deadline(), Instant::now()) == WaitDecision::Now {
667 state.app.run_due_timers();
668 if state.app.needs_frame() {
669 state.window.request_redraw();
670 }
671 }
672 match wait_decision(state.app.next_deadline(), Instant::now()) {
673 WaitDecision::Now => state.window.request_redraw(),
674 WaitDecision::Until(deadline) => {
675 event_loop.set_control_flow(ControlFlow::WaitUntil(deadline));
676 }
677 WaitDecision::Wait => event_loop.set_control_flow(ControlFlow::Wait),
678 }
679 }
680
681 /// Records `error` and stops the event loop.
682 fn fail(&mut self, event_loop: &ActiveEventLoop, error: PlatformError) {
683 if self.error.is_none() {
684 self.error = Some(error);
685 }
686 event_loop.exit();
687 }
688
689 /// Answers an event the accessibility adapter posted.
690 ///
691 /// The first request for the tree is answered with a full update, which
692 /// is also what turns the projection on: until the backend asks, no
693 /// frame spends any work on it. A request that arrives while a frame is
694 /// pending waits for that frame, so the tree it gets has been laid out
695 /// and every later change is queued after it. An action presses or focuses the widget
696 /// the request names, through the same paths a pointer press and a
697 /// keyboard focus change take. When the platform stops reading the tree,
698 /// the projection goes quiet until the backend asks again.
699 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
700 fn handle_a11y_event(&mut self, event: accesskit_winit::Event) {
701 let Some(state) = self.state.as_mut() else {
702 return;
703 };
704 match event.window_event {
705 accesskit_winit::WindowEvent::InitialTreeRequested => {
706 state.a11y_active = true;
707 state.a11y_full = true;
708 if state.app.needs_frame() {
709 state.window.request_redraw();
710 } else {
711 state.update_a11y();
712 }
713 }
714 accesskit_winit::WindowEvent::ActionRequested(request) => {
715 let id = smudge_core::tree::WidgetId::from_u64(request.target_node.0);
716 match request.action {
717 accesskit::Action::Click => {
718 state.app.press(id);
719 }
720 accesskit::Action::Focus => {
721 state.app.request_focus(id);
722 }
723 _ => {}
724 }
725 if state.app.needs_frame() {
726 state.window.request_redraw();
727 }
728 }
729 accesskit_winit::WindowEvent::AccessibilityDeactivated => {
730 state.a11y_active = false;
731 }
732 }
733 }
734}
735
736impl<E, B> ApplicationHandler<UserEvent> for Platform<E, B>
737where
738 E: IntoElement + 'static,
739 B: FnOnce() -> E + 'static,
740{
741 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
742 if self.state.is_some() {
743 return;
744 }
745 match self.create(event_loop) {
746 Ok(state) => self.state = Some(state),
747 Err(error) => self.fail(event_loop, error),
748 }
749 }
750
751 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
752 fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
753 let UserEvent::A11y(event) = event;
754 self.handle_a11y_event(event);
755 }
756
757 fn window_event(
758 &mut self,
759 event_loop: &ActiveEventLoop,
760 _window_id: WindowId,
761 event: WindowEvent,
762 ) {
763 // The accessibility adapter sees every event before the app does,
764 // because it reacts to moves, resizes and focus changes itself.
765 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
766 if let Some(state) = self.state.as_mut() {
767 state.adapter.process_event(&state.window, &event);
768 }
769 let result = match event {
770 WindowEvent::CloseRequested => {
771 event_loop.exit();
772 Ok(())
773 }
774 WindowEvent::Resized(physical) => {
775 let scale = self
776 .state
777 .as_ref()
778 .map_or(1.0, |state| state.window.scale_factor());
779 self.resize(physical, scale)
780 }
781 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
782 match self.state.as_ref().map(|state| state.window.inner_size()) {
783 Some(physical) => self.resize(physical, scale_factor),
784 None => Ok(()),
785 }
786 }
787 WindowEvent::RedrawRequested => {
788 let result = self.redraw();
789 if result.is_ok() && self.state.as_ref().is_some_and(WindowState::finished) {
790 event_loop.exit();
791 }
792 result
793 }
794 other => {
795 if let Some(state) = self.state.as_mut() {
796 let input = state.translator.translate(&other, state.scale);
797 if let Some(input) = input {
798 state.app.handle_event(input);
799 if state.app.needs_frame() {
800 state.window.request_redraw();
801 }
802 }
803 state.update_ime();
804 }
805 Ok(())
806 }
807 };
808 if let Err(error) = result {
809 self.fail(event_loop, error);
810 }
811 }
812
813 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
814 self.wait_for_timers(event_loop);
815 }
816}
817
818impl WindowState {
819 /// Reports the area the focused widget offers the input method to the
820 /// window system.
821 ///
822 /// This runs after every window event and after every presented frame,
823 /// because the area can move without a pixel changing anywhere: the
824 /// window asks for a frame only when the tree has work that changes
825 /// pixels, so a caret move inside composed text would otherwise leave
826 /// the input method's candidate window where it was. A widget that
827 /// reports no area turns the input method off, which is what a window
828 /// does while no text field holds the focus.
829 fn update_ime(&mut self) {
830 let area = self.app.ime_area();
831 if area == self.ime_area && self.scale == self.ime_scale {
832 return;
833 }
834 self.ime_area = area;
835 self.ime_scale = self.scale;
836 match area {
837 Some(area) => {
838 if !self.ime_allowed {
839 self.ime_allowed = true;
840 self.window.set_ime_allowed(true);
841 }
842 self.window.set_ime_cursor_area(
843 LogicalPosition::new(f64::from(area.x0), f64::from(area.y0)),
844 LogicalSize::new(f64::from(area.width()), f64::from(area.height())),
845 );
846 }
847 None => {
848 if self.ime_allowed {
849 self.ime_allowed = false;
850 self.window.set_ime_allowed(false);
851 }
852 }
853 }
854 }
855
856 /// Sends the accessibility projection to the adapter when the backend is
857 /// reading the tree and something changed.
858 ///
859 /// This runs after every frame. The update is whole when the backend
860 /// asked for the tree or the scale changed, and it is also whole when
861 /// the changes say a frame changed too much to describe one node at a
862 /// time. While the backend is not reading, or while nothing projected
863 /// changed, this builds nothing, and the adapter calls the closure that
864 /// builds an update only while an assistive technology listens.
865 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
866 fn update_a11y(&mut self) {
867 if !self.a11y_active || !(self.a11y_full || self.app.has_a11y_changes()) {
868 return;
869 }
870 let full = std::mem::take(&mut self.a11y_full);
871 let (width, height) = self.physical;
872 let (projection, app, scale) = (&mut self.a11y, &mut self.app, self.scale);
873 self.adapter.update_if_active(|| {
874 projection.update(app, f64::from(width), f64::from(height), scale, full)
875 });
876 }
877
878 /// Returns whether the window has presented the number of frames its frame
879 /// limit sets, or `false` when no limit is set.
880 fn finished(&self) -> bool {
881 self.frame_limit
882 .is_some_and(|limit| self.frames_presented >= limit)
883 }
884
885 /// Returns whether the window has a frame limit it has not reached yet.
886 fn below_frame_limit(&self) -> bool {
887 self.frame_limit
888 .is_some_and(|limit| self.frames_presented < limit)
889 }
890}
891
892/// Builds the presentation path the window opens with.
893///
894/// The GPU path asks the window system for an adapter when the choice can use
895/// one. An automatic choice falls back on the CPU renderer whenever that
896/// fails or finds only a software rasterizer; an explicit GPU request fails
897/// the run instead.
898fn create_presenter(
899 window: &Arc<Window>,
900 choice: RendererChoice,
901 width: u32,
902 height: u32,
903 show_damage: bool,
904) -> Result<Presenter, PlatformError> {
905 #[cfg(not(feature = "gpu"))]
906 {
907 match choice {
908 RendererChoice::Gpu => Err(PlatformError::Renderer(String::from(
909 "the GPU renderer needs a build with the gpu feature",
910 ))),
911 _ => Ok(Presenter::Cpu(Box::new(CpuPresenter::new(
912 window,
913 width,
914 height,
915 show_damage,
916 )?))),
917 }
918 }
919 #[cfg(feature = "gpu")]
920 match choice {
921 RendererChoice::Cpu => Ok(Presenter::Cpu(Box::new(CpuPresenter::new(
922 window,
923 width,
924 height,
925 show_damage,
926 )?))),
927 // The overlay tints the window buffer of the CPU path, so an
928 // automatic choice keeps the CPU renderer while it is on.
929 RendererChoice::Auto if show_damage => Ok(Presenter::Cpu(Box::new(CpuPresenter::new(
930 window,
931 width,
932 height,
933 show_damage,
934 )?))),
935 RendererChoice::Gpu | RendererChoice::Auto => {
936 let explicit = choice == RendererChoice::Gpu;
937 if explicit && show_damage {
938 return Err(PlatformError::Renderer(String::from(
939 "the damage overlay tints the window buffer of the CPU \
940 presentation path and cannot run on the GPU renderer",
941 )));
942 }
943 match crate::gpu::GpuPath::new(window, width, height, explicit)? {
944 Some(gpu) => Ok(Presenter::Gpu(Box::new(gpu))),
945 None => Ok(Presenter::Cpu(Box::new(CpuPresenter::new(
946 window,
947 width,
948 height,
949 show_damage,
950 )?))),
951 }
952 }
953 }
954}
955
956/// The renderer and presentation path a window presents with.
957///
958/// Both paths are boxed: the wgpu handles of the GPU path and the pixmap state
959/// of the CPU path dwarf a boxed pointer, and the enum only dispatches.
960enum Presenter {
961 /// The CPU renderer, presenting through the software surface.
962 Cpu(Box<CpuPresenter>),
963 /// The GPU renderer, presenting through a wgpu surface.
964 #[cfg(feature = "gpu")]
965 Gpu(Box<crate::gpu::GpuPath>),
966}
967
968impl Presenter {
969 /// Renders the frame and presents it, returning the statistics of a
970 /// presented frame.
971 ///
972 /// Returns `Ok(None)` when nothing was presented: a frame that changed
973 /// nothing while a tick runs, or a surface without a pixel or texture to
974 /// present, both of which the next redraw retries.
975 fn render_and_present(
976 &mut self,
977 frame: &FrameOutput<'_>,
978 has_tick: bool,
979 ) -> Result<Option<PresentStats>, PlatformError> {
980 match self {
981 Presenter::Cpu(cpu) => cpu.render_and_present(frame, has_tick),
982 #[cfg(feature = "gpu")]
983 Presenter::Gpu(gpu) => gpu.render_and_present(frame, has_tick),
984 }
985 }
986
987 /// Takes the window's new extent.
988 fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), PlatformError> {
989 match self {
990 Presenter::Cpu(cpu) => cpu.resize(width, height),
991 #[cfg(feature = "gpu")]
992 Presenter::Gpu(gpu) => gpu.resize(width.get(), height.get()),
993 }
994 }
995
996 /// Returns the GPU adapter this presenter draws on, when it has one.
997 #[cfg(feature = "gpu")]
998 fn adapter_summary(&self) -> Option<AdapterSummary> {
999 match self {
1000 Presenter::Cpu(_) => None,
1001 Presenter::Gpu(gpu) => Some(gpu.adapter_summary()),
1002 }
1003 }
1004}
1005
1006/// The CPU renderer and the software surface it presents through.
1007struct CpuPresenter {
1008 /// The surface that holds the pixels the window shows.
1009 surface: Surface<SharedWindow, SharedWindow>,
1010 /// The window whose frames are presented, notified before every present.
1011 window: SharedWindow,
1012 /// The renderer that paints the app's damage into a pixmap.
1013 renderer: CpuRenderer,
1014 /// Whether every present tints the frame's damage in the window buffer.
1015 show_damage: bool,
1016 /// The damage of the frames the window presented, most recent first.
1017 history: DamageHistory,
1018 /// The frame that was rendered and waits to be presented.
1019 pending: PendingFrame,
1020 /// Scratch for the region the next present copies.
1021 region: Vec<PixelRect>,
1022 /// Scratch for the damage rectangles the next present reports.
1023 reported: Vec<softbuffer::Rect>,
1024}
1025
1026impl CpuPresenter {
1027 /// Creates the surface and the renderer for a window of `width` by
1028 /// `height` physical pixels.
1029 ///
1030 /// A window without pixels gets a renderer without pixels, and nothing
1031 /// presents until a resize gives the window an extent.
1032 fn new(
1033 window: &Arc<Window>,
1034 width: u32,
1035 height: u32,
1036 show_damage: bool,
1037 ) -> Result<CpuPresenter, PlatformError> {
1038 let shared = SharedWindow(Arc::clone(window));
1039 let context = Context::new(shared.clone()).map_err(PlatformError::Surface)?;
1040 let mut surface = Surface::new(&context, shared).map_err(PlatformError::Surface)?;
1041 drop(context);
1042 if let (Some(width), Some(height)) = (NonZeroU32::new(width), NonZeroU32::new(height)) {
1043 surface
1044 .resize(width, height)
1045 .map_err(PlatformError::Surface)?;
1046 }
1047 Ok(CpuPresenter {
1048 surface,
1049 window: SharedWindow(Arc::clone(window)),
1050 renderer: CpuRenderer::new(width, height),
1051 show_damage,
1052 history: DamageHistory::default(),
1053 pending: PendingFrame::default(),
1054 region: Vec::new(),
1055 reported: Vec::new(),
1056 })
1057 }
1058
1059 /// Takes the window's new extent.
1060 fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), PlatformError> {
1061 self.surface
1062 .resize(width, height)
1063 .map_err(PlatformError::Surface)?;
1064 self.renderer.resize(width.get(), height.get());
1065 Ok(())
1066 }
1067
1068 /// Renders the frame into the pixmap and presents it.
1069 ///
1070 /// The frame's damage and the recorded damage of the frames still in a
1071 /// window buffer decide what has to be copied: when the buffer's age says
1072 /// which frame it holds and no full repaint happened since, only their
1073 /// union is copied and the present reports it as damage. Every other case
1074 /// copies the whole pixmap. While the damage overlay is on, every present
1075 /// copies the whole pixmap and blends the tint over the frame's damage in
1076 /// the window buffer, so no tint reaches the pixmap a later frame repaints
1077 /// from. It also presents a frame with no damage, so a tint never outlives
1078 /// the frame that caused it.
1079 ///
1080 /// Before every present, the window is told that a frame is about to
1081 /// show. That is the order winit needs on Wayland: it schedules a frame
1082 /// callback that paces the next redraw, and presenting without it lets
1083 /// frames outrun the compositor until a present waits forever for the
1084 /// buffer it cannot get.
1085 ///
1086 /// Returns the statistics of a presented frame, or `Ok(None)` when nothing
1087 /// was presented: a surface without pixels, or a frame with no damage
1088 /// while a tick runs. Such a frame changed nothing, so the window still
1089 /// shows the last present, no buffer is taken, and the frame limit does
1090 /// not count it. An empty frame without a tick is a redraw the window
1091 /// system asked for, such as an X11 window being shown again, and presents
1092 /// the whole pixmap to restore the window's contents.
1093 fn render_and_present(
1094 &mut self,
1095 frame: &FrameOutput<'_>,
1096 has_tick: bool,
1097 ) -> Result<Option<PresentStats>, PlatformError> {
1098 let (width, height) = (self.renderer.width(), self.renderer.height());
1099 if width == 0 || height == 0 {
1100 return Ok(None);
1101 }
1102 let raster_start = Instant::now();
1103 self.renderer.render(frame);
1104 let raster = raster_start.elapsed();
1105 self.pending.damage.clear();
1106 self.pending.damage.extend_from_slice(frame.damage);
1107 self.pending.full = frame.full;
1108 if present_skipped(&self.pending.damage, has_tick, self.show_damage) {
1109 return Ok(None);
1110 }
1111 let acquire_start = Instant::now();
1112 let mut buffer = self.surface.buffer_mut().map_err(PlatformError::Surface)?;
1113 let acquire = acquire_start.elapsed();
1114 let present_start = Instant::now();
1115 debug_assert_eq!(
1116 buffer.len(),
1117 self.renderer.pixels().len() / 4,
1118 "the window buffer must hold one entry per rendered pixel"
1119 );
1120 let age = buffer.age();
1121 let with_damage = !self.show_damage
1122 && damage_region(
1123 age,
1124 &self.pending.damage,
1125 self.pending.full,
1126 &self.history,
1127 &mut self.region,
1128 );
1129 let mut bytes_copied = 0;
1130 if with_damage {
1131 self.reported.clear();
1132 for rect in &self.region {
1133 let (Some(rect_width), Some(rect_height)) =
1134 (NonZeroU32::new(rect.width), NonZeroU32::new(rect.height))
1135 else {
1136 continue;
1137 };
1138 bytes_copied += u64::from(rect.width) * u64::from(rect.height) * 4;
1139 self.reported.push(softbuffer::Rect {
1140 x: rect.x,
1141 y: rect.y,
1142 width: rect_width,
1143 height: rect_height,
1144 });
1145 }
1146 for &rect in &self.region {
1147 copy_rect(self.renderer.pixels(), width, &mut buffer, rect);
1148 }
1149 self.window.0.pre_present_notify();
1150 buffer
1151 .present_with_damage(&self.reported)
1152 .map_err(PlatformError::Surface)?;
1153 } else {
1154 bytes_copied = u64::from(width) * u64::from(height) * 4;
1155 copy_rect(
1156 self.renderer.pixels(),
1157 width,
1158 &mut buffer,
1159 PixelRect {
1160 x: 0,
1161 y: 0,
1162 width,
1163 height,
1164 },
1165 );
1166 if self.show_damage {
1167 tint_rects(&mut buffer, width, &self.pending.damage);
1168 }
1169 self.window.0.pre_present_notify();
1170 buffer.present().map_err(PlatformError::Surface)?;
1171 }
1172 self.history.push(&self.pending.damage, self.pending.full);
1173 Ok(Some(PresentStats {
1174 buffer_age: age,
1175 with_damage,
1176 bytes_copied,
1177 surface_width: width,
1178 surface_height: height,
1179 raster,
1180 acquire,
1181 present: present_start.elapsed(),
1182 renderer: "cpu",
1183 }))
1184 }
1185}
1186
1187/// Converts a physical size to logical pixels for `scale`.
1188fn logical_size(physical: PhysicalSize<u32>, scale: f32) -> Size {
1189 Size {
1190 width: physical.width as f32 / scale,
1191 height: physical.height as f32 / scale,
1192 }
1193}
1194
1195/// What the window does about the next timer.
1196#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1197enum WaitDecision {
1198 /// A timer is already due: run the frame that fires it now.
1199 Now,
1200 /// Sleep until the soonest timer fires.
1201 Until(Instant),
1202 /// No timer is scheduled: wait for events.
1203 Wait,
1204}
1205
1206/// Decides what the window does about the next timer due at `deadline`.
1207fn wait_decision(deadline: Option<Instant>, now: Instant) -> WaitDecision {
1208 match deadline {
1209 Some(deadline) if deadline <= now => WaitDecision::Now,
1210 Some(deadline) => WaitDecision::Until(deadline),
1211 None => WaitDecision::Wait,
1212 }
1213}
1214
1215/// A window handle that the presentation surfaces can own.
1216///
1217/// The surfaces store the window they present to and the platform state keeps
1218/// the window too, so both hold the same allocation. The `Arc` makes the
1219/// handle sendable, which the wgpu surface target requires.
1220#[derive(Clone)]
1221pub(crate) struct SharedWindow(pub(crate) Arc<Window>);
1222
1223impl HasWindowHandle for SharedWindow {
1224 fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
1225 self.0.window_handle()
1226 }
1227}
1228
1229impl HasDisplayHandle for SharedWindow {
1230 fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
1231 self.0.display_handle()
1232 }
1233}
1234
1235#[cfg(test)]
1236#[path = "window_tests.rs"]
1237mod tests;