GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/backend/app.rs (31.3K)
1 //! The client contract: the `Application` trait and the plain types it
2 //! speaks in (`WindowSettings`, `LayerSettings`, `LogicalPosition`, …).
3 //! Moved out of `window_runner` unchanged; still re-exported from there
4 //! (and from `engine`) at the old paths.
5
6 use cosmic_text::FontSystem;
7 use crate::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent};
8 #[cfg(not(target_arch = "wasm32"))]
9 use crate::vk::VkRenderer;
10 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
11 use super::window_runner::EngineState;
12 use cursor_icon::CursorIcon;
13 use super::tessellate::Vertex;
14 pub use crate::draw::scene::Stage3D;
15
16 #[derive(Debug, Clone)]
17 pub struct WindowSettings {
18 pub title: String,
19 pub app_id: String,
20 pub width: u32,
21 pub height: u32,
22 pub fullscreen: bool,
23 pub min_size: Option<(u32, u32)>,
24 }
25
26 /// The edge a [`WindowAction::Resize`] grabs. On Wayland it is
27 /// `xdg_toplevel`'s own enum, as it always was; elsewhere the driver's.
28 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
29 pub use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as WindowEdge;
30 #[cfg(any(target_arch = "wasm32", target_os = "macos"))]
31 pub use super::driver::ResizeEdge as WindowEdge;
32
33 /// A compositor-side window operation requested by the app: an interactive
34 /// move or resize grab. Returned from [`Application::take_window_action`];
35 /// the runner executes it with the serial of the most recent pointer press.
36 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
37 pub enum WindowAction {
38 Move,
39 Resize(WindowEdge),
40 }
41
42 // Re-export the wlr-layer-shell types apps need to describe a layer surface.
43 // Layer surfaces are a Wayland (wlr) notion: native only, like `layer()`.
44 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
45 pub use smithay_client_toolkit::shell::wlr_layer::{
46 Anchor as LayerAnchor, KeyboardInteractivity as LayerKeyboardInteractivity, Layer as LayerKind,
47 };
48
49 /// Opt-in configuration for running an [`Application`] on a wlr-layer-shell
50 /// surface (panels, overlays, notifications) instead of an xdg toplevel.
51 /// Return one from [`Application::layer`] to select layer-shell.
52 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
53 #[derive(Debug, Clone)]
54 pub struct LayerSettings {
55 pub layer: LayerKind,
56 pub anchor: LayerAnchor,
57 pub exclusive_zone: i32,
58 pub keyboard_interactivity: LayerKeyboardInteractivity,
59 /// (top, right, bottom, left) margins in logical pixels.
60 pub margin: (i32, i32, i32, i32),
61 pub namespace: String,
62 }
63
64 #[derive(Debug, Clone, Copy, PartialEq)]
65 pub struct LogicalPosition {
66 pub x: f32,
67 pub y: f32,
68 }
69
70 impl LogicalPosition {
71 pub fn new(x: f32, y: f32) -> Self {
72 Self { x, y }
73 }
74 }
75
76 #[derive(Debug, Clone, Copy, PartialEq)]
77 pub struct LogicalSize {
78 pub width: f32,
79 pub height: f32,
80 }
81
82 impl LogicalSize {
83 pub fn new(width: f32, height: f32) -> Self {
84 Self { width, height }
85 }
86 }
87
88 pub struct RenderContext<'a> {
89 pub font_system: &'a mut FontSystem,
90 }
91
92 /// The app's handle for posting a message to itself: from a worker thread, a
93 /// callback, a timer the app runs itself. Each message reaches
94 /// [`Application::update`] on the UI thread and wakes an idle loop, so
95 /// background results arrive without polling (see "`tick` is not a clock"
96 /// in CLAUDE.md).
97 ///
98 /// It names no window system. Handed to [`Application::create`], it is what
99 /// a client written against it can be run by any shell with; the Wayland
100 /// runner backs it with its calloop channel, and `From` converts both ways
101 /// for a client that still keeps a `calloop::channel::Sender` somewhere.
102 /// Elsewhere (the browser, macOS) it is a `std::sync::mpsc` sender whose
103 /// receiver the shell drains every turn — still `Send`, so app code that
104 /// hands it to a worker compiles unchanged — and a send wakes the shell's
105 /// loop through `set_wake`, as a calloop channel wakes the Wayland one.
106 pub struct AppSender<M> {
107 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
108 inner: calloop::channel::Sender<M>,
109 #[cfg(any(target_arch = "wasm32", target_os = "macos"))]
110 inner: std::sync::mpsc::Sender<M>,
111 }
112
113 impl<M> AppSender<M> {
114 /// Post `msg` to the app. Fails, handing it back, only once the loop is
115 /// gone: the app has exited.
116 pub fn send(&self, msg: M) -> Result<(), std::sync::mpsc::SendError<M>> {
117 self.inner.send(msg)?;
118 #[cfg(any(target_arch = "wasm32", target_os = "macos"))]
119 wake();
120 Ok(())
121 }
122 }
123
124 /// Call the hook [`set_wake`] installed, if any.
125 #[cfg(target_arch = "wasm32")]
126 pub(crate) fn wake() {
127 WAKE.with(|w| {
128 if let Some(wake) = w.borrow().as_ref() {
129 wake();
130 }
131 });
132 }
133
134 #[cfg(target_arch = "wasm32")]
135 thread_local! {
136 static WAKE: std::cell::RefCell<Option<Box<dyn Fn()>>> = const { std::cell::RefCell::new(None) };
137 }
138
139 /// What an [`AppSender::send`] on this thread calls after posting: the
140 /// browser shell's "turn the loop soon". Per thread, and the page's app runs
141 /// on the one thread a page has; a send from a worker thread posts without
142 /// waking, and is drained at the next turn the page takes.
143 #[cfg(target_arch = "wasm32")]
144 pub fn set_wake(wake: Option<Box<dyn Fn()>>) {
145 WAKE.with(|w| *w.borrow_mut() = wake);
146 }
147
148 /// Call the hook [`set_wake`] installed, if any, from whatever thread sent.
149 #[cfg(target_os = "macos")]
150 pub(crate) fn wake() {
151 let hook = WAKE.read().ok().and_then(|w| w.clone());
152 if let Some(wake) = hook {
153 wake();
154 }
155 }
156
157 #[cfg(target_os = "macos")]
158 static WAKE: std::sync::RwLock<Option<std::sync::Arc<dyn Fn() + Send + Sync>>> = std::sync::RwLock::new(None);
159
160 /// What an [`AppSender::send`] calls after posting: the AppKit shell's "turn
161 /// the loop soon". Process-wide, unlike the browser's: an AppKit app's
162 /// workers send from their own threads, and the hook hops to the main thread
163 /// itself, as a calloop channel's ping wakes the Wayland loop from anywhere.
164 #[cfg(target_os = "macos")]
165 pub fn set_wake(wake: Option<std::sync::Arc<dyn Fn() + Send + Sync>>) {
166 if let Ok(mut w) = WAKE.write() {
167 *w = wake;
168 }
169 }
170
171 impl<M> Clone for AppSender<M> {
172 fn clone(&self) -> Self {
173 Self { inner: self.inner.clone() }
174 }
175 }
176
177 impl<M> std::fmt::Debug for AppSender<M> {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 f.write_str("AppSender")
180 }
181 }
182
183 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
184 impl<M> From<calloop::channel::Sender<M>> for AppSender<M> {
185 fn from(inner: calloop::channel::Sender<M>) -> Self {
186 Self { inner }
187 }
188 }
189
190 #[cfg(any(target_arch = "wasm32", target_os = "macos"))]
191 impl<M> From<std::sync::mpsc::Sender<M>> for AppSender<M> {
192 fn from(inner: std::sync::mpsc::Sender<M>) -> Self {
193 Self { inner }
194 }
195 }
196
197 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
198 impl<M> From<AppSender<M>> for calloop::channel::Sender<M> {
199 fn from(sender: AppSender<M>) -> Self {
200 sender.inner
201 }
202 }
203
204 pub trait Application: Sized + 'static {
205 type Message: Send + Clone + 'static;
206
207 /// Build the app. `sender` posts messages to [`update`](Self::update) from any thread, and
208 /// waking the loop if it sleeps; it converts into calloop's `Sender` for an app that keeps
209 /// that type (`let tx: calloop::channel::Sender<_> = sender.into();`). It names no window
210 /// system, so every shell (Wayland, AppKit, the browser) builds the app the same way.
211 ///
212 /// Required since 2026-10-07: the legacy `new(qh, sender)` — whose Wayland queue handle no
213 /// client ever used — and the default that panicked when neither was implemented are gone,
214 /// so an app without a constructor is a compile error rather than a crash at startup.
215 fn create(sender: AppSender<Self::Message>) -> Self;
216 fn settings(&self) -> WindowSettings;
217 /// Return `Some(..)` to run on a wlr-layer-shell surface (overlay/panel)
218 /// instead of an xdg toplevel. Defaults to `None` (a normal window).
219 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
220 fn layer(&self) -> Option<LayerSettings> {
221 None
222 }
223 /// For a [`layer`](Self::layer) app that is usually empty — a
224 /// notification stack — whether there is anything to show right now.
225 /// While this is `false` the Wayland runner destroys the layer surface,
226 /// keeping its renderer detached, and on the turn it turns `true` builds a
227 /// new surface and moves the same renderer onto it — so image ids stay
228 /// good and [`renderer_init`](Self::renderer_init) does not run. An always-mapped transparent overlay is not free: the
229 /// compositor blurs behind it whenever anything under it changes, and it
230 /// keeps a fullscreen client off direct scanout. Asked once a loop turn;
231 /// ignored for xdg windows. Default `true`: always mapped.
232 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
233 fn wants_surface(&self) -> bool {
234 true
235 }
236 /// Declare the window a UTILITY window: a tool whose shape is decided by
237 /// its contents. The compositor then never dictates a size to it (every
238 /// configure is the "you choose" 0x0 — [`WindowSettings::width`]/`height`
239 /// become the surface's own initial size), offers no resize affordance
240 /// (the whole border band moves the window), and never saves geometry
241 /// for it, so a stale remembered size can't be restored over what the
242 /// app asks for. Declared over the cce window-management protocol at
243 /// window creation; on a compositor too old to know the request this is
244 /// silently a plain floating window. Defaults to `false`.
245 fn utility(&self) -> bool {
246 false
247 }
248 /// Declare the window the DESKTOP-GRID layer (zcce set_grid): the
249 /// compositor world-anchors the surface to the virtual desktop and
250 /// pans/zooms it per frame like window content; the app renders only
251 /// when handed a patch (see [`Application::grid_patch`]). The surface
252 /// becomes input-transparent and lives behind all windows. Needs
253 /// manager v6; on an older compositor the declaration is skipped.
254 /// Defaults to `false`.
255 fn grid(&self) -> bool {
256 false
257 }
258 /// A grid patch to render (grid apps only): virtual origin (`x`, `y`),
259 /// virtual size (`w`, `h`), and `scale` surface px per virtual unit.
260 /// Called right before the frame that must show it; the runner has
261 /// already resized the surface to `(w*scale, h*scale)` and acks the
262 /// patch so the coming commit is latched at the new anchor.
263 fn grid_patch(&mut self, _x: f64, _y: f64, _w: f64, _h: f64, _scale: f64) {}
264 fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool);
265 fn tick(&mut self, dt: f32, needs_rebuild: &mut bool);
266 /// How long the runner may sleep between `tick`s while the window is
267 /// idle — nothing to draw, no animation, no key held, no frame callback
268 /// outstanding. `None` (the default) lets it sleep until a Wayland
269 /// event or a message on the app's calloop `Sender` arrives, bounded by
270 /// [`IDLE_DISPATCH`]. Override with `Some` ONLY if your `tick` polls
271 /// something the loop cannot see — a `std::sync::mpsc` receiver drained
272 /// in `tick`, say — because with the default that poll waits for the
273 /// next unrelated event. The better fix is to send through the calloop
274 /// `Sender` handed to `new`, which wakes the loop by itself.
275 fn idle_poll_interval(&self) -> Option<std::time::Duration> {
276 None
277 }
278 /// On-top overlay quads drawn after the display list and its text (e.g. the status bar's
279 /// tray-hover highlights). Deliberately separate from the single paint path.
280 fn overlay_quads(&mut self, _quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: LogicalSize, _scale: f64) {}
281 /// The part of the surface that changed since the last frame this app
282 /// painted, as (x, y, w, h) in logical px, taken (and reset) once per
283 /// rendered frame right after `display_list`. `None` — the default —
284 /// means all of it. Returning a rect makes the frame a partial one: only
285 /// that rect is repainted and only it is reported to the compositor as
286 /// damage, which is what keeps a small change on a very large surface
287 /// (an image dragged across the desktop grid) from costing a full
288 /// repaint on both sides. The app vouches for the rect: anything that
289 /// changed outside it keeps its old pixels. A frame that was skipped is
290 /// the runner's to make up — the next one is painted in full.
291 fn take_damage(&mut self, _size: LogicalSize, _scale: f64) -> Option<(f32, f32, f32, f32)> {
292 None
293 }
294 fn input_regions(&self) -> Option<Vec<(i32, i32, i32, i32)>> {
295 None
296 }
297
298 /// Transparent overflow rim, in logical px, on the RIGHT and BOTTOM of
299 /// the window. Non-zero opts into buffer-larger-than-geometry mode: the
300 /// runner sizes the surface `margin` wider/taller than the configured
301 /// window size, publishes the top-left rect as the xdg window geometry
302 /// (what the compositor tiles, borders, and snaps) and an input region of
303 /// the frame plus any open popover rects — an overhanging menu stays
304 /// clickable while empty rim falls through to whatever is behind.
305 ///
306 /// Right/bottom ONLY, deliberately: the surface grows away from its
307 /// origin, so the frame never moves relative to the surface and pointer
308 /// coordinates stay valid across the resize (a leading rim shifts the
309 /// surface under an unmoved cursor, and the compositor's stale pointer
310 /// state then drops the very next click). Frame coords == surface coords:
311 /// no input translation, no paint shift — the app's only obligation is to
312 /// lay out against the frame (`display_list`'s `size` minus the margin);
313 /// content emitted past the frame edge renders in the rim instead of
314 /// clipping at the buffer edge.
315 ///
316 /// The value may change at runtime (return the popover overhang while a
317 /// menu is open, 0 otherwise): the engine re-derives the surface from the
318 /// stored frame and resizes on drift. Quantize the answer (e.g. 64px
319 /// steps) so an animating popover doesn't resize the surface per frame.
320 /// xdg toplevels only (layer surfaces ignore it).
321 fn overflow_margin(&self) -> u32 {
322 0
323 }
324
325 fn desired_size(&self) -> Option<(u32, u32)> {
326 None
327 }
328
329 /// Publish this app's accessibility tree to screen readers (AT-SPI on Wayland), with
330 /// cce-ui built with its `a11y` feature. Default false while the adapter is proven
331 /// (`docs/rfc-accessibility-locale.md`, phase 2); `CCE_A11Y=1` turns it on for any app.
332 fn publishes_accessibility(&self) -> bool {
333 false
334 }
335
336 /// What the app shows that its [`ui_context`](Self::ui_context) does not: the nodes of
337 /// an accessibility tree, for an app that draws without widgets (a status bar module, a
338 /// terminal, a map) or draws parts of its window itself. Pushed as AccessKit nodes into
339 /// `nodes` (`crate::a11y::AppNodes`); the widgets and an open context menu are added
340 /// around them (`crate::a11y::app_tree`). Default: nothing.
341 fn accessibility(&mut self, nodes: &mut crate::a11y::AppNodes) {
342 let _ = nodes;
343 }
344
345 /// An assistive tool asked something of one of the app's own nodes
346 /// ([`accessibility`](Self::accessibility)), named by the app's own number `n`
347 /// (`AppNodes::id(n)`): a reader's edit of a field the app draws
348 /// (`AppAction::SetText`, for one published with `AppNodes::text_field`), a request
349 /// for the keyboard, a press. Do what the user doing it would, and answer whether
350 /// anything changed (the window is then redrawn and the tree republished). Default:
351 /// nothing.
352 fn accessibility_action(&mut self, n: u64, action: crate::a11y::AppAction) -> bool {
353 let _ = (n, action);
354 false
355 }
356
357 fn ui_context(&self) -> Option<&crate::context::UiContext> {
358 None
359 }
360
361 fn ui_context_mut(&mut self) -> Option<&mut crate::context::UiContext> {
362 None
363 }
364
365 /// Where a widget's open popover is DRAWN, as an offset from the rect it
366 /// reports (`popover_rect`). A widget reports in the coordinates it was
367 /// laid out in; an app that lays its page out unscrolled and shifts what
368 /// it emits draws the popover `scroll` px away from there, and returns
369 /// `(0.0, -scroll_y)` here for the page's widgets. Everything the engine
370 /// derives from a popover rect reads it through this: the text-occlusion
371 /// clamp, the overflow input region, and the region sent to the
372 /// compositor. Without it a menu opened on a scrolled page had the page's
373 /// text drawn over it, and cut a menu-shaped hole in the text one scroll
374 /// offset away.
375 fn popover_offset(&self, _id: crate::widget::WidgetId) -> (f32, f32) {
376 (0.0, 0.0)
377 }
378
379 /// Whether a left-press at (px, py) should start a compositor window drag. Every root
380 /// root plate container is dissolved (Phase 6), so the default is "no" — apps that want
381 /// drag-anywhere override this with `ctx.drag_allowed_at(px, py)`.
382 fn is_movable_root_plate_at(&self, _px: f32, _py: f32) -> bool {
383 false
384 }
385
386 fn clear_color(&self) -> [f32; 4] {
387 [0.0, 0.0, 0.0, 0.0]
388 }
389
390 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
391 fn register_sources(&mut self, _handle: &calloop::LoopHandle<'_, EngineState<Self>>) {}
392
393 fn adjust_size(&self, width: f32, height: f32) -> (f32, f32) {
394 (width, height)
395 }
396
397 /// Mime types this app accepts from a drag, in the app's own preference
398 /// order (the source's order is ignored — a browser lists `text/html`
399 /// before `text/uri-list` and which is more useful is the app's call).
400 /// The default is empty: the app accepts nothing and drags over it read
401 /// as "can't drop here", which is what every client did before drops
402 /// existed. Opting in also requires [`Application::handle_drop`].
403 fn drop_mimes(&self) -> &'static [&'static str] {
404 &[]
405 }
406
407 /// A completed drop: `data` is everything the source wrote for `mime`,
408 /// and `pos` is where it was released in the app's logical coordinates.
409 /// Runs on the main loop, after the transfer finished — this is not the
410 /// place to block, since the compositor is waiting on the next frame.
411 fn handle_drop(
412 &mut self,
413 _mime: &str,
414 _data: &[u8],
415 _pos: LogicalPosition,
416 _needs_rebuild: &mut bool,
417 ) {
418 }
419
420 fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool);
421 fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message>;
422 fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool);
423 /// Trackpad pinch (zwp_pointer_gestures pinch). `factor` is the scale
424 /// change SINCE THE LAST update (1.0 = no change, >1 = fingers spreading),
425 /// so direct-manipulation zoom is `content_scale *= factor`. Return true
426 /// to consume; returning false falls back to the engine's legacy
427 /// synthesis — a ctrl+wheel PixelDelta sized for the graph's zoom mapping
428 /// (`y = (factor-1)/0.015`) — so ctrl-scroll-zoom surfaces keep working
429 /// without implementing this.
430 fn handle_pinch(&mut self, _factor: f32, _pos: LogicalPosition, _needs_rebuild: &mut bool) -> bool {
431 false
432 }
433 fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message>;
434
435 /// Undo, after the focused widget declined the chord (a text box that is
436 /// editing takes it for its own typing). Return true when something was
437 /// undone; false lets the key fall through to `handle_key_input` like any
438 /// other. The chords are `undo` / `redo` in `input.kdl` (cce-ui domain
439 /// defaults `ctrl+z` / `ctrl+shift+z`), resolved once at startup. Build
440 /// the history on `cce_ui::history::History`.
441 fn undo(&mut self, _needs_rebuild: &mut bool) -> bool {
442 false
443 }
444
445 /// Redo — see [`undo`](Self::undo).
446 fn redo(&mut self, _needs_rebuild: &mut bool) -> bool {
447 false
448 }
449
450 /// The toolkit's keyboard navigation in plate terms: Tab and Shift+Tab move
451 /// focus to the next / previous plate or well in reading order
452 /// (`UiContext::focus_step`), a press (Enter / Space) acts on the focused
453 /// plate, a well opens for typing when focused. **On by default** (since
454 /// 2026-10-08, `docs/rfc-accessibility-locale.md` phase 3): the keyboard is
455 /// how a person who cannot use a pointer reaches anything. It needs the
456 /// app's `ui_context_mut`, so an app without a widget tree is untouched
457 /// (Tab reaches its `handle_key_input` as before); a focused widget that
458 /// takes Tab itself keeps it (`WidgetHost::keeps_tab`). An app that routes
459 /// Tab itself (its own field order, a completion popup) returns false.
460 fn plate_navigation(&self) -> bool {
461 true
462 }
463
464 /// Wait for the NEXT compositor when this one goes away, instead of
465 /// exiting. Default false, which is right for any window the compositor
466 /// saves and restores: its successor respawns the app itself, and a
467 /// client that rejoined too came up beside its own copy (see
468 /// [`after_session`]). Return true from a process the compositor does NOT
469 /// restore and that must outlive it — a systemd user service like the
470 /// status bar or the notifier, whose D-Bus names (the tray's
471 /// StatusNotifierWatcher, org.freedesktop.Notifications) other programs
472 /// depend on. Exiting took those names down at every logout and
473 /// compositor restart, and Dropbox, starting into the gap, found no tray.
474 fn outlives_compositor(&self) -> bool {
475 false
476 }
477
478 /// Keyboard focus just moved by the toolkit's Tab traversal. An app that
479 /// caches its geometry until its own rebuild flag (relief carves collected
480 /// in a view pass, widget lists built on layout) raises that flag here, so
481 /// the new ring is drawn; an app that paints fresh every frame needs
482 /// nothing. Default: nothing.
483 fn focus_stepped(&mut self) {}
484 /// Keyboard focus entered/left the window (the compositor keyboard-focuses
485 /// the focused window, so this is the "am I the focused window" signal —
486 /// e.g. for focus-dependent chrome). Default: ignore.
487 fn handle_focus_change(&mut self, _focused: bool, _needs_rebuild: &mut bool) {}
488
489 fn custom_vertices(&mut self, _verts: &mut Vec<Vertex>, _size: LogicalSize, _scale: f64) {}
490
491 /// The frame's geometry, drawn via one batched, GPU-scissor-clipped pass (the single
492 /// paint path). Every rendering app implements this — the legacy `view*` sinks are gone;
493 /// `None` yields an empty frame. Overlays ([`overlay_quads`](Application::overlay_quads))
494 /// and [`custom_vertices`](Application::custom_vertices) still go through their own paths;
495 /// text renders from the list when [`display_list_text`](Application::display_list_text)
496 /// opts in. Receives the frame's logical size and HiDPI scale. Typically implemented as
497 /// `Some(cce_ui::scene::painter::paint_tree(&self.ui_context, &self.root))`.
498 fn display_list(&mut self, _size: LogicalSize, _scale: f64) -> Option<crate::scene::paint::DisplayList> {
499 None
500 }
501
502 /// Opt in to render the display list's `Prim::Text` items through the glyph pass
503 /// (shaped via the shared buffer cache, clipped to the item clip ∩ the prim bounds). An
504 /// app's ENTIRE frame — geometry and text — is then one
505 /// [`display_list`](Application::display_list). Default `false` draws no text (an app that
506 /// only draws geometry, or none at all).
507 ///
508 /// Display-list text gets the same popover-occlusion clamp as the legacy `text_areas`
509 /// mapping (`popover_occlusion_clamp`, driven by `ui_context().active_popovers`), so an
510 /// open popover's plate clips list text beneath it on both paths.
511 fn display_list_text(&self) -> bool {
512 false
513 }
514
515 /// Opt into system fonts in the ENGINE's render `FontSystem` (the one that shapes
516 /// display-list text and rasterizes every glyph at prepare time). Default `false`: the
517 /// render FontSystem loads only the bundled CCE fonts, and text asking for a family that
518 /// exists only among installed system fonts is silently invisible — buffers shaped
519 /// app-side against a system-fonts `FontSystem` carry fontdb face IDs the engine's
520 /// database doesn't have (the cce-colors Phase 6e bug). An app whose UI must render
521 /// arbitrary installed families (the font picker) returns `true`; its own `FontSystem`,
522 /// if it keeps one for measurement, should be `create_font_system_with_system_fonts()`
523 /// so both databases load identically. Consulted once, at GPU init.
524 fn load_system_fonts(&self) -> bool {
525 false
526 }
527
528 /// Called once per renderer, right after it is created and before its
529 /// first frame (a reconnect's replacement too — see "`renderer_init`"
530 /// in CLAUDE.md): create persistent renderer resources here (3D meshes
531 /// via [`VkRenderer::create_mesh`]). Most 2D apps never need this. The
532 /// default hands the renderer to the portable [`init_3d`](Self::init_3d),
533 /// so an app written against that runs here unchanged.
534 #[cfg(not(target_arch = "wasm32"))]
535 fn renderer_init(&mut self, renderer: &mut VkRenderer) {
536 self.init_3d(renderer);
537 }
538
539 /// Direct renderer staging, called every frame after the engine's own text
540 /// prep and immediately before the frame is drawn: stage 3D scene panes
541 /// (`stage_scene`), path-traced panes (`stage_rt`), flush mesh updates, or
542 /// prepare app-shaped text (`prepare_text` — an app that returns `false`
543 /// from [`display_list_text`](Application::display_list_text) fully owns
544 /// the renderer's text state, the engine never touches it). Return `true`
545 /// to request another frame immediately (e.g. while a path tracer is still
546 /// accumulating samples). The default is the portable
547 /// [`stage_3d`](Self::stage_3d).
548 #[cfg(not(target_arch = "wasm32"))]
549 fn stage_renderer(&mut self, renderer: &mut VkRenderer, size: LogicalSize, scale: f64) -> bool {
550 self.stage_3d(renderer, size, scale)
551 }
552
553 /// The portable [`renderer_init`](Self::renderer_init): once per
554 /// renderer, before its first frame, through [`Stage3D`] — the 3D half
555 /// every renderer has, the Vulkan one natively and the WebGPU one in a
556 /// browser. Make the app's meshes here. Called by the browser shell, and
557 /// natively by `renderer_init`'s default.
558 fn init_3d(&mut self, _stage: &mut dyn Stage3D) {}
559
560 /// The portable [`stage_renderer`](Self::stage_renderer): every frame,
561 /// just before it is drawn, stage the scene through [`Stage3D`]. `size`
562 /// is the window's logical size and `scale` its pixel ratio; a scissor is
563 /// physical px. Return `true` to ask for another frame at once.
564 fn stage_3d(&mut self, _stage: &mut dyn Stage3D, _size: LogicalSize, _scale: f64) -> bool {
565 false
566 }
567
568 /// The surface was resized (or the scale factor changed): `width`/`height`
569 /// are the new logical size. The renderer has already been resized; use
570 /// this for stateful relayout that can't wait for the next paint callback.
571 fn handle_resize(&mut self, _width: f32, _height: f32, _scale: f64) {}
572
573 /// Whether the runner's built-in client-side decorations apply: the
574 /// titlebar move band, the movable-root plate drag regions, and — when
575 /// [`csd_resize_borders`](Application::csd_resize_borders) is also on —
576 /// the rect-edge resize grabs and their edge cursors. Return `false` for a
577 /// window whose chrome doesn't follow its rect (e.g. a circular pane) and
578 /// drive moves/resizes yourself via
579 /// [`take_window_action`](Application::take_window_action).
580 fn standard_csd(&self) -> bool {
581 true
582 }
583
584 /// Whether the standard CSD claims the outer 8px of the surface as resize
585 /// grabs (with matching edge cursors). Off by default: under the cce
586 /// compositor the server already provides a resize band just *outside* the
587 /// window, so enabling this gives a window two adjacent 8px gutters driven
588 /// by different code paths — and only the compositor's snaps to the
589 /// desktop grid. It also costs the app clicks, since a press inside the
590 /// band starts a grab and never reaches the widgets underneath.
591 ///
592 /// Turn it on for a window that must be resizable by its own edges under a
593 /// compositor that provides no such affordance. Only consulted when
594 /// [`standard_csd`](Application::standard_csd) is on.
595 fn csd_resize_borders(&self) -> bool {
596 false
597 }
598
599 /// Whether the standard CSD reserves an implicit title-bar strip (`y` in `[8, 32)`) as a
600 /// drag-to-move handle. Opt-in: off by default, so a window has no title bar and is moved
601 /// through the compositor (or via explicitly-declared handles —
602 /// [`is_movable_root_plate_at`](Application::is_movable_root_plate_at)); nothing is
603 /// implicitly draggable. An app with an actual title bar returns `true`. Separate from
604 /// [`standard_csd`](Application::standard_csd), which also gates the resize borders, and
605 /// only consulted when `standard_csd()` is on.
606 fn csd_titlebar_move(&self) -> bool {
607 false
608 }
609
610 /// Override the pointer cursor at (x, y). `None` falls back to the
611 /// runner's standard CSD edge cursors (or `Default` when
612 /// [`standard_csd`](Application::standard_csd) is off).
613 fn cursor_icon(&self, _x: f32, _y: f32) -> Option<CursorIcon> {
614 None
615 }
616
617 /// Polled after each pointer frame is dispatched: return a
618 /// [`WindowAction`] to start an interactive move/resize grab with the
619 /// serial of the most recent pointer press. This is take-semantics — the
620 /// implementation should clear its pending action when returning it.
621 fn take_window_action(&mut self) -> Option<WindowAction> {
622 None
623 }
624
625 /// Called once when the event loop ends (window closed, app-requested
626 /// exit): last-chance work like autosave. The surface is still alive.
627 fn on_exit(&mut self) {}
628 }
629
630
631 #[cfg(all(test, not(any(target_arch = "wasm32", target_os = "macos"))))]
632 mod app_sender_tests {
633 use super::AppSender;
634 use calloop::channel::{channel, Event};
635
636 #[test]
637 fn an_app_sender_delivers_through_the_loop_and_fails_once_it_is_gone() {
638 let (tx, rx) = channel::<u32>();
639 let sender = AppSender::from(tx);
640 let worker = sender.clone();
641 std::thread::spawn(move || worker.send(7).unwrap()).join().unwrap();
642 sender.send(8).unwrap();
643
644 let mut event_loop = calloop::EventLoop::<Vec<u32>>::try_new().unwrap();
645 let token = event_loop
646 .handle()
647 .insert_source(rx, |event, _, got: &mut Vec<u32>| {
648 if let Event::Msg(m) = event {
649 got.push(m);
650 }
651 })
652 .unwrap();
653 let mut got = Vec::new();
654 event_loop.dispatch(Some(std::time::Duration::ZERO), &mut got).unwrap();
655 assert_eq!(got, vec![7, 8]);
656
657 // The loop dropping its end is the app having exited: the message
658 // comes back to the sender rather than vanishing.
659 event_loop.handle().remove(token);
660 assert_eq!(sender.send(9).unwrap_err().0, 9);
661
662 // And the escape hatch for a client still holding calloop's type.
663 let _raw: calloop::channel::Sender<u32> = sender.into();
664 }
665 }