//! The winit application shell: window lifecycle, backend selection, frame //! presentation, and AccessKit event interception. use std::error::Error; use std::fmt; use std::num::NonZeroU32; use std::sync::{Arc, Mutex}; use crate::gpu::GpuSurface; use guiduck_core::clipboard::{Clipboard, Selection, TextRequest}; use guiduck_core::{ ImeInput, Key as CoreKey, KeyInput, Modifiers, PointerButton, PointerInput, WidgetTree, }; use guiduck_render_tinyskia::TinySkiaBackend; use guiduck_render_vello::VelloBackend; use guiduck_scene::geom::{Point, Size}; use guiduck_scene::{FragmentId, FragmentStore, RenderBackend, RenderError}; use winit::application::ApplicationHandler; use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}; use winit::keyboard::{Key, NamedKey}; use winit::window::{Window, WindowId}; /// Produces the frame's fragment tree. [`WidgetTree`] implements this (layout /// then paint); anything else that can emit fragments — like the raw scene /// examples — can implement it directly. pub trait FrameSource { /// Build (or update) the scene for a viewport of the given logical size, /// returning the root fragment and the store holding it. The source owns /// its store, which is what lets it retain unchanged fragments across /// frames instead of rebuilding the scene from scratch. fn frame(&mut self, viewport: Size) -> (FragmentId, &FragmentStore); /// The current accessibility tree, if this source has one. Sources /// without semantic content return `None` and assistive technologies see /// a bare window. fn accessibility(&mut self) -> Option { None } /// Feed pointer input (logical window coordinates). Sources without /// interaction ignore it. The clipboard travels with pointer input /// because of the primary selection (drag-select stores, middle-click /// pastes). fn pointer(&mut self, _input: PointerInput, _clipboard: &mut dyn Clipboard) {} /// A file was dropped on the window. Sources without a drop target ignore /// it; the framework carries the path and the app interprets it. fn file_drop(&mut self, _path: std::path::PathBuf) {} /// Whether internal state changed such that a new frame should be /// rendered. The shell polls this after delivering input and after each /// frame; returning false is what keeps an idle app at zero frames. fn needs_frame(&mut self) -> bool { false } /// Handle a keyboard event; return true to consume it. fn key(&mut self, _key: KeyInput, _clipboard: &mut dyn Clipboard) -> bool { false } /// Handle an input-method event. fn ime(&mut self, _ime: ImeInput) {} /// Perform an accessibility action requested by an assistive technology. fn accessibility_action(&mut self, _request: &accesskit::ActionRequest) {} /// Where the input method should place candidate windows, in logical /// window coordinates. fn ime_cursor_area(&mut self) -> Option { None } /// The pointer cursor for the current hover target. fn cursor(&mut self) -> guiduck_core::CursorShape { guiduck_core::CursorShape::Default } /// The earliest moment this source needs waking (a pending timer), or /// `None` to wait indefinitely — which is what keeps an idle app at /// zero frames. `now` is supplied by the shell so sources stay /// clock-free and deterministic under test. fn next_wake(&mut self, _now: std::time::Instant) -> Option { None } /// A deadline from [`next_wake`](Self::next_wake) has passed; run due /// timers. A needs-frame check follows. fn tick(&mut self, _now: std::time::Instant) {} /// Receive a handle that wakes the event loop from other threads. /// Called once at startup; sources without background activity ignore /// it. fn connect_waker(&mut self, _waker: Waker) {} /// Process out-of-band work (e.g. hot-reload file events). Called on /// every [`Waker`] wake; a needs-frame check follows. fn poll(&mut self) {} } impl FrameSource for WidgetTree { fn connect_waker(&mut self, waker: Waker) { // Background work (a file dialog, a clipboard load) finishes on its // own thread and needs to say so; this is the handle that lets it. crate::background::set_waker(waker.clone()); // So does a watcher noticing a saved component file. self.connect_live_waker(move || waker.wake()); } fn poll(&mut self) { crate::background::deliver(self); self.poll_live_reload(); } fn frame(&mut self, viewport: Size) -> (FragmentId, &FragmentStore) { let root = self .render_frame(viewport) .expect("a widget tree used as a frame source must have a root"); (root, self.fragments()) } fn accessibility(&mut self) -> Option { Some(self.accessibility_tree()) } fn pointer(&mut self, input: PointerInput, clipboard: &mut dyn Clipboard) { self.dispatch_pointer(input, clipboard); } fn file_drop(&mut self, path: std::path::PathBuf) { self.drop_file(path.to_string_lossy().into_owned()); } fn key(&mut self, key: KeyInput, clipboard: &mut dyn Clipboard) -> bool { self.dispatch_key(&key, clipboard) } fn ime(&mut self, ime: ImeInput) { self.dispatch_ime(&ime); } fn accessibility_action(&mut self, request: &accesskit::ActionRequest) { WidgetTree::accessibility_action(self, request); } fn ime_cursor_area(&mut self) -> Option { WidgetTree::ime_cursor_area(self) } fn needs_frame(&mut self) -> bool { WidgetTree::needs_frame(self) } fn cursor(&mut self) -> guiduck_core::CursorShape { self.cursor_shape() } fn next_wake(&mut self, now: std::time::Instant) -> Option { WidgetTree::next_wake(self, now) } fn tick(&mut self, now: std::time::Instant) { WidgetTree::tick(self, now); } } /// Which render backend the shell drives. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum BackendKind { Vello, Software, } impl BackendKind { /// Read the backend choice from `GUIDUCK_BACKEND` (`vello` or /// `software`), defaulting to vello. pub fn from_env() -> Result { match std::env::var("GUIDUCK_BACKEND") { Ok(value) => match value.as_str() { "vello" => Ok(Self::Vello), "software" => Ok(Self::Software), other => Err(ShellError::Config(format!( "unknown GUIDUCK_BACKEND {other:?}; expected \"vello\" or \"software\"" ))), }, Err(std::env::VarError::NotPresent) => Ok(Self::Vello), Err(e) => Err(ShellError::Config(format!("GUIDUCK_BACKEND: {e}"))), } } } /// Failure while running the shell. #[derive(Debug)] pub enum ShellError { Config(String), Render(RenderError), Platform(Box), } impl fmt::Display for ShellError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ShellError::Config(msg) => write!(f, "configuration error: {msg}"), ShellError::Render(e) => write!(f, "render error: {e}"), ShellError::Platform(e) => write!(f, "platform error: {e}"), } } } impl Error for ShellError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { ShellError::Render(e) => Some(e), ShellError::Platform(e) => Some(e.as_ref()), ShellError::Config(_) => None, } } } impl From for ShellError { fn from(e: RenderError) -> Self { ShellError::Render(e) } } /// Events delivered through the winit user-event channel. #[derive(Debug)] pub enum ShellEvent { Access(accesskit_winit::Event), /// An external thread asked the loop to wake and poll the source. Wake, } impl From for ShellEvent { fn from(event: accesskit_winit::Event) -> Self { Self::Access(event) } } /// Wakes the UI event loop from any thread; the shell then calls /// [`FrameSource::poll`]. Hot reload uses this to apply file changes /// immediately instead of on the next input event. #[derive(Clone)] pub struct Waker { proxy: EventLoopProxy, } impl Waker { pub fn wake(&self) { // A closed event loop just means shutdown; nothing to do. let _ = self.proxy.send_event(ShellEvent::Wake); } } /// Logical pixels per wheel-notch line, for `MouseScrollDelta::LineDelta`. const LINE_SCROLL_PX: f64 = 40.0; /// The platform clipboard: smithay-clipboard on Wayland, otherwise inert. /// /// Two smithay instances, not one. `load()` blocks until the selection's /// owner has served the content over a pipe — as long as it likes, forever /// if it is wedged — so loads run on [`background`](crate::background) /// threads and deliver through `WidgetTree::dispatch_paste`. The instance /// they share sits behind a `Mutex` because concurrent `load()` calls would /// cross replies on its single answer channel (`Clipboard` is not `Sync`). /// Stores are fire-and-forget and must never queue behind a wedged load, /// which is exactly what sharing that mutex with the UI thread would do — /// hence their own instance, touched only here. enum ShellClipboard { None, Wayland { store: smithay_clipboard::Clipboard, load: Arc>, }, } impl Clipboard for ShellClipboard { fn request_text(&mut self, selection: Selection) -> TextRequest { match self { ShellClipboard::None => TextRequest::Ready(None), ShellClipboard::Wayland { load, .. } => { let load = Arc::clone(load); crate::background::run( move || { let clipboard = load.lock().expect("not poisoned"); match selection { Selection::Clipboard => clipboard.load().ok(), // On a compositor without zwp_primary_selection // this returns Err, which is exactly the None // we want. Selection::Primary => clipboard.load_primary().ok(), } }, |tree, text| { // An empty or failed read has nothing to paste. if let Some(text) = text.filter(|t| !t.is_empty()) { tree.dispatch_paste(&text); } }, ); TextRequest::Pending } } } fn set_text(&mut self, selection: Selection, text: &str) { if let ShellClipboard::Wayland { store, .. } = self { match selection { Selection::Clipboard => store.store(text), Selection::Primary => store.store_primary(text), } } } } /// Run the shell until the window closes. The backend is chosen via /// `GUIDUCK_BACKEND` (default vello). pub fn run(title: &str, source: impl FrameSource) -> Result<(), ShellError> { let backend_kind = BackendKind::from_env()?; let event_loop = EventLoop::::with_user_event() .build() .map_err(|e| ShellError::Platform(Box::new(e)))?; let proxy = event_loop.create_proxy(); let mut shell = Shell { title: title.to_owned(), source, backend_kind, proxy, window: None, adapter: None, backend: None, error: None, cursor: None, cursor_shape: guiduck_core::CursorShape::Default, modifiers: Modifiers::default(), clipboard: ShellClipboard::None, trace_frames: std::env::var_os("GUIDUCK_TRACE_FRAMES").is_some_and(|v| v != "0"), trace_input: std::env::var_os("GUIDUCK_TRACE_INPUT").is_some_and(|v| v != "0"), frame_counter: 0, redraw_deadline: None, redraw_gate_suspect: false, last_direct_redraw: None, }; event_loop .run_app(&mut shell) .map_err(|e| ShellError::Platform(Box::new(e)))?; match shell.error { Some(e) => Err(e), None => Ok(()), } } struct Shell { title: String, source: F, backend_kind: BackendKind, proxy: EventLoopProxy, window: Option>, adapter: Option, backend: Option, error: Option, /// Last pointer position, in logical window coordinates. cursor: Option, /// The cursor shape last handed to the window, to skip redundant sets. cursor_shape: guiduck_core::CursorShape, modifiers: Modifiers, clipboard: ShellClipboard, /// `GUIDUCK_TRACE_FRAMES`: log one stderr line per rendered frame, so /// tests can assert that idle applications render nothing. trace_frames: bool, /// `GUIDUCK_TRACE_INPUT`: log translated key and IME events, for /// debugging input-method integration. trace_input: bool, frame_counter: u64, /// When the redraw requested from winit should have arrived by; None /// while none is outstanding. The renderer's dead-man switch: on /// Wayland, winit delivers `RedrawRequested` only after the /// compositor's frame callback, and the compositor sends a frame /// callback only after the next commit — one lost callback deadlocks /// that cycle forever, freezing the display while input keeps /// flowing (seen in the wild under Hyprland special workspaces). redraw_deadline: Option, /// True from the moment a deadline expires until `RedrawRequested` /// delivery is observed again. While set, the shell renders directly, /// paced to roughly a display refresh; the presents also recommit the /// surface, which is what restarts the compositor's callbacks. redraw_gate_suspect: bool, /// The last direct (watchdog) render, for pacing. last_direct_redraw: Option, } /// How long a requested redraw may remain undelivered before the shell /// stops trusting the event loop and renders directly. const REDRAW_WATCHDOG: std::time::Duration = std::time::Duration::from_millis(1000); /// Pacing for direct renders while delivery is broken: roughly a 60 Hz /// frame budget, since no compositor callback is throttling us. const DIRECT_REDRAW_INTERVAL: std::time::Duration = std::time::Duration::from_millis(16); enum BackendState { Vello { surface: GpuSurface, backend: VelloBackend, }, Software { surface: softbuffer::Surface, Arc>, backend: TinySkiaBackend, }, } impl Shell { fn fail(&mut self, event_loop: &ActiveEventLoop, error: ShellError) { self.error = Some(error); event_loop.exit(); } /// Deliver pointer input to the source, follow the hover target's /// cursor shape, and request a frame if anything went dirty. fn pointer(&mut self, input: PointerInput) { self.source.pointer(input, &mut self.clipboard); let shape = self.source.cursor(); if shape != self.cursor_shape { self.cursor_shape = shape; if let Some(window) = &self.window { window.set_cursor(winit::window::Cursor::Icon(match shape { guiduck_core::CursorShape::Default => winit::window::CursorIcon::Default, guiduck_core::CursorShape::Pointer => winit::window::CursorIcon::Pointer, guiduck_core::CursorShape::Text => winit::window::CursorIcon::Text, })); } } self.request_frame_if_needed(); } /// After keys or IME events: reposition the IME candidate window at the /// focused editor's cursor, and schedule a frame if anything changed. fn after_text_input(&mut self) { if let (Some(window), Some(area)) = (self.window.clone(), self.source.ime_cursor_area()) { window.set_ime_cursor_area( winit::dpi::LogicalPosition::new(area.x0, area.y1), winit::dpi::LogicalSize::new(area.width(), area.height()), ); } self.request_frame_if_needed(); } fn request_frame_if_needed(&mut self) { if self.source.needs_frame() && let Some(window) = &self.window { window.request_redraw(); // Arm the dead-man switch (an already-armed one keeps its // deadline). Delivery clears it; expiry means the event loop // has stopped delivering redraws and the shell takes over. if self.redraw_deadline.is_none() { self.redraw_deadline = Some(std::time::Instant::now() + REDRAW_WATCHDOG); } } } /// The dead-man switch (see the `redraw_deadline` field): if a /// requested redraw is overdue, stop waiting for the event loop and /// render directly, paced to a frame budget, until delivery resumes. /// The direct presents recommit the surface, which restarts the /// compositor's frame callbacks — so besides keeping the display /// truthful, this usually breaks the deadlock that caused it. fn check_redraw_watchdog(&mut self) { if self .redraw_deadline .is_some_and(|deadline| std::time::Instant::now() >= deadline) { self.redraw_deadline = None; if !self.redraw_gate_suspect { self.redraw_gate_suspect = true; eprintln!( "guiduck: RedrawRequested overdue by {}ms+; rendering directly until delivery resumes", REDRAW_WATCHDOG.as_millis() ); } } if self.redraw_gate_suspect && self.source.needs_frame() { let now = std::time::Instant::now(); if self .last_direct_redraw .is_none_or(|last| now - last >= DIRECT_REDRAW_INTERVAL) { self.last_direct_redraw = Some(now); // Not fatal, unlike the delivered-redraw path: a hidden or // mid-reconfigure surface can refuse a frame, and the // watchdog simply tries again on the next check. match self.redraw() { Ok(()) => { if self.trace_frames { self.frame_counter += 1; eprintln!("guiduck-frame {} (watchdog)", self.frame_counter); } self.push_accessibility(); } Err(e) => eprintln!("guiduck: watchdog render failed: {e}"), } } } } fn scale(&self) -> f64 { self.window.as_ref().map_or(1.0, |w| w.scale_factor()) } /// Send the source's current accessibility tree to any active assistive /// technology. No-op (and no tree construction) when none is listening. fn push_accessibility(&mut self) { let Some(adapter) = self.adapter.as_mut() else { return; }; let source = &mut self.source; adapter.update_if_active(|| source.accessibility().unwrap_or_else(bare_window_tree)); } fn init_window(&mut self, event_loop: &ActiveEventLoop) -> Result<(), ShellError> { let attrs = Window::default_attributes() .with_title(self.title.clone()) // The AccessKit adapter must exist before the window is first // shown, so the window starts invisible. .with_visible(false); let window = Arc::new( event_loop .create_window(attrs) .map_err(|e| ShellError::Platform(Box::new(e)))?, ); let adapter = accesskit_winit::Adapter::with_event_loop_proxy( event_loop, &window, self.proxy.clone(), ); let size = window.inner_size(); let backend = match self.backend_kind { BackendKind::Vello => { let surface = pollster::block_on(GpuSurface::new( window.clone(), size.width.max(1), size.height.max(1), wgpu::PresentMode::AutoVsync, )) .map_err(ShellError::Platform)?; let backend = VelloBackend::new(surface.device.clone(), surface.queue.clone())?; BackendState::Vello { surface, backend } } BackendKind::Software => { let context = softbuffer::Context::new(window.clone()) .map_err(|e| ShellError::Platform(Box::new(e)))?; let surface = softbuffer::Surface::new(&context, window.clone()) .map_err(|e| ShellError::Platform(Box::new(e)))?; BackendState::Software { surface, backend: TinySkiaBackend::new(), } } }; // Text widgets drive the input method; harmless when nothing // focusable exists. window.set_ime_allowed(true); // The clipboard is tied to the Wayland connection. self.clipboard = wayland_clipboard(&window); window.set_visible(true); self.window = Some(window); self.adapter = Some(adapter); self.backend = Some(backend); Ok(()) } fn resize(&mut self, width: u32, height: u32) { if width == 0 || height == 0 { return; } match self.backend.as_mut() { Some(BackendState::Vello { surface, .. }) => { surface.resize(width, height); } Some(BackendState::Software { surface, .. }) => { let (Some(w), Some(h)) = (NonZeroU32::new(width), NonZeroU32::new(height)) else { return; }; // Errors here recur on present; report them there instead. let _ = surface.resize(w, h); } None => {} } if let Some(window) = &self.window { window.request_redraw(); } } fn redraw(&mut self) -> Result<(), ShellError> { let Some(window) = self.window.clone() else { return Ok(()); }; let size = window.inner_size(); if size.width == 0 || size.height == 0 { return Ok(()); } let scale = window.scale_factor(); let viewport = Size::new(size.width as f64 / scale, size.height as f64 / scale); // The source owns the scene and retains unchanged fragments across // frames; the shell just asks for the current root. let (root, store) = self.source.frame(viewport); match self.backend.as_mut().expect("backend initialized") { BackendState::Vello { surface, backend } => { backend.set_target(surface.target_view()); backend.render(root, store, size.width, size.height, scale)?; let frame = match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(frame) | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, // Transient conditions: skip this frame and wait for the // next redraw request. wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return Ok(()), // The surface needs reconfiguring; do so and retry on the // next frame. wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => { surface.resize(size.width, size.height); surface.reconfigure(); window.request_redraw(); return Ok(()); } wgpu::CurrentSurfaceTexture::Validation => { return Err(ShellError::Platform("wgpu surface validation error".into())); } }; let frame_view = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); surface.blit_to(&frame_view); window.pre_present_notify(); frame.present(); } BackendState::Software { surface, backend } => { backend.render(root, store, size.width, size.height, scale)?; let (Some(w), Some(h)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) else { return Ok(()); }; surface .resize(w, h) .map_err(|e| ShellError::Platform(Box::new(e)))?; let mut buffer = surface .buffer_mut() .map_err(|e| ShellError::Platform(Box::new(e)))?; for (dst, px) in buffer.iter_mut().zip(backend.pixmap().pixels()) { let c = px.demultiply(); *dst = (u32::from(c.red()) << 16) | (u32::from(c.green()) << 8) | u32::from(c.blue()); } window.pre_present_notify(); buffer .present() .map_err(|e| ShellError::Platform(Box::new(e)))?; } } Ok(()) } } impl ApplicationHandler for Shell { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.window.is_none() { match self.init_window(event_loop) { Ok(()) => { let waker = Waker { proxy: self.proxy.clone(), }; self.source.connect_waker(waker); } Err(e) => self.fail(event_loop, e), } } } /// Release everything that belongs to the display, while the display is /// still there. /// /// `EventLoop::run_app` takes the loop by value, so by the time it returns /// the platform connection is already gone — and on Wayland that means the /// `wl_display` these objects were made from has been destroyed. Dropping /// them with the `Shell` afterwards is a use-after-free: the clipboard's /// worker thread tears down its selection proxies through a freed /// connection and the process dies in `wl_proxy_destroy`, after the /// application's last line of code has run. /// /// This hook is the last point at which the connection is guaranteed /// alive, so it is where they go. Order within it is the usual one — /// surfaces before the window they were made from. fn exiting(&mut self, _event_loop: &ActiveEventLoop) { self.clipboard = ShellClipboard::None; self.backend = None; self.adapter = None; self.window = None; } fn window_event( &mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent, ) { // AccessKit must observe events before the application reacts to // them; this ordering is part of the adapter's contract. if let (Some(adapter), Some(window)) = (self.adapter.as_mut(), self.window.as_ref()) { adapter.process_event(window, &event); } match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::ModifiersChanged(modifiers) => { let state = modifiers.state(); self.modifiers = Modifiers { ctrl: state.control_key(), shift: state.shift_key(), alt: state.alt_key(), logo: state.super_key(), }; } WindowEvent::KeyboardInput { event, .. } => { let input = KeyInput { key: translate_key(&event), modifiers: self.modifiers, pressed: event.state == ElementState::Pressed, }; if self.trace_input { eprintln!("guiduck-input: {input:?}"); } self.source.key(input, &mut self.clipboard); self.after_text_input(); } WindowEvent::Ime(ime) => { let input = match ime { winit::event::Ime::Enabled => ImeInput::Enabled, winit::event::Ime::Preedit(text, cursor) => ImeInput::Preedit { text, cursor }, winit::event::Ime::Commit(text) => ImeInput::Commit(text), winit::event::Ime::Disabled => ImeInput::Disabled, }; if self.trace_input { eprintln!("guiduck-input: ime {input:?}"); } self.source.ime(input); self.after_text_input(); } WindowEvent::Resized(size) => self.resize(size.width, size.height), WindowEvent::ScaleFactorChanged { .. } => { if let Some(window) = &self.window { window.request_redraw(); } } WindowEvent::CursorMoved { position, .. } => { let scale = self.scale(); let pos = Point::new(position.x / scale, position.y / scale); self.cursor = Some(pos); self.pointer(PointerInput::Moved(pos)); } WindowEvent::CursorLeft { .. } => { self.cursor = None; self.pointer(PointerInput::Left); } WindowEvent::DroppedFile(path) => { self.source.file_drop(path); self.request_frame_if_needed(); } WindowEvent::MouseWheel { delta, .. } => { if let Some(pos) = self.cursor { let delta = match delta { winit::event::MouseScrollDelta::LineDelta(x, y) => { guiduck_scene::geom::Vec2::new( f64::from(x) * LINE_SCROLL_PX, f64::from(y) * LINE_SCROLL_PX, ) } winit::event::MouseScrollDelta::PixelDelta(p) => { let scale = self.scale(); guiduck_scene::geom::Vec2::new(p.x / scale, p.y / scale) } }; self.pointer(PointerInput::Scroll { pos, delta }); } } WindowEvent::MouseInput { state, button, .. } => { if let Some(pos) = self.cursor { let button = match button { MouseButton::Left => PointerButton::Left, MouseButton::Right => PointerButton::Right, MouseButton::Middle => PointerButton::Middle, MouseButton::Back => PointerButton::Other(4), MouseButton::Forward => PointerButton::Other(5), MouseButton::Other(n) => PointerButton::Other(n), }; let input = match state { ElementState::Pressed => PointerInput::Down { pos, button, time: std::time::Instant::now(), }, ElementState::Released => PointerInput::Up { pos, button }, }; self.pointer(input); } } WindowEvent::RedrawRequested => { // The event loop delivered; the dead-man switch stands down. self.redraw_deadline = None; if self.redraw_gate_suspect { self.redraw_gate_suspect = false; eprintln!("guiduck: RedrawRequested delivery resumed"); } match self.redraw() { Ok(()) => { if self.trace_frames { self.frame_counter += 1; eprintln!("guiduck-frame {}", self.frame_counter); } self.push_accessibility(); // Effects run during the frame may have re-dirtied state // (continuation frames). self.request_frame_if_needed(); } Err(e) => self.fail(event_loop, e), } } _ => {} } } fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ShellEvent) { match event { ShellEvent::Wake => { self.source.poll(); self.request_frame_if_needed(); } ShellEvent::Access(event) => match event.window_event { accesskit_winit::WindowEvent::InitialTreeRequested => { self.push_accessibility(); } accesskit_winit::WindowEvent::ActionRequested(request) => { self.source.accessibility_action(&request); self.request_frame_if_needed(); } accesskit_winit::WindowEvent::AccessibilityDeactivated => {} }, } } fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) { // A timer deadline came due: run the source's timers, then render // if they dirtied anything. if matches!(cause, winit::event::StartCause::ResumeTimeReached { .. }) { self.source.tick(std::time::Instant::now()); self.request_frame_if_needed(); } } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { // This runs after every batch of events, which makes it the one // place the watchdog needs checking: every wake passes through // here, including the deadline wakes scheduled just below. self.check_redraw_watchdog(); // Sleep until the source's next timer deadline, or indefinitely // when it has none — the zero-idle-frames guarantee is exactly // this Wait. The watchdog adds its own deadlines: the arrival // check for an outstanding redraw, and the next paced render // while delivery is broken. let now = std::time::Instant::now(); let mut deadline = self.source.next_wake(now); let mut cap = |candidate: std::time::Instant| { deadline = Some(deadline.map_or(candidate, |d: std::time::Instant| d.min(candidate))); }; if let Some(pending) = self.redraw_deadline { cap(pending); } if self.redraw_gate_suspect && self.source.needs_frame() { cap(self .last_direct_redraw .map_or(now, |last| last + DIRECT_REDRAW_INTERVAL)); } event_loop.set_control_flow(match deadline { Some(deadline) => winit::event_loop::ControlFlow::WaitUntil(deadline), None => winit::event_loop::ControlFlow::Wait, }); } } /// The fallback accessibility tree for sources without semantics: a lone /// window node. fn bare_window_tree() -> accesskit::TreeUpdate { const ROOT: accesskit::NodeId = accesskit::NodeId(0); let mut root = accesskit::Node::new(accesskit::Role::Window); root.set_label("guiduck"); accesskit::TreeUpdate { nodes: vec![(ROOT, root)], tree: Some(accesskit::Tree::new(ROOT)), tree_id: accesskit::TreeId::ROOT, focus: ROOT, } } /// Build the clipboard for the window's Wayland connection, if any. fn wayland_clipboard(window: &Window) -> ShellClipboard { use winit::raw_window_handle::{HasDisplayHandle, RawDisplayHandle}; match window.display_handle().map(|h| h.as_raw()) { Ok(RawDisplayHandle::Wayland(handle)) => { // Safety: the display pointer is valid for the window's // lifetime, and the shell owns both. (A load wedged on a dead // selection owner can keep its instance alive on a background // thread past teardown; such a thread dies with the process, // which is where a closing shell is headed anyway.) let store = unsafe { smithay_clipboard::Clipboard::new(handle.display.as_ptr()) }; let load = unsafe { smithay_clipboard::Clipboard::new(handle.display.as_ptr()) }; ShellClipboard::Wayland { store, load: Arc::new(Mutex::new(load)), } } _ => ShellClipboard::None, } } /// Reduce a winit key event to the tree's vocabulary. fn translate_key(event: &KeyEvent) -> CoreKey { match &event.logical_key { Key::Named(named) => match named { NamedKey::Backspace => CoreKey::Backspace, NamedKey::Delete => CoreKey::Delete, NamedKey::ArrowLeft => CoreKey::Left, NamedKey::ArrowRight => CoreKey::Right, NamedKey::ArrowUp => CoreKey::Up, NamedKey::ArrowDown => CoreKey::Down, NamedKey::Home => CoreKey::Home, NamedKey::PageUp => CoreKey::PageUp, NamedKey::PageDown => CoreKey::PageDown, NamedKey::End => CoreKey::End, NamedKey::Enter => CoreKey::Enter, NamedKey::Tab => CoreKey::Tab, NamedKey::Escape => CoreKey::Escape, NamedKey::Space => CoreKey::Character(" ".to_owned()), _ => CoreKey::Other, }, Key::Character(_) => { // Prefer the produced text (dead keys, layouts); fall back to // the logical character. match event.text.as_ref() { Some(text) => CoreKey::Character(text.to_string()), None => match &event.logical_key { Key::Character(c) => CoreKey::Character(c.to_string()), _ => CoreKey::Other, }, } } _ => CoreKey::Other, } }