shell.rs
raw
//! 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<accesskit::TreeUpdate> {
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<guiduck_scene::geom::Rect> {
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<std::time::Instant> {
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<accesskit::TreeUpdate> {
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<guiduck_scene::geom::Rect> {
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<std::time::Instant> {
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<Self, ShellError> {
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<dyn Error>),
}
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<RenderError> 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<accesskit_winit::Event> 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<ShellEvent>,
}
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<Mutex<smithay_clipboard::Clipboard>>,
},
}
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::<ShellEvent>::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<F: FrameSource> {
title: String,
source: F,
backend_kind: BackendKind,
proxy: EventLoopProxy<ShellEvent>,
window: Option<Arc<Window>>,
adapter: Option<accesskit_winit::Adapter>,
backend: Option<BackendState>,
error: Option<ShellError>,
/// Last pointer position, in logical window coordinates.
cursor: Option<Point>,
/// 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<std::time::Instant>,
/// 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<std::time::Instant>,
}
/// 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<Window>, Arc<Window>>,
backend: TinySkiaBackend,
},
}
impl<F: FrameSource> Shell<F> {
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<F: FrameSource> ApplicationHandler<ShellEvent> for Shell<F> {
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,
}
}