Skip to main content

teksilo_platform/
window.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Mutex, OnceLock, mpsc};
6
7use teksilo_canvas::wake::{RedrawWaker, WakeKind};
8
9use winit::event::WindowEvent;
10use winit::window::Window;
11
12use accesskit::ActionRequest;
13use teksilo_render::Renderer;
14
15use crate::wake::{LiveWakeStats, RequestRedraw, WindowWakeTarget};
16
17/// Error returned when surface texture acquisition fails during rendering.
18#[derive(Debug, thiserror::Error)]
19#[error("Surface error: {0}")]
20pub struct SurfaceRenderError(pub String);
21
22/// Why a window could not configure its swapchain.
23///
24/// `Surface::configure` reports nothing: it hands its error to wgpu's
25/// uncaptured-error handler, whose default is to panic. Every configure in
26/// this file goes through [`configure_surface`] instead, which catches the
27/// error and sorts it into one of these two.
28///
29/// Crate-internal: what a caller outside acts on is
30/// [`FrameOutcome::DisplayLost`] and [`PlatformWindow::display_lost`], not the
31/// classification behind them.
32#[derive(Debug, thiserror::Error)]
33pub(crate) enum SurfaceConfigureError {
34    /// The surface can no longer answer for its adapter. On every backend
35    /// that means the connection to the display server is gone: the
36    /// compositor exited or crashed, or the GPU was reset under it.
37    ///
38    /// wgpu's own wording for this is `Surface does not support the
39    /// adapter's queue family`, which reads like a hardware mismatch and has
40    /// been reported as one. It is not. The adapter was chosen with
41    /// `compatible_surface`, and `request_adapter` filters on the very query
42    /// that fails here (`wgpu_core::instance`), so it answered yes to the
43    /// same question moments earlier. What changed is the surface, not the
44    /// adapter.
45    #[error(
46        "the display server connection is gone: the window surface now reports no \
47         supported formats for an adapter that was selected for it"
48    )]
49    DisplayLost,
50    /// wgpu refused the configuration for some other reason, carrying its own
51    /// message so a genuine mistake is not relabelled as a dead compositor.
52    #[error("wgpu refused the surface configuration: {0}")]
53    Rejected(String),
54}
55
56/// Outcome of [`PlatformWindow::render_frame`]. Mirrors the wgpu
57/// surface-status cases that matter to the caller so the app loop can
58/// decide how to respond (ignore, reconfigure, log) without every frame
59/// getting logged as an error.
60#[derive(Debug)]
61pub enum FrameOutcome {
62    /// Frame was rendered and presented.
63    Rendered,
64    /// wgpu reported the window as occluded or the acquire timed out.
65    /// Per wgpu guidance, skip this frame. On macOS, the initial paint
66    /// after window creation often hits `Occluded` one or more times
67    /// before Metal finishes compositing, so the caller should still
68    /// request another redraw once, and later ones after a growing wait —
69    /// unless it knows the window is hidden (occluded or minimised), or
70    /// [`PlatformWindow::occluded_now`] says it is.
71    ///
72    /// Wayland does not report a hidden surface this way: acquire keeps
73    /// succeeding there. A hidden Wayland window is throttled by the frame
74    /// callbacks `render_frame` requests, which its compositor withholds.
75    Skipped,
76    /// Surface became outdated (resize, scale change, device switch).
77    /// Caller should reconfigure the surface and try again.
78    NeedsReconfigure,
79    /// The display server is gone, so this window can never present again.
80    /// Caller should wind the application down. It must not reconfigure or
81    /// ask for another redraw: both come straight back here, and that spin
82    /// is the whole reason this is its own outcome rather than an `Error`.
83    DisplayLost,
84    /// Acquisition failed with a non-transient error.
85    Error(SurfaceRenderError),
86}
87
88/// A platform window wrapping a winit window, wgpu surface, renderer,
89/// and AccessKit adapter for screen reader support.
90pub struct PlatformWindow {
91    window: Arc<Window>,
92    surface: wgpu::Surface<'static>,
93    surface_config: wgpu::SurfaceConfiguration,
94    /// The adapter this surface was matched against. Kept so that a configure
95    /// failure can ask the surface whether it still has formats for it, which
96    /// is how [`classify_configure_failure`] tells a departed display server
97    /// from a configuration wgpu genuinely refused.
98    adapter: wgpu::Adapter,
99    /// Latched the first time the display server is found to be gone. Every
100    /// later configure and every frame is then skipped: with no compositor
101    /// there is nothing to present to, and retrying only spins the loop.
102    display_lost: bool,
103    renderer: Renderer,
104    scale_factor: f64,
105    a11y_adapter: Option<accesskit_winit::Adapter>,
106    /// Set by the activation handler when an assistive technology asks for
107    /// the tree; cleared by the first delivery after it. Shared because the
108    /// handler may run off the main thread.
109    a11y_needs_full_tree: Arc<AtomicBool>,
110    /// Receiver for accessibility action requests from the adapter.
111    a11y_action_rx: mpsc::Receiver<ActionRequest>,
112    /// The accessibility state the adapter's off-thread handlers share with
113    /// the UI thread. See [`AccessibilityBridge`].
114    a11y_bridge: Arc<AccessibilityBridge>,
115    /// The window's wake target: what [`redraw_waker`](Self::redraw_waker)
116    /// hands out, what the accessibility handlers wake the loop through, and
117    /// what [`request_redraw`](Self::request_redraw) goes through.
118    wake: Arc<WindowWakeTarget>,
119}
120
121/// The state an AccessKit adapter's handlers share with the UI thread.
122///
123/// `accesskit_winit::Adapter::with_direct_handlers` requires every handler to
124/// be `Send` and calls it from whatever thread the platform's accessibility
125/// stack happens to use — the UIA provider thread on Windows, an AT-SPI task on
126/// Linux. A [`teksilo_core::WidgetTree`] is `!Send`, so no handler can reach
127/// one. Everything they need to say to the UI thread therefore goes through
128/// this, and everything they need to read from it is a snapshot the UI thread
129/// leaves here.
130/// The whole policy lives here rather than in the three handler types,
131/// because a handler owns an `Arc<Window>` and so cannot be built in a test
132/// without an event loop, while this can.
133#[derive(Debug, Default)]
134pub(crate) struct AccessibilityBridge {
135    /// The most recent `TreeUpdate` the UI thread published, kept so that
136    /// `request_initial_tree` can answer with the real tree instead of a
137    /// placeholder. `None` before the first frame.
138    snapshot: Mutex<Option<accesskit::TreeUpdate>>,
139    /// Whether an AccessKit client is attached right now. Set on activation,
140    /// cleared on deactivation.
141    active: std::sync::atomic::AtomicBool,
142}
143
144impl AccessibilityBridge {
145    /// Leave a tree where the activation handler can find it. Called from the
146    /// UI thread on every published update.
147    ///
148    /// A no-op while a client is attached, and that is the point: the snapshot
149    /// is read by `request_initial_tree` alone, which by definition runs while
150    /// nothing is attached — an attached client already has the live tree
151    /// through `update_if_active`. Skipping the clone there keeps the cost off
152    /// the frame path exactly when a screen reader is running and frames matter
153    /// most. The window between a detach and the next frame leaves the snapshot
154    /// one frame stale, which is a frame-old application rather than an empty
155    /// one; the deactivation handler asks for that frame.
156    pub(crate) fn publish(&self, update: &accesskit::TreeUpdate) {
157        if self.is_active() {
158            return;
159        }
160        if let Ok(mut slot) = self.snapshot.lock() {
161            *slot = Some(update.clone());
162        }
163    }
164
165    /// A client attached: record it and answer with the best tree available.
166    ///
167    /// The last published one if there is one — an assistive technology
168    /// attaching to an idle window must not be shown an empty application —
169    /// and the bare window node only before this window has ever drawn.
170    pub(crate) fn on_activate(&self) -> accesskit::TreeUpdate {
171        self.active
172            .store(true, std::sync::atomic::Ordering::Relaxed);
173        self.snapshot
174            .lock()
175            .ok()
176            .and_then(|slot| slot.clone())
177            .unwrap_or_else(empty_initial_tree)
178    }
179
180    /// The last client detached.
181    pub(crate) fn on_deactivate(&self) {
182        self.active
183            .store(false, std::sync::atomic::Ordering::Relaxed);
184    }
185
186    /// Whether a client is attached right now.
187    pub(crate) fn is_active(&self) -> bool {
188        self.active.load(std::sync::atomic::Ordering::Relaxed)
189    }
190}
191
192/// The wgpu objects every window in the process shares.
193///
194/// All three are `Arc` handles internally, so cloning one is a refcount bump,
195/// not a second GPU object.
196#[derive(Clone)]
197struct SharedGpu {
198    adapter: wgpu::Adapter,
199    device: wgpu::Device,
200    queue: wgpu::Queue,
201    /// The device's lost-device latch, installed once when it was opened and
202    /// shared by every window's renderer on it.
203    health: teksilo_render::DeviceHealth,
204}
205
206/// The platform display connection the wgpu instance is built against.
207///
208/// Installed by the app layer via [`install_display_handle`] before the first
209/// window exists, and read once by [`shared_instance`].
210static DISPLAY_HANDLE: OnceLock<winit::event_loop::OwnedDisplayHandle> = OnceLock::new();
211
212/// Hand wgpu the platform display connection, before any window is created.
213///
214/// Load-bearing for the OpenGL backend, which is the only backend a machine
215/// with no Vulkan driver has left — an older GPU, or a VM whose guest driver
216/// stops at GL. Without a display handle, wgpu-hal's GLES backend has no
217/// windowing system to bind EGL to and falls back to
218/// `EGL_MESA_platform_surfaceless`: a display that can render offscreen but can
219/// never be compatible with a *window* surface. `request_adapter` then rejects
220/// the only adapter on the machine with `incompatible_surface_backends: GL`,
221/// and the process dies before its first window. Vulkan, Metal and D3D12 ignore
222/// the handle entirely, so this costs those paths nothing.
223///
224/// Only the first call counts; later ones are ignored, because the instance is
225/// built once per process and wgpu forbids presenting a surface from a display
226/// other than the one the instance was created with.
227pub fn install_display_handle(handle: winit::event_loop::OwnedDisplayHandle) {
228    let _ = DISPLAY_HANDLE.set(handle);
229}
230
231/// The one wgpu instance for this process.
232///
233/// A surface has to come from the same instance that later enumerates adapters
234/// for it, so this is the root every window hangs off. `Instance::new` is
235/// synchronous, which is why this one can be a plain `OnceLock` while the
236/// adapter and device below cannot.
237///
238/// The descriptor is built `_from_env`, so wgpu's own variables —
239/// `WGPU_BACKEND`, `WGPU_GLES_MINOR_VERSION` and the rest — work here as they
240/// do in every other wgpu application. That is the escape hatch for the machine
241/// whose preferred backend has a broken driver, and it is worth having
242/// precisely where the default choice is the thing under suspicion.
243///
244/// Its flags come from [`teksilo_render::instance_flags`] rather than from
245/// `_from_env` alone, so this instance and the offscreen one
246/// [`teksilo_render::test_support`] opens agree about them.
247///
248/// On Windows the OpenGL backend is left out when Direct3D 12 or Vulkan has
249/// an adapter, unless `WGPU_BACKEND` names the backends. wgpu's OpenGL
250/// backend there starts a thread that creates a hidden top-level window and
251/// never handles a message: every message Windows sends to all top-level
252/// windows waits on it, and a sign-out waited five seconds for its
253/// `WM_QUERYENDSESSION` answer, then ended the process, in every
254/// application, although Direct3D 12 drew. A machine with neither keeps
255/// OpenGL.
256fn shared_instance() -> &'static wgpu::Instance {
257    static INSTANCE: OnceLock<wgpu::Instance> = OnceLock::new();
258    INSTANCE.get_or_init(|| {
259        let descriptor = || {
260            let mut descriptor = match DISPLAY_HANDLE.get() {
261                Some(display) => wgpu::InstanceDescriptor::new_with_display_handle_from_env(
262                    Box::new(display.clone()),
263                ),
264                // No app layer installed one: an embedder driving
265                // `PlatformWindow` itself, or a test. Offscreen work is
266                // unaffected; only a GL-backed window needs the handle.
267                None => wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
268            };
269            descriptor.flags = teksilo_render::instance_flags();
270            descriptor
271        };
272        #[cfg(target_os = "windows")]
273        if std::env::var_os("WGPU_BACKEND").is_none() {
274            let mut without_gl = descriptor();
275            without_gl.backends.remove(wgpu::Backends::GL);
276            let backends = without_gl.backends;
277            let instance = wgpu::Instance::new(without_gl);
278            if !pollster::block_on(instance.enumerate_adapters(backends)).is_empty() {
279                return instance;
280            }
281        }
282        wgpu::Instance::new(descriptor())
283    })
284}
285
286/// The adapter, device and queue every window shares.
287///
288/// One device per process, not one per window. A device is a heavyweight,
289/// process-level object and a second one buys nothing: each window still needs
290/// its own surface and its own [`Renderer`] (that is where the glyph and path
291/// atlases live), but the driver objects underneath are the same for every
292/// window on the same adapter. Opening one per window duplicated the entire
293/// pipeline set and both atlas textures for every window a user opened.
294///
295/// It also closes a latent crash. Two D3D12 **WARP** devices rasterizing at the
296/// same time fault inside `d3d10warp.dll` — Microsoft's software rasterizer,
297/// and what a GPU-less Windows host actually draws with. Teksilo renders its
298/// windows sequentially on the winit main thread, so that was not reachable
299/// here; it would have become reachable the moment any window work moved off
300/// that thread. `teksilo_render::test_support` shares its offscreen device for
301/// the same reason, where it *was* reachable and did crash.
302///
303/// `surface` is used only to pick an adapter that can actually present to it.
304/// If a later window's surface turns out to be incompatible with the adapter we
305/// cached — a genuinely multi-GPU machine, where the second window opens on the
306/// other GPU — that window quietly gets its own device rather than failing.
307/// The limits a live window asks its device for.
308///
309/// Deliberately **not** [`wgpu::Limits::default`]. That set demands eight
310/// colour attachments, 64 KiB uniform bindings and 8192-pixel textures. This
311/// renderer draws every pass into a *single* colour attachment, binds at most
312/// 8 KiB of uniforms (128 animation slots of 64 bytes) and caps its path atlas
313/// at 4096 pixels. The headroom was inherited from the default, never needed.
314///
315/// On GLES-3.1 class hardware that headroom is not merely unused, it is
316/// refused: a Raspberry Pi 4's V3D driver allows four colour attachments, so
317/// `default()` failed device creation outright and the app could not open a
318/// window at all.
319///
320/// `downlevel_defaults` is wgpu's GLES-3.1 floor, which is exactly that class
321/// of hardware, and it is already what [`teksilo_render::test_support`] opens
322/// its offscreen device with, so a frame that renders in a test now renders in
323/// a window too. `using_resolution` lifts the three texture-dimension limits
324/// back to whatever this adapter really supports, because the path atlas grows
325/// past the 2048-pixel downlevel cap.
326fn window_device_limits(adapter_limits: wgpu::Limits) -> wgpu::Limits {
327    wgpu::Limits::downlevel_defaults().using_resolution(adapter_limits)
328}
329
330/// Open a device on `adapter`, preferring [`window_device_limits`] and falling
331/// back to whatever the adapter itself reports.
332///
333/// The fallback is not redundant. `downlevel_defaults` is a floor for a *class*
334/// of hardware, not a promise about any given adapter. Anything below GLES 3.1
335/// (an old GL driver, a constrained software rasterizer) can sit under it on a
336/// field `using_resolution` does not lift, and then the principled ask fails
337/// for the same reason `default()` did on the Pi. `adapter.limits()` is by
338/// construction the most that adapter can give, so it cannot be refused on
339/// limit grounds; a request that still fails has a real problem rather than a
340/// mis-sized ask, and that is the error worth propagating.
341async fn open_device(
342    adapter: &wgpu::Adapter,
343) -> Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> {
344    let descriptor = |limits| wgpu::DeviceDescriptor {
345        label: Some("teksilo_device"),
346        required_features: wgpu::Features::empty(),
347        required_limits: limits,
348        ..Default::default()
349    };
350
351    match adapter
352        .request_device(&descriptor(window_device_limits(adapter.limits())))
353        .await
354    {
355        Ok(pair) => Ok(pair),
356        Err(err) => {
357            // Say why we dropped to the adapter's own limits: a silent
358            // fallback turns "this GPU is below the GLES-3.1 floor" into an
359            // unexplained difference in behaviour between two machines.
360            eprintln!(
361                "teksilo-platform: downlevel device limits refused ({err}); \
362                 retrying with the adapter's own limits"
363            );
364            adapter.request_device(&descriptor(adapter.limits())).await
365        }
366    }
367}
368
369/// The backends this platform prefers, most preferred first.
370///
371/// wgpu does not rank backends. `Instance::new` initialises them in a fixed
372/// order — Vulkan, Metal, D3D12, GLES — and `request_adapter` then sorts the
373/// adapters it collected **only** by device type, and only when a power
374/// preference is set. Ours is `PowerPreference::None` unless `WGPU_POWER_PREF`
375/// says otherwise, which is wgpu's own default and sorts nothing at all. So the
376/// winner has been "the first adapter the first initialised backend
377/// enumerated", which on Windows means D3D12 was never reached as long as any
378/// Vulkan ICD was installed, however old.
379///
380/// That is the wrong default there. D3D12 is the backend Windows GPU drivers
381/// are tested against hardest — it is what the browsers use on Windows — while
382/// Vulkan support on older Windows hardware ranges from good to a stub that
383/// enumerates an adapter it cannot really drive. The field report that prompted
384/// this is one of those: a Windows 10 machine whose Vulkan ICD cannot build
385/// wgpu's own indirect-validation pipelines (see
386/// [`teksilo_render::instance_flags`]) and, past that, cannot present at all,
387/// while D3D12 on the same machine works.
388///
389/// Elsewhere the order simply writes down what wgpu already did, so this is a
390/// change of behaviour on Windows only. An explicit `WGPU_BACKEND` still wins:
391/// it is applied at `Instance::new`, so the backends it excludes enumerate
392/// nothing here and this order silently narrows to the one that was asked for.
393fn preferred_backends() -> &'static [wgpu::Backends] {
394    #[cfg(target_os = "windows")]
395    {
396        &[
397            wgpu::Backends::DX12,
398            wgpu::Backends::VULKAN,
399            wgpu::Backends::GL,
400        ]
401    }
402    #[cfg(any(target_os = "macos", target_os = "ios"))]
403    {
404        &[
405            wgpu::Backends::METAL,
406            wgpu::Backends::VULKAN,
407            wgpu::Backends::GL,
408        ]
409    }
410    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "ios")))]
411    {
412        &[wgpu::Backends::VULKAN, wgpu::Backends::GL]
413    }
414}
415
416/// The adapters on `backends` that can present to `surface`, ranked as
417/// `request_adapter` would rank them.
418///
419/// `enumerate_adapters` asks each backend for its adapters with no surface, so
420/// the "can this one actually present to this window" filter that
421/// `request_adapter` applies internally has to be applied here instead. A
422/// non-empty format list is wgpu's own answer to that question, and it is the
423/// same one [`shared_gpu_for`] uses to revalidate the cached adapter.
424///
425/// The ordering within a backend deliberately mirrors
426/// `wgpu_core::instance::request_adapter`: rank by device type under a power
427/// preference, and leave enumeration order alone under `None`. Keeping the two
428/// identical means this pass changes *which backend* is tried first and nothing
429/// else about how an adapter is chosen.
430async fn presentable_adapters(
431    surface: &wgpu::Surface<'static>,
432    backends: wgpu::Backends,
433    power_preference: wgpu::PowerPreference,
434) -> Vec<wgpu::Adapter> {
435    let mut adapters: Vec<wgpu::Adapter> = shared_instance()
436        .enumerate_adapters(backends)
437        .await
438        .into_iter()
439        .filter(|adapter| !surface.get_capabilities(adapter).formats.is_empty())
440        .collect();
441
442    let prefer_integrated = match power_preference {
443        wgpu::PowerPreference::LowPower => true,
444        wgpu::PowerPreference::HighPerformance => false,
445        // wgpu does not sort at all here, so neither do we.
446        _ => return adapters,
447    };
448    adapters
449        .sort_by_key(|adapter| device_type_rank(adapter.get_info().device_type, prefer_integrated));
450    adapters
451}
452
453/// `wgpu_core::instance::request_adapter`'s `get_order`, kept in step with it.
454///
455/// "Other" outranks the virtual and CPU types because a backend that does not
456/// report device types at all (OpenGL) lands there, and it is likelier to be
457/// real hardware than a software rasterizer is.
458fn device_type_rank(device_type: wgpu::DeviceType, prefer_integrated: bool) -> u8 {
459    match device_type {
460        wgpu::DeviceType::DiscreteGpu if prefer_integrated => 2,
461        wgpu::DeviceType::IntegratedGpu if prefer_integrated => 1,
462        wgpu::DeviceType::DiscreteGpu => 1,
463        wgpu::DeviceType::IntegratedGpu => 2,
464        wgpu::DeviceType::Other => 3,
465        wgpu::DeviceType::VirtualGpu => 4,
466        wgpu::DeviceType::Cpu => 5,
467    }
468}
469
470/// Find an adapter that can present to `surface` *and* yields a device.
471///
472/// Adapter selection is a search, not a single request — the same lesson
473/// [`teksilo_render::test_support`] already encodes for its offscreen device,
474/// which the window path did not have. A host can enumerate an adapter it
475/// cannot actually open (a VM's GL driver is the usual one) while a perfectly
476/// good software adapter sits behind `force_fallback_adapter`. Treating the
477/// first failure as fatal reports "no GPU" on a machine that has one.
478///
479/// Every pass filters on surface compatibility, so an adapter that cannot
480/// present to this window is never chosen — that is the check that failed on a
481/// machine with no Vulkan driver, and it is load-bearing, not a formality. The
482/// ordered pass tests it with [`presentable_adapters`], the fallback pass with
483/// `request_adapter`'s own `compatible_surface`; both ask wgpu the same
484/// question.
485///
486/// Panics only when *every* adapter on the machine declines, with a message
487/// naming what was tried and what the user can do about it.
488async fn open_gpu_for(
489    surface: &wgpu::Surface<'static>,
490) -> (wgpu::Adapter, wgpu::Device, wgpu::Queue) {
491    // `WGPU_POWER_PREF` is wgpu's own knob; honour it for the same reason the
492    // instance is built `_from_env`.
493    let power_preference = wgpu::PowerPreference::from_env().unwrap_or_default();
494    let mut adapter_error = None;
495    let mut device_error = None;
496
497    // First pass: this platform's own backend order (see
498    // `preferred_backends`). `request_adapter` cannot express "try D3D12
499    // before Vulkan" — its options carry no backend field — so the ordering
500    // has to be done by enumerating one backend at a time.
501    for &backends in preferred_backends() {
502        for adapter in presentable_adapters(surface, backends, power_preference).await {
503            match open_device(&adapter).await {
504                Ok((device, queue)) => return (adapter, device, queue),
505                Err(err) => {
506                    eprintln!(
507                        "teksilo-platform: adapter {:?} could not open a device ({err}); \
508                         trying the next one",
509                        adapter.get_info().name
510                    );
511                    device_error.get_or_insert(err);
512                }
513            }
514        }
515    }
516
517    // Second pass: whatever wgpu itself would have picked, then an explicit
518    // software adapter. The first arm is not redundant with the loop above —
519    // it reaches any backend `preferred_backends` does not name — and the
520    // second is the only way to ask for a CPU adapter, which
521    // `enumerate_adapters` cannot express.
522    for force_fallback_adapter in [false, true] {
523        let adapter = match shared_instance()
524            .request_adapter(&wgpu::RequestAdapterOptions {
525                power_preference,
526                compatible_surface: Some(surface),
527                force_fallback_adapter,
528                ..Default::default()
529            })
530            .await
531        {
532            Ok(adapter) => adapter,
533            Err(err) => {
534                adapter_error.get_or_insert(err);
535                continue;
536            }
537        };
538
539        match open_device(&adapter).await {
540            Ok((device, queue)) => return (adapter, device, queue),
541            Err(err) => {
542                // Worth saying out loud: the next pass silently landing on a
543                // software adapter is a large performance difference, and an
544                // unexplained one is the sort of thing that gets reported as
545                // "Teksilo is slow on my machine".
546                eprintln!(
547                    "teksilo-platform: adapter {:?} could not open a device ({err}); \
548                     trying the next one",
549                    adapter.get_info().name
550                );
551                device_error.get_or_insert(err);
552            }
553        }
554    }
555
556    panic!(
557        "no usable GPU adapter for this window.\n\
558         Tried every backend wgpu was built with, then an explicit software \
559         fallback; none could both present to the window and open a device.\n\
560         adapter search: {adapter_error:?}\n\
561         device open:    {device_error:?}\n\
562         Teksilo needs Vulkan, Metal, D3D12 or OpenGL (3.3 desktop / ES 3.0). \
563         On Linux, installing a Vulkan driver is usually the fix: \
564         `mesa-vulkan-drivers` carries both the hardware drivers and the \
565         software `lavapipe`. `WGPU_BACKEND=gl|vulkan|dx12|metal` forces a \
566         specific backend."
567    );
568}
569
570async fn shared_gpu_for(surface: &wgpu::Surface<'static>) -> SharedGpu {
571    static SHARED: Mutex<Option<SharedGpu>> = Mutex::new(None);
572
573    // Clone out and release the lock: it is never held across the awaits below.
574    let cached = SHARED.lock().unwrap_or_else(|e| e.into_inner()).clone();
575    if let Some(gpu) = cached {
576        // A non-empty format list is wgpu's own answer to "can this adapter
577        // present to this surface".
578        if !surface.get_capabilities(&gpu.adapter).formats.is_empty() {
579            return gpu;
580        }
581    }
582
583    let (adapter, device, queue) = open_gpu_for(surface).await;
584
585    // Installed where the device is opened, the shared one and the
586    // multi-GPU loser alike, so every renderer on it reads one latch.
587    let health = teksilo_render::DeviceHealth::install(&device);
588    let gpu = SharedGpu {
589        adapter,
590        device,
591        queue,
592        health,
593    };
594    // First one in becomes the shared device. Losing here is the multi-GPU case
595    // above (or a race that cannot happen while windows are created on one
596    // thread): the loser keeps the device it just opened, which is the old
597    // per-window behaviour and still correct.
598    let mut slot = SHARED.lock().unwrap_or_else(|e| e.into_inner());
599    if slot.is_none() {
600        *slot = Some(gpu.clone());
601    }
602    gpu
603}
604
605/// Configure `surface`, handing back the failure instead of letting wgpu's
606/// default uncaptured-error handler panic.
607///
608/// Every `Surface::configure` in this file goes through here, and the error
609/// scope is what makes that sufficient. Asking the surface whether it is still
610/// alive and *then* configuring it leaves a gap between the two in which the
611/// compositor can exit, and that gap is the bug: `request_adapter` validated
612/// this adapter against this surface with the same query `configure` runs, so
613/// only a change in between can make the second one fail. A scope catches the
614/// error from this exact call, however late the display server goes away.
615///
616/// `pop` resolves immediately on native (wgpu answers with a ready future) and
617/// wgpu's error scopes are thread-local. Both hold because every window in this
618/// process is created and drawn on the winit main thread.
619fn configure_surface(
620    surface: &wgpu::Surface<'static>,
621    adapter: &wgpu::Adapter,
622    device: &wgpu::Device,
623    config: &wgpu::SurfaceConfiguration,
624) -> Result<(), SurfaceConfigureError> {
625    let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
626    surface.configure(device, config);
627    match pollster::block_on(scope.pop()) {
628        None => Ok(()),
629        Some(err) => Err(classify_configure_failure(
630            err.to_string(),
631            !surface.get_capabilities(adapter).formats.is_empty(),
632        )),
633    }
634}
635
636/// Sort a configure failure into "the display server left" and everything
637/// else, on the one signal that separates them.
638///
639/// A live surface offers its adapter a non-empty format list; a surface whose
640/// display server has gone offers none, and that transition is observable:
641/// seven formats before the compositor exits, zero after. Deliberately not a
642/// match on wgpu's message text, which is both misleading here and free to
643/// change between releases.
644fn classify_configure_failure(
645    description: String,
646    surface_has_formats: bool,
647) -> SurfaceConfigureError {
648    if surface_has_formats {
649        SurfaceConfigureError::Rejected(description)
650    } else {
651        SurfaceConfigureError::DisplayLost
652    }
653}
654
655/// What [`PlatformWindow::surface_and_renderer`] hands to both constructors.
656struct WindowGpu {
657    surface: wgpu::Surface<'static>,
658    surface_config: wgpu::SurfaceConfiguration,
659    renderer: Renderer,
660    adapter: wgpu::Adapter,
661    display_lost: bool,
662}
663
664/// A window rendered offscreen and read back: what
665/// [`PlatformWindow::capture_offscreen`] returns.
666#[derive(Debug, Clone, PartialEq, Eq)]
667#[non_exhaustive]
668pub struct OffscreenCapture {
669    /// Tightly packed RGBA8, `width × height` pixels, no padding.
670    pub rgba: Vec<u8>,
671    pub width: u32,
672    pub height: u32,
673    /// Where these pixels sit in the window's surface, in physical pixels:
674    /// the whole surface, or the crop asked for, clamped to it. Empty when
675    /// the crop missed the surface.
676    pub region: teksilo_canvas::PixelRect,
677}
678
679/// The surface pixels a crop rect (physical pixels) covers: its edges
680/// rounded outwards, clamped to a `w × h` surface. Empty when it misses.
681fn crop_region(rect: teksilo_canvas::Rect, w: u32, h: u32) -> teksilo_canvas::PixelRect {
682    let x0 = (rect.x.floor().max(0.0) as u32).min(w);
683    let y0 = (rect.y.floor().max(0.0) as u32).min(h);
684    let x1 = ((rect.x + rect.width).ceil().max(0.0) as u32).min(w);
685    let y1 = ((rect.y + rect.height).ceil().max(0.0) as u32).min(h);
686    if x1 <= x0 || y1 <= y0 {
687        return teksilo_canvas::PixelRect::new(x0, y0, 0, 0);
688    }
689    teksilo_canvas::PixelRect::new(x0, y0, x1 - x0, y1 - y0)
690}
691
692/// The pixels of `region` out of a tightly packed RGBA buffer `w` pixels
693/// wide that contains it.
694fn crop_rgba(src: &[u8], w: u32, region: teksilo_canvas::PixelRect) -> Vec<u8> {
695    let row_bytes = region.width as usize * 4;
696    let mut out = Vec::with_capacity(row_bytes * region.height as usize);
697    for y in region.y..region.y + region.height {
698        let start = (y as usize * w as usize + region.x as usize) * 4;
699        out.extend_from_slice(&src[start..start + row_bytes]);
700    }
701    out
702}
703
704/// One refresh of the display `window` is on, when the display reports its
705/// rate.
706fn display_refresh_interval(window: &Window) -> Option<std::time::Duration> {
707    let millihertz = window.current_monitor()?.refresh_rate_millihertz()?;
708    Some(refresh_interval_from_millihertz(millihertz))
709}
710
711/// The interval of a refresh rate in millihertz. Rates below 20 Hz or above
712/// 1 kHz are read as those bounds: a frame never waits more than 50 ms for a
713/// producer, and always waits at least one millisecond.
714fn refresh_interval_from_millihertz(millihertz: u32) -> std::time::Duration {
715    let millihertz = millihertz.clamp(20_000, 1_000_000);
716    std::time::Duration::from_nanos(1_000_000_000_000 / u64::from(millihertz))
717}
718
719impl PlatformWindow {
720    /// Everything both constructors do: surface, shared device, swapchain
721    /// configuration, renderer. Kept in one place because the two entry points
722    /// differ only in whether they attach an AccessKit adapter, and sixty
723    /// duplicated lines of GPU setup is exactly the sort of thing that drifts.
724    async fn surface_and_renderer(window: &Arc<Window>) -> WindowGpu {
725        let size = window.inner_size();
726        let surface = shared_instance()
727            .create_surface(window.clone())
728            .expect("wgpu surface creation failed for the platform window");
729
730        let gpu = shared_gpu_for(&surface).await;
731
732        let surface_caps = surface.get_capabilities(&gpu.adapter);
733        // Guard the index accesses: a degenerate adapter/surface (software
734        // fallback, headless) can report empty `formats` / `alpha_modes`, and
735        // `[0]` would panic with an opaque out-of-bounds instead of degrading.
736        let surface_format = surface_caps
737            .formats
738            .iter()
739            .find(|f| f.is_srgb())
740            .copied()
741            .or_else(|| surface_caps.formats.first().copied())
742            .unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb);
743
744        let surface_config = wgpu::SurfaceConfiguration {
745            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
746            format: surface_format,
747            width: size.width.max(1),
748            height: size.height.max(1),
749            present_mode: wgpu::PresentMode::Fifo,
750            alpha_mode: surface_caps
751                .alpha_modes
752                .first()
753                .copied()
754                .unwrap_or(wgpu::CompositeAlphaMode::Auto),
755            view_formats: vec![],
756            desired_maximum_frame_latency: 2,
757            // `Auto` reproduces wgpu's pre-30 behaviour: sRGB for the
758            // non-`Rgba16Float` formats we select above.
759            color_space: wgpu::SurfaceColorSpace::Auto,
760        };
761        // A compositor that goes away while the device above is being opened
762        // lands here, because opening one is the slowest step between the
763        // adapter's validation and this call. It used to panic out of wgpu's
764        // default error handler before the window ever existed.
765        let display_lost =
766            match configure_surface(&surface, &gpu.adapter, &gpu.device, &surface_config) {
767                Ok(()) => false,
768                Err(err) => {
769                    eprintln!("teksilo-platform: {err}");
770                    matches!(err, SurfaceConfigureError::DisplayLost)
771                }
772            };
773
774        // The renderer stays per-window: it owns the glyph atlas, the path
775        // atlas and the blur pool, and it is `!Sync` besides.
776        let mut renderer =
777            Renderer::with_device_health(gpu.device, gpu.queue, surface_format, gpu.health);
778        if let Some(interval) = display_refresh_interval(window) {
779            renderer.set_live_refresh_interval(interval);
780        }
781        WindowGpu {
782            surface,
783            surface_config,
784            renderer,
785            adapter: gpu.adapter,
786            display_lost,
787        }
788    }
789
790    /// Create a new platform window from a winit window.
791    /// The `event_loop` parameter is needed for the AccessKit adapter.
792    ///
793    /// The window, built hidden, is shown once its adapter exists: made key
794    /// and brought to the front with `activate`, otherwise shown without
795    /// taking the focus (a dialog opened while the user is elsewhere). On
796    /// macOS the inactive show orders the window front without making it
797    /// key; on Windows it is the first show of a window built with
798    /// `with_active(false)`, which winit makes with `SW_SHOWNOACTIVATE`, so
799    /// the caller builds it so. X11 and Wayland leave focus to the window
800    /// manager or compositor either way.
801    pub async fn new_with_a11y(
802        window: Window,
803        event_loop: &winit::event_loop::ActiveEventLoop,
804        activate: bool,
805    ) -> Self {
806        let window = Arc::new(window);
807        let scale_factor = window.scale_factor();
808        let WindowGpu {
809            surface,
810            surface_config,
811            renderer,
812            adapter,
813            display_lost,
814        } = Self::surface_and_renderer(&window).await;
815
816        // Create AccessKit adapter with action channel
817        let (action_tx, action_rx) = mpsc::channel();
818
819        let a11y_needs_full_tree = Arc::new(AtomicBool::new(true));
820        let a11y_bridge = Arc::new(AccessibilityBridge::default());
821
822        // Every handler below runs off the UI thread and ends by waking the
823        // event loop, which is the *only* thing that gets what it left read:
824        // without a wakeup an action issued by Narrator or Orca would sit in
825        // the channel until some unrelated window event happened to arrive.
826        // A state wake, since what they leave must reach a window that draws
827        // nothing: see `WindowWakeTarget`.
828        let wake = WindowWakeTarget::new(Arc::clone(&window) as Arc<dyn RequestRedraw>);
829        let a11y_adapter = accesskit_winit::Adapter::with_direct_handlers(
830            event_loop,
831            &window,
832            TeksiloActivationHandler {
833                needs_full_tree: a11y_needs_full_tree.clone(),
834                bridge: Arc::clone(&a11y_bridge),
835                wake: Arc::clone(&wake),
836            },
837            TeksiloActionHandler {
838                tx: action_tx,
839                wake: Arc::clone(&wake),
840            },
841            TeksiloDeactivationHandler {
842                bridge: Arc::clone(&a11y_bridge),
843                wake: Arc::clone(&wake),
844            },
845        );
846
847        // Show the window now that the adapter is created
848        if activate {
849            window.set_visible(true);
850        } else {
851            crate::window_system::show_without_activating(&window);
852        }
853
854        Self {
855            window,
856            surface,
857            surface_config,
858            adapter,
859            display_lost,
860            renderer,
861            scale_factor,
862            a11y_adapter: Some(a11y_adapter),
863            a11y_action_rx: action_rx,
864            a11y_needs_full_tree,
865            a11y_bridge,
866            wake,
867        }
868    }
869
870    /// Create a platform window without AccessKit (for contexts without ActiveEventLoop).
871    pub async fn new(window: Window) -> Self {
872        let window = Arc::new(window);
873        let scale_factor = window.scale_factor();
874        let WindowGpu {
875            surface,
876            surface_config,
877            renderer,
878            adapter,
879            display_lost,
880        } = Self::surface_and_renderer(&window).await;
881        let (_action_tx, action_rx) = mpsc::channel();
882        let wake = WindowWakeTarget::new(Arc::clone(&window) as Arc<dyn RequestRedraw>);
883
884        Self {
885            window,
886            surface,
887            surface_config,
888            adapter,
889            display_lost,
890            renderer,
891            scale_factor,
892            a11y_adapter: None,
893            a11y_action_rx: action_rx,
894            a11y_needs_full_tree: Arc::new(AtomicBool::new(false)),
895            a11y_bridge: Arc::new(AccessibilityBridge::default()),
896            wake,
897        }
898    }
899
900    pub fn window(&self) -> &Window {
901        &self.window
902    }
903
904    /// Whether the platform says, when asked, that no part of the window is
905    /// visible now: covered entirely, or never shown, as a window created
906    /// while the display slept or the screen was locked is until both end.
907    /// macOS answers (`NSWindow`'s `occlusionState`), and `None` elsewhere,
908    /// where winit's events are all there is.
909    ///
910    /// winit sends `WindowEvent::Occluded` only when that state changes, so
911    /// a window that was never visible never hears it; and wgpu refuses
912    /// every acquire while AppKit does not show the window. Asking is how
913    /// the caller learns it. A window already showing stays visible here
914    /// while the displays sleep or the screen is locked: that is the
915    /// [session watch](crate::session)'s to hear.
916    pub fn occluded_now(&self) -> Option<bool> {
917        #[cfg(target_os = "macos")]
918        {
919            macos_occlusion::occluded(&self.window)
920        }
921        #[cfg(not(target_os = "macos"))]
922        {
923            None
924        }
925    }
926
927    /// Get a clonable `Arc` reference to the underlying winit window.
928    /// Used by `teksilo_platform::create_title_bar_host` and other components
929    /// that need shared ownership of the window.
930    pub fn window_arc(&self) -> Arc<Window> {
931        self.window.clone()
932    }
933
934    pub fn renderer(&self) -> &Renderer {
935        &self.renderer
936    }
937
938    pub fn renderer_mut(&mut self) -> &mut Renderer {
939        &mut self.renderer
940    }
941
942    pub fn scale_factor(&self) -> f64 {
943        self.scale_factor
944    }
945
946    pub fn set_scale_factor(&mut self, factor: f64) {
947        self.scale_factor = factor;
948    }
949
950    /// Read the refresh rate of the display the window is on, and have a
951    /// presented frame wait about one refresh of it for a producer holding a
952    /// live picture's lock. Done when the window is created; call it again
953    /// when the window may have moved to another display. A display that
954    /// reports no rate keeps the last one (60 Hz until one is read).
955    pub fn track_display_refresh(&mut self) {
956        if let Some(interval) = display_refresh_interval(&self.window) {
957            self.renderer.set_live_refresh_interval(interval);
958        }
959    }
960
961    /// What this window's renderer holds and uploads for live pictures.
962    pub fn live_texture_stats(&self) -> teksilo_render::LiveTextureStats {
963        self.renderer.live_texture_stats()
964    }
965
966    /// How long this window's live pass has taken, each figure over its
967    /// latest 1,024 samples: see `Renderer::live_texture_timings`. With the
968    /// `live-image-timings` feature.
969    #[cfg(feature = "live-image-timings")]
970    pub fn live_texture_timings(&self) -> teksilo_render::LiveImageTimings {
971        self.renderer.live_texture_timings()
972    }
973
974    /// Resize the surface.
975    ///
976    /// A resize that arrives once the display server has gone is dropped:
977    /// there is nothing left to present to.
978    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
979        if self.display_lost || new_size.width == 0 || new_size.height == 0 {
980            return;
981        }
982        self.surface_config.width = new_size.width;
983        self.surface_config.height = new_size.height;
984        self.apply_surface_config();
985    }
986
987    /// Get current surface dimensions.
988    pub fn surface_size(&self) -> (u32, u32) {
989        (self.surface_config.width, self.surface_config.height)
990    }
991
992    /// Reconfigure the surface with the current config.
993    /// Use after a Lost or Outdated surface error.
994    ///
995    /// Answers whether this window can still present. `false` means the
996    /// display server is gone, and the caller should wind down rather than ask
997    /// for another frame: the next one would come back here unchanged.
998    pub fn reconfigure_surface(&mut self) -> bool {
999        if self.display_lost {
1000            return false;
1001        }
1002        self.apply_surface_config();
1003        !self.display_lost
1004    }
1005
1006    /// Whether the display server has gone away under this window.
1007    pub fn display_lost(&self) -> bool {
1008        self.display_lost
1009    }
1010
1011    /// Push `surface_config` to the surface, latching a departed display
1012    /// server and reporting anything else wgpu refused.
1013    ///
1014    /// The latch is what keeps the report to one line: every later configure
1015    /// returns before reaching here.
1016    fn apply_surface_config(&mut self) {
1017        if let Err(err) = configure_surface(
1018            &self.surface,
1019            &self.adapter,
1020            self.renderer.device(),
1021            &self.surface_config,
1022        ) {
1023            eprintln!("teksilo-platform: {err}");
1024            if matches!(err, SurfaceConfigureError::DisplayLost) {
1025                self.display_lost = true;
1026            }
1027        }
1028    }
1029
1030    /// Render a frame to the surface.
1031    pub fn render_frame(
1032        &mut self,
1033        frame: &teksilo_canvas::RenderFrame,
1034        clear_color: [f32; 4],
1035    ) -> FrameOutcome {
1036        if self.display_lost {
1037            return FrameOutcome::DisplayLost;
1038        }
1039        let current = self.surface.get_current_texture();
1040        let output = match current {
1041            wgpu::CurrentSurfaceTexture::Success(tex)
1042            | wgpu::CurrentSurfaceTexture::Suboptimal(tex) => tex,
1043            wgpu::CurrentSurfaceTexture::Occluded | wgpu::CurrentSurfaceTexture::Timeout => {
1044                return FrameOutcome::Skipped;
1045            }
1046            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
1047                return FrameOutcome::NeedsReconfigure;
1048            }
1049            other => return FrameOutcome::Error(SurfaceRenderError(format!("{other:?}"))),
1050        };
1051
1052        let view = output
1053            .texture
1054            .create_view(&wgpu::TextureViewDescriptor::default());
1055
1056        let (w, h) = self.surface_size();
1057        self.renderer
1058            .render(frame, &view, self.scale_factor as f32, w, h, clear_color);
1059
1060        // Wayland: request a frame callback with the commit this present
1061        // makes. winit then holds the next `RedrawRequested` back until the
1062        // compositor sends it, which it does not for a hidden surface, so a
1063        // minimised window stops drawing (a no-op on other platforms). winit
1064        // never cancels the request, so every call here MUST be followed by
1065        // the present on the next line: an early return in between would
1066        // leave the window waiting for a callback that never comes.
1067        self.window.pre_present_notify();
1068        self.renderer.queue().present(output);
1069        FrameOutcome::Rendered
1070    }
1071
1072    /// Render `frame` into an offscreen texture of the surface's size and
1073    /// read it back as tightly packed RGBA8.
1074    ///
1075    /// Used by the debug-only automation bridge to capture a *live* window
1076    /// without going through the swapchain — the surface texture is
1077    /// configured `RENDER_ATTACHMENT` only (no `COPY_SRC`), so it can't be
1078    /// read back directly. It renders through the window's own renderer with
1079    /// [`Renderer::render_capture`]: every live picture shows its latest
1080    /// commit, and the window's next frame draws the textures this one
1081    /// filled. The whole frame is rendered, then `crop` (physical pixels,
1082    /// clamped to the surface) is cut out on the CPU: rendering only part of
1083    /// a frame through the window's renderer would drop the textures of the
1084    /// live pictures it left out. The offscreen texture uses the window's
1085    /// own surface format so it matches the renderer's pipelines; a BGRA
1086    /// readback is swizzled to RGBA here so the output is always RGBA. A
1087    /// crop outside the surface returns an empty capture.
1088    ///
1089    /// Fails, without panicking, when the device cannot read the texture
1090    /// back (it was lost).
1091    ///
1092    /// Note: a native `WebView` subview composites *on top of* the wgpu
1093    /// surface and is invisible to this readback (a transparent hole).
1094    pub fn capture_offscreen(
1095        &mut self,
1096        frame: &teksilo_canvas::RenderFrame,
1097        clear_color: [f32; 4],
1098        crop: Option<teksilo_canvas::Rect>,
1099    ) -> Result<OffscreenCapture, teksilo_render::test_support::ReadbackError> {
1100        let (w, h) = self.surface_size();
1101        let format = self.surface_config.format;
1102        // The readback assumes a 4-byte, 8-bit RGBA/BGRA layout (the BGRA
1103        // swizzle below + `try_read_texture_rgba`'s fixed 4-bytes-per-pixel
1104        // copy). Desktop wgpu surfaces are always one of these four; a packed
1105        // (Rgb10a2) or wide (Rgba16Float) surface format would read back
1106        // garbage, so flag it loudly in debug builds.
1107        debug_assert!(
1108            matches!(
1109                format,
1110                wgpu::TextureFormat::Rgba8Unorm
1111                    | wgpu::TextureFormat::Rgba8UnormSrgb
1112                    | wgpu::TextureFormat::Bgra8Unorm
1113                    | wgpu::TextureFormat::Bgra8UnormSrgb
1114            ),
1115            "capture_offscreen: unsupported surface format {format:?} (expected 8-bit RGBA/BGRA)"
1116        );
1117        let texture = self
1118            .renderer
1119            .device()
1120            .create_texture(&wgpu::TextureDescriptor {
1121                label: Some("teksilo-automation capture"),
1122                size: wgpu::Extent3d {
1123                    width: w,
1124                    height: h,
1125                    depth_or_array_layers: 1,
1126                },
1127                mip_level_count: 1,
1128                sample_count: 1,
1129                dimension: wgpu::TextureDimension::D2,
1130                format,
1131                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1132                view_formats: &[],
1133            });
1134        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1135        self.renderer
1136            .render_capture(frame, &view, self.scale_factor as f32, w, h, clear_color);
1137        let mut bytes = teksilo_render::test_support::try_read_texture_rgba(
1138            self.renderer.device(),
1139            self.renderer.queue(),
1140            &texture,
1141            w,
1142            h,
1143        )?;
1144        // The readback copies raw channel bytes; a BGRA surface needs its B
1145        // and R swapped to become RGBA for PNG encoding.
1146        if matches!(
1147            format,
1148            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
1149        ) {
1150            for px in bytes.as_chunks_mut::<4>().0 {
1151                px.swap(0, 2);
1152            }
1153        }
1154        let region = match crop {
1155            Some(rect) => crop_region(rect, w, h),
1156            None => teksilo_canvas::PixelRect::full(w, h),
1157        };
1158        Ok(OffscreenCapture {
1159            rgba: crop_rgba(&bytes, w, region),
1160            width: region.width,
1161            height: region.height,
1162            region,
1163        })
1164    }
1165
1166    /// Ask winit for a redraw. A no-op once the window's wake target is
1167    /// disconnected ([`disconnect_redraw_waker`](Self::disconnect_redraw_waker)),
1168    /// which is how a request after the event loop ended stays harmless.
1169    pub fn request_redraw(&self) {
1170        self.wake.request_on_slot();
1171    }
1172
1173    /// Push an AccessKit TreeUpdate to the adapter (called after layout).
1174    /// Publish a freshly built `TreeUpdate` to the adapter, and leave a copy
1175    /// where the activation handler can find it.
1176    ///
1177    /// The copy is what lets an assistive technology that attaches to an *idle*
1178    /// window see the application instead of an empty window node: the handler
1179    /// runs off the UI thread and cannot build a tree, so the last one the UI
1180    /// thread built is the best answer available synchronously.
1181    ///
1182    /// Hand it what
1183    /// [`WidgetTree::deliver_accessibility`](teksilo_core::WidgetTree::deliver_accessibility)
1184    /// returns, not `sync_accessibility`'s update: a node that left the tree a
1185    /// reader sees and came back must reach the adapter under an id it has
1186    /// never had, or a reader on AT-SPI holds it defunct and hears nothing
1187    /// from it.
1188    pub fn update_accessibility(&mut self, update: accesskit::TreeUpdate) {
1189        self.a11y_bridge.publish(&update);
1190        if let Some(adapter) = &mut self.a11y_adapter {
1191            adapter.update_if_active(|| update);
1192        }
1193    }
1194
1195    /// Push an update the adapter builds only when it is actually going to
1196    /// be delivered, and only when `build` says there is one worth sending.
1197    ///
1198    /// The caller decides *inside* the closure, because that is where the
1199    /// decision belongs: `update_if_active` runs its closure only when an
1200    /// assistive technology is attached, and on Linux it runs it under the
1201    /// adapter's own state lock. Deciding outside would build a tree for
1202    /// nobody on every frame, and would make the throttle count frames
1203    /// nothing was listening to.
1204    ///
1205    /// `build` returning `None` means "nothing to deliver": `previous` then
1206    /// gives the adapter an update that leaves its tree as it is, since
1207    /// `update_if_active` must be given one. An update with no node does
1208    /// that without the adapter comparing a node
1209    /// ([`WidgetTree::accessibility_unchanged`](teksilo_core::WidgetTree::accessibility_unchanged)).
1210    pub fn update_accessibility_with(
1211        &mut self,
1212        build: impl FnOnce() -> Option<accesskit::TreeUpdate>,
1213        previous: impl FnOnce() -> accesskit::TreeUpdate,
1214    ) {
1215        if let Some(adapter) = &mut self.a11y_adapter {
1216            adapter.update_if_active(|| build().unwrap_or_else(previous));
1217        }
1218    }
1219
1220    /// Whether an assistive technology has asked this window for its tree
1221    /// and has not yet been given a full one.
1222    ///
1223    /// Set by the activation handler, which runs on whichever thread the
1224    /// platform's accessibility layer calls it from, and cleared by the
1225    /// first delivery after it — so a reader that attaches mid-session gets
1226    /// a complete tree rather than a geometry patch onto a tree it has
1227    /// never seen.
1228    pub fn accessibility_needs_full_tree(&self) -> bool {
1229        self.a11y_needs_full_tree.load(Ordering::Relaxed)
1230    }
1231
1232    /// Clear the flag above, reporting what it was.
1233    pub fn take_accessibility_needs_full_tree(&self) -> bool {
1234        self.a11y_needs_full_tree.swap(false, Ordering::Relaxed)
1235    }
1236
1237    /// Whether an AccessKit client is attached to this window's adapter.
1238    ///
1239    /// True from the moment the platform accessibility stack asks for an
1240    /// initial tree until it says it has gone away. Read once per frame by
1241    /// `teksilo-app` and pushed into the window's tree; see
1242    /// [`WidgetTree::set_at_client_attached`](teksilo_core::WidgetTree::set_at_client_attached)
1243    /// for why attaching and detaching are read asymmetrically.
1244    ///
1245    /// Always `false` for a window built without an adapter
1246    /// ([`PlatformWindow::new`]).
1247    pub fn accessibility_active(&self) -> bool {
1248        self.a11y_bridge.is_active()
1249    }
1250
1251    /// Forward a winit WindowEvent to the AccessKit adapter.
1252    pub fn process_accessibility_event(&mut self, event: &WindowEvent) {
1253        if let Some(adapter) = &mut self.a11y_adapter {
1254            adapter.process_event(&self.window, event);
1255        }
1256    }
1257
1258    /// This window's wake target, as a waker any thread may call to have the
1259    /// window run a frame. See [`teksilo_canvas::wake`] for the two kinds of
1260    /// wake and how a burst coalesces.
1261    ///
1262    /// On macOS, winit runs a redraw requested off the main thread
1263    /// synchronously on it, so a draw wake made on another thread never asks
1264    /// winit itself there: it posts through the off-main route
1265    /// ([`set_off_main_wake_route`](Self::set_off_main_wake_route)), which
1266    /// teksilo-app installs, or with none hands the request to the main
1267    /// dispatch queue. Either way it returns without waiting for the main
1268    /// thread.
1269    pub fn redraw_waker(&self) -> Arc<dyn RedrawWaker> {
1270        self.wake.clone()
1271    }
1272
1273    /// The route a draw wake made off the main thread posts through, instead
1274    /// of asking winit for a redraw from there. Set once, before the first
1275    /// wake; later calls are ignored. teksilo-app sets it on macOS, where a
1276    /// redraw requested off the main thread waits for it, so that the wake
1277    /// reaches the event loop it runs.
1278    pub fn set_off_main_wake_route(&self, route: Arc<dyn Fn() + Send + Sync>) {
1279        self.wake.set_off_main_route(route);
1280    }
1281
1282    /// The route a state wake posts through: one post per burst, until the
1283    /// event loop takes it ([`take_posted_wake`](Self::take_posted_wake)).
1284    /// Set once; later calls are ignored. teksilo-app's seam: a posted event
1285    /// reaches a window whose redraw the compositor withholds, which a redraw
1286    /// request does not.
1287    #[doc(hidden)]
1288    pub fn set_state_wake_route(&self, route: Arc<dyn Fn() + Send + Sync>) {
1289        self.wake.set_state_route(route);
1290    }
1291
1292    /// The event loop received a posted wake of `kind`: re-arm its
1293    /// coalescing, before acting on it, so the next wake posts again. Returns
1294    /// whether one was pending.
1295    #[doc(hidden)]
1296    pub fn take_posted_wake(&self, kind: WakeKind) -> bool {
1297        self.wake.take_posted(kind)
1298    }
1299
1300    /// `true` while the window is hidden, in which case one redraw is
1301    /// requested when it is shown again: for a draw wake the event loop
1302    /// received while the window was hidden.
1303    #[doc(hidden)]
1304    pub fn defer_redraw_until_shown(&self) -> bool {
1305        self.wake.defer_until_shown()
1306    }
1307
1308    /// Make every waker [`redraw_waker`](Self::redraw_waker) handed out a
1309    /// no-op, and [`request_redraw`](Self::request_redraw) with them. Waits
1310    /// for a wake already asking winit; idempotent. Dropping the window does
1311    /// it too; teksilo-app does it for every window when the event loop ends.
1312    pub fn disconnect_redraw_waker(&self) {
1313        self.wake.disconnect();
1314    }
1315
1316    /// Record whether the window is hidden (minimised, fully covered, or
1317    /// drawing nothing because its compositor withholds its frames). While it
1318    /// is, draw wakes are dropped; showing it requests one redraw if any was.
1319    pub fn set_hidden(&self, hidden: bool) {
1320        self.wake.set_hidden(hidden);
1321    }
1322
1323    /// The window draws again and a frame is about to run: record it shown,
1324    /// without the redraw [`set_hidden`](Self::set_hidden)`(false)` requests
1325    /// for draw wakes dropped while it was hidden, which that frame serves.
1326    /// teksilo-app's seam for the redraw that ends a withheld one.
1327    #[doc(hidden)]
1328    pub fn show_for_drawing(&self) {
1329        self.wake.show_for_drawing();
1330    }
1331
1332    /// Whether the window is hidden, as last set.
1333    pub fn is_hidden(&self) -> bool {
1334        self.wake.is_hidden()
1335    }
1336
1337    /// The window's wake counters.
1338    pub fn live_wake_stats(&self) -> LiveWakeStats {
1339        self.wake.stats()
1340    }
1341
1342    /// Drain any pending AccessKit action requests from the adapter.
1343    ///
1344    /// They name nodes by the ids the adapter was handed; pass each through
1345    /// [`WidgetTree::resolve_adapter_action`](teksilo_core::WidgetTree::resolve_adapter_action)
1346    /// before looking its target up in the tree.
1347    pub fn drain_accessibility_actions(&self) -> Vec<ActionRequest> {
1348        let mut actions = Vec::new();
1349        while let Ok(req) = self.a11y_action_rx.try_recv() {
1350            actions.push(req);
1351        }
1352        actions
1353    }
1354}
1355
1356// --- AccessKit handler implementations ---
1357
1358/// Activation handler — answers with the last tree the UI thread built.
1359///
1360/// An assistive technology attaching to a window that is sitting idle used to
1361/// be shown a bare `Role::Window` node with no children, and stayed shown it
1362/// until something unrelated caused a frame. Answering from the published
1363/// snapshot fixes the common case; the redraw request covers the rest, since
1364/// the adapter is active from here on and the next
1365/// [`PlatformWindow::update_accessibility`] reaches it.
1366/// `needs_full_tree` is what makes the delivery that follows a *full*
1367/// tree rather than a geometry patch: updates are otherwise throttled to
1368/// the moves-only rate, and a reader that attaches mid-session has never
1369/// seen the tree such a patch would be applied to.
1370struct TeksiloActivationHandler {
1371    needs_full_tree: Arc<AtomicBool>,
1372    bridge: Arc<AccessibilityBridge>,
1373    wake: Arc<WindowWakeTarget>,
1374}
1375
1376/// The tree handed to a client that attached before this window ever drew.
1377///
1378/// A window node with no children — the same placeholder as before — because
1379/// there is genuinely nothing else to say yet. The accompanying redraw request
1380/// is what makes it short-lived.
1381fn empty_initial_tree() -> accesskit::TreeUpdate {
1382    let root = accesskit::Node::new(accesskit::Role::Window);
1383    let root_id = teksilo_core::accessibility::root_node_id();
1384    accesskit::TreeUpdate {
1385        nodes: vec![(root_id, root)],
1386        tree: Some(accesskit::TreeInfo::new(root_id)),
1387        tree_id: accesskit::TreeId::ROOT,
1388        focus: root_id,
1389    }
1390}
1391
1392impl accesskit::ActivationHandler for TeksiloActivationHandler {
1393    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
1394        self.needs_full_tree.store(true, Ordering::Relaxed);
1395        let update = self.bridge.on_activate();
1396        // Whether or not we could answer with a real tree, ask for a frame: it
1397        // is what carries the *next* update to the now-active adapter, and it
1398        // is also how the UI thread learns that a client attached.
1399        self.wake.wake_state();
1400        Some(update)
1401    }
1402}
1403
1404/// Action handler — forwards action requests to the main thread via a channel,
1405/// then wakes the loop so the channel is actually drained.
1406struct TeksiloActionHandler {
1407    tx: mpsc::Sender<ActionRequest>,
1408    wake: Arc<WindowWakeTarget>,
1409}
1410
1411impl accesskit::ActionHandler for TeksiloActionHandler {
1412    fn do_action(&mut self, request: ActionRequest) {
1413        let _ = self.tx.send(request);
1414        self.wake.wake_state();
1415    }
1416}
1417
1418/// Deactivation handler — records that the last client detached.
1419///
1420/// Unlike activation, this *is* evidence about screen readers: when no client
1421/// is attached, none of them is reading the tree either.
1422struct TeksiloDeactivationHandler {
1423    bridge: Arc<AccessibilityBridge>,
1424    wake: Arc<WindowWakeTarget>,
1425}
1426
1427impl accesskit::DeactivationHandler for TeksiloDeactivationHandler {
1428    fn deactivate_accessibility(&mut self) {
1429        self.bridge.on_deactivate();
1430        // The UI thread reads the flag once per frame, so it needs a frame.
1431        self.wake.wake_state();
1432    }
1433}
1434
1435impl Drop for PlatformWindow {
1436    /// Disconnect the wake target first: a waker another thread still holds
1437    /// must not reach a window that is going away.
1438    fn drop(&mut self) {
1439        self.wake.disconnect();
1440    }
1441}
1442
1443#[cfg(test)]
1444mod capture_tests {
1445    use super::{crop_region, crop_rgba, refresh_interval_from_millihertz};
1446    use std::time::Duration;
1447    use teksilo_canvas::{PixelRect, Rect};
1448
1449    /// A crop's edges round outwards to whole pixels and clamp to the
1450    /// surface; one that misses the surface is empty.
1451    #[test]
1452    fn a_crop_covers_every_pixel_it_touches_inside_the_surface() {
1453        assert_eq!(
1454            crop_region(Rect::new(1.5, 2.25, 3.0, 4.5), 10, 10),
1455            PixelRect::new(1, 2, 4, 5)
1456        );
1457        assert_eq!(
1458            crop_region(Rect::new(-3.0, 8.0, 20.0, 5.0), 10, 10),
1459            PixelRect::new(0, 8, 10, 2)
1460        );
1461        assert!(crop_region(Rect::new(12.0, 0.0, 4.0, 4.0), 10, 10).is_empty());
1462        assert!(crop_region(Rect::new(2.0, 2.0, 0.0, 3.0), 10, 10).is_empty());
1463    }
1464
1465    /// The crop is cut from rows of the full surface, not from its start.
1466    #[test]
1467    fn the_cropped_pixels_are_the_regions_own() {
1468        let (w, h) = (5u32, 4u32);
1469        let src: Vec<u8> = (0..w * h)
1470            .flat_map(|i| [i as u8, (i >> 8) as u8, 7, 255])
1471            .collect();
1472        let region = PixelRect::new(1, 2, 3, 2);
1473        let out = crop_rgba(&src, w, region);
1474        let firsts: Vec<u8> = out.chunks(4).map(|px| px[0]).collect();
1475        assert_eq!(firsts, vec![11, 12, 13, 16, 17, 18]);
1476        assert!(crop_rgba(&src, w, PixelRect::new(0, 0, 0, 0)).is_empty());
1477    }
1478
1479    /// One refresh of the display, its rate bounded to 20 Hz – 1 kHz.
1480    #[test]
1481    fn a_display_rate_becomes_one_refresh() {
1482        assert_eq!(
1483            refresh_interval_from_millihertz(60_000),
1484            Duration::from_nanos(16_666_666)
1485        );
1486        assert_eq!(
1487            refresh_interval_from_millihertz(144_000),
1488            Duration::from_nanos(6_944_444)
1489        );
1490        assert_eq!(
1491            refresh_interval_from_millihertz(0),
1492            Duration::from_millis(50)
1493        );
1494        assert_eq!(
1495            refresh_interval_from_millihertz(u32::MAX),
1496            Duration::from_millis(1)
1497        );
1498    }
1499}
1500
1501#[cfg(test)]
1502mod surface_configure_tests {
1503    use super::{SurfaceConfigureError, classify_configure_failure};
1504
1505    /// The defect this pins: a compositor crash reached the user as
1506    /// `Surface does not support the adapter's queue family`, and was read as
1507    /// a GPU mismatch by everyone who saw it, including the maintainer. A
1508    /// surface with no formats left for the adapter it was matched against has
1509    /// lost its display server, whatever wgpu chooses to call it.
1510    #[test]
1511    fn a_surface_with_no_formats_left_means_the_display_server_is_gone() {
1512        let err = classify_configure_failure(
1513            "Surface does not support the adapter's queue family".to_string(),
1514            false,
1515        );
1516        assert!(matches!(err, SurfaceConfigureError::DisplayLost));
1517        assert!(err.to_string().contains("display server"));
1518    }
1519
1520    /// The other half, and the reason this is not simply "any configure
1521    /// failure means the compositor left": a surface that still answers with
1522    /// formats has a real configuration problem, and its own message has to
1523    /// survive rather than be relabelled as a dead compositor.
1524    #[test]
1525    fn a_surface_that_still_has_formats_keeps_wgpus_own_message() {
1526        let err = classify_configure_failure(
1527            "Requested format Rgba8Unorm is not in the list of supported formats".to_string(),
1528            true,
1529        );
1530        assert!(matches!(err, SurfaceConfigureError::Rejected(_)));
1531        assert!(err.to_string().contains("Rgba8Unorm"));
1532        assert!(!err.to_string().contains("display server"));
1533    }
1534}
1535
1536#[cfg(test)]
1537mod accessibility_bridge_tests {
1538    use super::{AccessibilityBridge, empty_initial_tree};
1539
1540    /// A recognisable tree that is not the placeholder.
1541    fn published_tree() -> accesskit::TreeUpdate {
1542        let root_id = teksilo_core::accessibility::root_node_id();
1543        let child_id = accesskit::NodeId(4242);
1544        let mut root = accesskit::Node::new(accesskit::Role::Window);
1545        root.push_child(child_id);
1546        let mut child = accesskit::Node::new(accesskit::Role::Button);
1547        child.set_label("Save");
1548        accesskit::TreeUpdate {
1549            nodes: vec![(root_id, root), (child_id, child)],
1550            tree: Some(accesskit::TreeInfo::new(root_id)),
1551            tree_id: accesskit::TreeId::ROOT,
1552            focus: root_id,
1553        }
1554    }
1555
1556    #[test]
1557    fn a_fresh_bridge_reports_no_client() {
1558        assert!(!AccessibilityBridge::default().is_active());
1559    }
1560
1561    #[test]
1562    fn activation_before_the_first_frame_answers_with_the_placeholder() {
1563        let bridge = AccessibilityBridge::default();
1564        let update = bridge.on_activate();
1565        assert_eq!(update.nodes.len(), empty_initial_tree().nodes.len());
1566        assert_eq!(update.nodes[0].1.children().len(), 0);
1567        assert!(bridge.is_active());
1568    }
1569
1570    #[test]
1571    fn activation_after_a_frame_answers_with_the_real_tree() {
1572        // The defect this pins: an assistive technology attaching to an idle
1573        // window was shown a childless window node and nothing scheduled a
1574        // frame to replace it.
1575        let bridge = AccessibilityBridge::default();
1576        bridge.publish(&published_tree());
1577        let update = bridge.on_activate();
1578        assert_eq!(
1579            update.nodes.len(),
1580            2,
1581            "the published tree, not a placeholder"
1582        );
1583        assert_eq!(update.nodes[0].1.children().len(), 1);
1584    }
1585
1586    #[test]
1587    fn the_snapshot_is_the_latest_published_tree() {
1588        let bridge = AccessibilityBridge::default();
1589        bridge.publish(&empty_initial_tree());
1590        bridge.publish(&published_tree());
1591        assert_eq!(bridge.on_activate().nodes.len(), 2);
1592    }
1593
1594    #[test]
1595    fn publishing_while_a_client_is_attached_is_skipped() {
1596        // Not a behaviour change anyone can observe through `on_activate` —
1597        // an attached client cannot ask for an initial tree — but it is what
1598        // keeps a per-frame `TreeUpdate` clone off the frame path while a
1599        // screen reader is running.
1600        let bridge = AccessibilityBridge::default();
1601        bridge.publish(&published_tree());
1602        let _ = bridge.on_activate();
1603        bridge.publish(&empty_initial_tree());
1604        bridge.on_deactivate();
1605        assert_eq!(
1606            bridge.on_activate().nodes.len(),
1607            2,
1608            "the tree published while attached must not have replaced the snapshot"
1609        );
1610    }
1611
1612    #[test]
1613    fn deactivation_clears_the_attached_flag() {
1614        let bridge = AccessibilityBridge::default();
1615        let _ = bridge.on_activate();
1616        assert!(bridge.is_active());
1617        bridge.on_deactivate();
1618        assert!(!bridge.is_active());
1619        // And the tree it published is still there for a client that comes back.
1620        bridge.publish(&published_tree());
1621        assert_eq!(bridge.on_activate().nodes.len(), 2);
1622        assert!(bridge.is_active());
1623    }
1624}
1625
1626#[cfg(test)]
1627mod device_limits_tests {
1628    use super::*;
1629
1630    /// A Raspberry Pi 4's V3D driver in the fields that matter here: four
1631    /// colour attachments and 4096-pixel textures. This is the adapter the
1632    /// crash report came from.
1633    fn pi4_class_limits() -> wgpu::Limits {
1634        wgpu::Limits {
1635            max_texture_dimension_1d: 4096,
1636            max_texture_dimension_2d: 4096,
1637            max_texture_dimension_3d: 256,
1638            max_color_attachments: 4,
1639            ..wgpu::Limits::downlevel_defaults()
1640        }
1641    }
1642
1643    #[test]
1644    fn the_default_limits_are_refused_by_gles_class_hardware() {
1645        // The bug, stated as a test: this is what the window used to ask for,
1646        // and `check_limits` is the same comparison wgpu makes inside
1647        // `request_device`. If this ever starts passing, wgpu changed its
1648        // defaults and the fallback below is what keeps us honest.
1649        assert!(
1650            !wgpu::Limits::default().check_limits(&pi4_class_limits()),
1651            "the wgpu default limits are supposed to over-ask for a Pi-4 class \
1652             adapter; that refusal is the crash this module exists to prevent"
1653        );
1654    }
1655
1656    #[test]
1657    fn the_window_ask_is_satisfiable_on_gles_class_hardware() {
1658        let adapter = pi4_class_limits();
1659        assert!(
1660            window_device_limits(adapter.clone()).check_limits(&adapter),
1661            "a Pi-4 class adapter must be able to grant what a window asks for"
1662        );
1663    }
1664
1665    #[test]
1666    fn the_window_never_asks_past_the_downlevel_floor() {
1667        // The regression pin: whatever the adapter offers, every limit that is
1668        // not a texture dimension stays at the GLES-3.1 floor. Re-introducing
1669        // `Limits::default()` fails here on a developer's desktop rather than
1670        // only on a reviewer's Raspberry Pi.
1671        let generous = wgpu::Limits::default();
1672        let asked = window_device_limits(generous.clone());
1673        let floor = wgpu::Limits::downlevel_defaults();
1674
1675        assert_eq!(asked.max_color_attachments, floor.max_color_attachments);
1676        assert_eq!(
1677            asked.max_uniform_buffer_binding_size,
1678            floor.max_uniform_buffer_binding_size
1679        );
1680        assert_eq!(
1681            asked.max_inter_stage_shader_variables,
1682            floor.max_inter_stage_shader_variables
1683        );
1684        assert_eq!(
1685            asked.max_storage_buffers_per_shader_stage,
1686            floor.max_storage_buffers_per_shader_stage
1687        );
1688        assert_ne!(
1689            asked, generous,
1690            "asking for the full default set is exactly the regression"
1691        );
1692    }
1693
1694    #[test]
1695    fn texture_dimensions_follow_the_adapter() {
1696        // `downlevel_defaults` caps 2D textures at 2048 and the path atlas
1697        // grows to 4096, so the resolution limits, and only those, are lifted
1698        // to whatever the adapter really offers.
1699        const PATH_ATLAS_MAX: u32 = 4096;
1700
1701        for adapter in [pi4_class_limits(), wgpu::Limits::default()] {
1702            let asked = window_device_limits(adapter.clone());
1703            assert_eq!(
1704                asked.max_texture_dimension_1d,
1705                adapter.max_texture_dimension_1d
1706            );
1707            assert_eq!(
1708                asked.max_texture_dimension_2d,
1709                adapter.max_texture_dimension_2d
1710            );
1711            assert_eq!(
1712                asked.max_texture_dimension_3d,
1713                adapter.max_texture_dimension_3d
1714            );
1715            assert!(
1716                asked.max_texture_dimension_2d >= PATH_ATLAS_MAX,
1717                "the path atlas grows to {PATH_ATLAS_MAX}; a device that cannot \
1718                 hold it would fail on a path-heavy frame instead of at startup"
1719            );
1720        }
1721    }
1722
1723    #[test]
1724    fn the_floor_still_covers_what_the_renderer_binds() {
1725        // What the renderer actually needs, so that lowering the ask further
1726        // fails here rather than in a frame. 128 animation slots of 64 bytes
1727        // is the largest uniform binding; every render pass has exactly one
1728        // colour attachment.
1729        const ANIM_UNIFORM_BYTES: u64 = 128 * 64;
1730        let asked = window_device_limits(pi4_class_limits());
1731
1732        assert!(asked.max_color_attachments >= 1);
1733        assert!(asked.max_uniform_buffer_binding_size >= ANIM_UNIFORM_BYTES);
1734    }
1735}
1736
1737#[cfg(all(test, not(teksilo_loom)))]
1738mod accessibility_wake_tests {
1739    use std::sync::atomic::AtomicUsize;
1740
1741    use accesskit::{ActionHandler, ActivationHandler, DeactivationHandler};
1742
1743    use super::*;
1744
1745    struct NoWindow;
1746    impl RequestRedraw for NoWindow {
1747        fn request_redraw(&self) {
1748            panic!("an accessibility handler asked for a redraw instead of posting");
1749        }
1750    }
1751
1752    fn request() -> ActionRequest {
1753        ActionRequest {
1754            action: accesskit::Action::Click,
1755            target_tree: accesskit::TreeId::ROOT,
1756            target_node: accesskit::NodeId(1),
1757            data: None,
1758        }
1759    }
1760
1761    /// What a handler leaves must reach a window that draws nothing, so all
1762    /// three wake the loop through the state route, never a redraw request;
1763    /// after the window's waker is disconnected they still enqueue, and wake
1764    /// nothing.
1765    #[test]
1766    fn accessibility_handlers_wake_through_the_state_route() {
1767        let wake = WindowWakeTarget::new(Arc::new(NoWindow));
1768        let posted = Arc::new(AtomicUsize::new(0));
1769        {
1770            let posted = posted.clone();
1771            wake.set_state_route(Arc::new(move || {
1772                posted.fetch_add(1, Ordering::SeqCst);
1773            }));
1774        }
1775        let bridge = Arc::new(AccessibilityBridge::default());
1776        let (tx, rx) = mpsc::channel();
1777        let mut action = TeksiloActionHandler {
1778            tx,
1779            wake: wake.clone(),
1780        };
1781        let mut activation = TeksiloActivationHandler {
1782            needs_full_tree: Arc::new(AtomicBool::new(false)),
1783            bridge: bridge.clone(),
1784            wake: wake.clone(),
1785        };
1786        let mut deactivation = TeksiloDeactivationHandler {
1787            bridge,
1788            wake: wake.clone(),
1789        };
1790
1791        action.do_action(request());
1792        assert!(
1793            rx.try_recv().is_ok(),
1794            "the request is queued for the UI thread"
1795        );
1796        assert_eq!(posted.load(Ordering::SeqCst), 1);
1797        assert!(wake.take_posted(WakeKind::Layout));
1798        let _ = activation.request_initial_tree();
1799        assert_eq!(posted.load(Ordering::SeqCst), 2);
1800        assert!(wake.take_posted(WakeKind::Layout));
1801        deactivation.deactivate_accessibility();
1802        assert_eq!(posted.load(Ordering::SeqCst), 3);
1803
1804        wake.disconnect();
1805        assert!(wake.take_posted(WakeKind::Layout));
1806        action.do_action(request());
1807        assert!(rx.try_recv().is_ok(), "still queued after disconnect");
1808        assert_eq!(posted.load(Ordering::SeqCst), 3, "but nothing is woken");
1809    }
1810}
1811
1812#[cfg(target_os = "macos")]
1813mod macos_occlusion {
1814    use objc2::rc::Retained;
1815    use objc2_app_kit::{NSView, NSWindowOcclusionState};
1816    use raw_window_handle::{HasWindowHandle, RawWindowHandle};
1817
1818    /// `occlusionState` of the `NSWindow` hosting `window`'s view, lacking
1819    /// *Visible*; `None` when the window has no AppKit view or no window yet.
1820    /// On the main thread, as every caller in teksilo-app is.
1821    pub(super) fn occluded(window: &winit::window::Window) -> Option<bool> {
1822        let handle = window.window_handle().ok()?;
1823        let RawWindowHandle::AppKit(raw) = handle.as_raw() else {
1824            return None;
1825        };
1826        // Re-retain the NSView winit hands us, as the title-bar host, the
1827        // drag destination and the safe-area read do.
1828        let view: Retained<NSView> = unsafe { Retained::retain(raw.ns_view.as_ptr().cast()) }?;
1829        let ns_window = view.window()?;
1830        Some(
1831            !ns_window
1832                .occlusionState()
1833                .contains(NSWindowOcclusionState::Visible),
1834        )
1835    }
1836}