git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/backend/window_runner.rs (292.2K)

   1 use std::time::Instant;
   2 use smithay_client_toolkit::{
   3     compositor::{CompositorHandler, CompositorState},
   4     data_device_manager::DataDeviceManagerState,
   5     delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
   6     delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
   7     delegate_layer,
   8     registry::{ProvidesRegistryState, RegistryState},
   9     output::{OutputHandler, OutputState},
  10     seat::{
  11         keyboard::KeyboardHandler,
  12         pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
  13         Capability, SeatHandler, SeatState,
  14     },
  15     shell::{
  16         xdg::{
  17             window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
  18             XdgShell, XdgSurface as XdgSurfaceExt,
  19         },
  20         wlr_layer::{LayerShell, LayerShellHandler, LayerSurface, LayerSurfaceConfigure},
  21         WaylandSurface,
  22     },
  23     shm::{Shm, ShmHandler},
  24 };
  25 use wayland_client::{
  26     globals::{registry_queue_init, GlobalList},
  27     protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface, wl_registry, wl_region, wl_callback},
  28     Connection, QueueHandle, Proxy,
  29 };
  30 
  31 use wayland_protocols::wp::pointer_gestures::zv1::client::{
  32     zwp_pointer_gesture_pinch_v1::{self, ZwpPointerGesturePinchV1},
  33     zwp_pointer_gestures_v1::{self as zwp_pointer_gestures, ZwpPointerGesturesV1},
  34 };
  35 pub use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel;
  36 pub use smithay_client_toolkit::seat::pointer::CursorIcon as PointerCursorIcon;
  37 use calloop::EventLoop;
  38 use calloop_wayland_source::WaylandSource;
  39 use cosmic_text::{FontSystem, Buffer, Attrs, Metrics};
  40 use crate::widget::{WidgetHost, TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey, Position};
  41 use crate::wayland::detect_scale_factor;
  42 use crate::vk::{Batch2D, Frame2D, TextSpan, VkRenderer};
  43 
  44 #[derive(Hash, PartialEq, Eq, Clone)]
  45 struct BufferCacheKey {
  46     text: String,
  47     size_milli: u32,
  48     font: Option<String>,
  49     is_vertical: bool,
  50     attrs: crate::scene::paint::TextAttrs,
  51 }
  52 
  53 #[derive(Clone)]
  54 struct CachedBuffer {
  55     buffer: Buffer,
  56     last_accessed: std::time::Instant,
  57 }
  58 
  59 std::thread_local! {
  60     static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<BufferCacheKey, CachedBuffer>> = std::cell::RefCell::new(std::collections::HashMap::new());
  61 }
  62 
  63 /// A droplet spec resolved against a concrete rect: the push-constant fields
  64 /// that define its SILHOUETTE, in logical px.
  65 ///
  66 /// Shared by [`crate::scene::paint::Prim::Droplet`] and
  67 /// [`crate::scene::paint::Prim::DropletScrim`] so the lit drop and the vignette
  68 /// drawn inside it can never disagree about the shape — the whole reason the
  69 /// scrim rides the droplet's shader path instead of approximating the outline
  70 /// with a rounded rect.
  71 struct DropletGeom {
  72     hx: f32,
  73     hy: f32,
  74     sag: f32,
  75     br: f32,
  76     bw: f32,
  77     k: f32,
  78     sr: f32,
  79     ar: f32,
  80     band: f32,
  81     bow: f32,
  82     /// How far the contact shadow reaches below/beside the box (0 when the
  83     /// spec has no shadow). The lit drop's cover quad grows by this; a scrim
  84     /// never draws outside the silhouette and ignores it.
  85     sh_reach: f32,
  86 }
  87 
  88 fn droplet_geom(rect: &crate::scene::layout::Rect, spec: &crate::scene::paint::DropletSpec) -> DropletGeom {
  89     let hx = rect.width * 0.5;
  90     let hy = rect.height * 0.5;
  91     let sag = spec.sag.clamp(0.0, 0.9) * rect.height;
  92     // belly <= 0 disables the belly outright (the oval-dewdrop default) — the
  93     // shader skips the smin when the radius is 0.
  94     let (br, bw) = if spec.belly > 0.0 {
  95         let br = (spec.belly.min(1.0) * rect.height).min(hy).min(hx);
  96         (br, ((hx - br).max(0.0) * spec.belly_w.clamp(0.0, 1.0)).max(1.0))
  97     } else {
  98         (0.0, 0.0)
  99     };
 100     let k = (spec.blend.max(0.0) * rect.height).max(1.0);
 101     let sheet_hy = hy - sag * 0.5;
 102     // Bottom (sheet_r) and top (attach) corner radii: when the pair overfills
 103     // the sheet height, scale both down proportionally — 0.5 + 0.5 is the
 104     // fully continuous egg.
 105     let mut sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(hx);
 106     let mut ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(hx);
 107     let sheet_h = (2.0 * sheet_hy).max(0.0);
 108     if sr + ar > sheet_h && sr + ar > 0.0 {
 109         let f = sheet_h / (sr + ar);
 110         sr *= f;
 111         ar *= f;
 112     }
 113     let band = (spec.band.max(0.05) * rect.height).max(1.0);
 114     // Bottom-bow edge rise; the shader derives the arc radius from it per drop
 115     // (R = hx^2/2*rise).
 116     let bow = (spec.bow.clamp(0.0, 0.5) * rect.height).min(hy * 0.9);
 117     let sh_reach = if spec.shadow > 0.0 { (0.18 * rect.height).max(2.0) } else { 0.0 };
 118     DropletGeom { hx, hy, sag, br, bw, k, sr, ar, band, bow, sh_reach }
 119 }
 120 
 121 fn find_cased_family(fs: &FontSystem, name: &str) -> Option<String> {
 122     let lower_name = name.to_lowercase();
 123     for face in fs.db().faces() {
 124         for (family, _) in &face.families {
 125             if family.to_lowercase() == lower_name {
 126                 return Some(family.clone());
 127             }
 128         }
 129     }
 130     None
 131 }
 132 
 133 thread_local! {
 134     /// Family name → is-monospaced, resolved once per family from fontdb's
 135     /// face metadata (the post table's isFixedPitch, as fontdb records it).
 136     static MONO_FAMILY_CACHE: std::cell::RefCell<std::collections::HashMap<String, bool>> =
 137         std::cell::RefCell::new(std::collections::HashMap::new());
 138 }
 139 
 140 fn family_is_monospaced(fs: &FontSystem, name: &str) -> bool {
 141     MONO_FAMILY_CACHE.with(|cache| {
 142         if let Some(&mono) = cache.borrow().get(name) {
 143             return mono;
 144         }
 145         let lower = name.to_lowercase();
 146         let mono = fs
 147             .db()
 148             .faces()
 149             .find(|face| face.families.iter().any(|(f, _)| f.to_lowercase() == lower))
 150             .map(|face| face.monospaced)
 151             .unwrap_or(false);
 152         cache.borrow_mut().insert(name.to_string(), mono);
 153         mono
 154     })
 155 }
 156 
 157 /// The shaping mode for one text run: ASCII-only text in a MONOSPACED face
 158 /// shapes `Basic`, everything else `Advanced`.
 159 ///
 160 /// `Basic` bypasses OpenType substitution and positioning, and for ASCII in a
 161 /// mono face that is exactly right: a mono font's ligatures are the one thing
 162 /// `Advanced` adds there, and they break the grid — Chivo Mono's `liga`
 163 /// squeezes f+i into a single-advance fi glyph, which is why the bar's window
 164 /// titles rendered "file" with a cramped fi — while mono faces carry no
 165 /// kerning to lose. Proportional faces keep `Advanced` (their kerning and
 166 /// ligatures are wanted — a font preview must not misrepresent the face), and
 167 /// any non-ASCII text keeps real shaping (combining marks, emoji, complex
 168 /// scripts) whatever the face.
 169 pub fn shaping_for(fs: &FontSystem, text: &str, family: &cosmic_text::Family) -> cosmic_text::Shaping {
 170     if text.is_ascii() {
 171         if let cosmic_text::Family::Name(name) = family {
 172             if family_is_monospaced(fs, name) {
 173                 return cosmic_text::Shaping::Basic;
 174             }
 175         }
 176     }
 177     cosmic_text::Shaping::Advanced
 178 }
 179 
 180 pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
 181     get_text_buffer_attrs(fs, text, size, font, crate::scene::paint::TextAttrs::default())
 182 }
 183 
 184 /// [`get_text_buffer`] plus shaping attributes (italic / weight) — the backend's shape entry
 185 /// for `Prim::Text` prims that carry [`TextAttrs`] (the font picker's style-variant previews).
 186 pub fn get_text_buffer_attrs(
 187     fs: &mut FontSystem,
 188     text: &str,
 189     size: f32,
 190     font: Option<&str>,
 191     text_attrs: crate::scene::paint::TextAttrs,
 192 ) -> Buffer {
 193     let scale = crate::scale::scale_factor();
 194     let mut font_size = size;
 195     let mut family_name = None;
 196 
 197     if let Some(font_str) = font {
 198         let (parsed_family, parsed_size) = crate::layout::parse_font_string(font_str);
 199         if let Some(ps) = parsed_size {
 200             font_size = ps;
 201         }
 202         family_name = Some(parsed_family);
 203     }
 204 
 205     let physical_size = font_size * scale;
 206     let size_key = (physical_size * 1000.0).round() as u32;
 207     let is_vertical = crate::IS_VERTICAL.load(std::sync::atomic::Ordering::Relaxed);
 208     let key = BufferCacheKey {
 209         text: text.to_string(),
 210         size_milli: size_key,
 211         font: family_name.clone(),
 212         is_vertical,
 213         attrs: text_attrs,
 214     };
 215 
 216     let cached = BUFFER_CACHE.with(|cache| {
 217         let mut cache = cache.borrow_mut();
 218         if let Some(cached_item) = cache.get_mut(&key) {
 219             cached_item.last_accessed = std::time::Instant::now();
 220             Some(cached_item.buffer.clone())
 221         } else {
 222             None
 223         }
 224     });
 225 
 226     if let Some(buf) = cached {
 227         return buf;
 228     }
 229 
 230     let line_height = if is_vertical {
 231         physical_size * 1.05
 232     } else {
 233         physical_size * 1.0
 234     };
 235     let metrics = Metrics::new(physical_size, line_height);
 236     let mut buf = Buffer::new(fs, metrics);
 237     let mut attrs = Attrs::new();
 238 
 239     let (sans_fallback, serif_fallback, mono_fallback, _) = crate::layout::read_preferred_fonts();
 240 
 241     let resolved_storage = family_name.as_deref().and_then(|font_name| match font_name {
 242         "monospace" if !mono_fallback.is_empty() => find_cased_family(fs, &mono_fallback),
 243         "sans-serif" if !sans_fallback.is_empty() => find_cased_family(fs, &sans_fallback),
 244         "serif" if !serif_fallback.is_empty() => find_cased_family(fs, &serif_fallback),
 245         _ => None,
 246     });
 247 
 248     let resolved_sans = if !sans_fallback.is_empty() {
 249         find_cased_family(fs, &sans_fallback)
 250     } else {
 251         None
 252     };
 253 
 254     let family = if let Some(ref font_family) = family_name {
 255         match font_family.as_str() {
 256             "monospace" => {
 257                 if !mono_fallback.is_empty() {
 258                     if let Some(ref cased) = resolved_storage {
 259                         cosmic_text::Family::Name(cased)
 260                     } else {
 261                         cosmic_text::Family::Name(crate::layout::get_system_monospace_font())
 262                     }
 263                 } else {
 264                     cosmic_text::Family::Name(crate::layout::get_system_monospace_font())
 265                 }
 266             }
 267             "sans-serif" => {
 268                 if !sans_fallback.is_empty() {
 269                     if let Some(ref cased) = resolved_storage {
 270                         cosmic_text::Family::Name(cased)
 271                     } else {
 272                         cosmic_text::Family::SansSerif
 273                     }
 274                 } else {
 275                     cosmic_text::Family::SansSerif
 276                 }
 277             }
 278             "serif" => {
 279                 if !serif_fallback.is_empty() {
 280                     if let Some(ref cased) = resolved_storage {
 281                         cosmic_text::Family::Name(cased)
 282                     } else {
 283                         cosmic_text::Family::Serif
 284                     }
 285                 } else {
 286                     cosmic_text::Family::Serif
 287                 }
 288             }
 289             name => cosmic_text::Family::Name(name),
 290         }
 291     } else {
 292         if !sans_fallback.is_empty() {
 293             if let Some(ref cased) = resolved_sans {
 294                 cosmic_text::Family::Name(cased)
 295             } else {
 296                 cosmic_text::Family::SansSerif
 297             }
 298         } else {
 299             cosmic_text::Family::SansSerif
 300         }
 301     };
 302     attrs = attrs.family(family);
 303     if text_attrs.italic {
 304         attrs = attrs.style(cosmic_text::Style::Italic);
 305     }
 306     if let Some(w) = text_attrs.weight {
 307         attrs = attrs.weight(cosmic_text::Weight(w));
 308     }
 309     let shaping = shaping_for(fs, text, &family);
 310     buf.set_text(fs, text, attrs, shaping);
 311     buf.shape_until_scroll(fs, true);
 312 
 313     BUFFER_CACHE.with(|cache| {
 314         let mut cache = cache.borrow_mut();
 315         if cache.len() >= 2000 {
 316             let mut items: Vec<(BufferCacheKey, std::time::Instant)> = cache
 317                 .iter()
 318                 .map(|(k, v)| (k.clone(), v.last_accessed))
 319                 .collect();
 320             items.sort_by_key(|&(_, time)| time);
 321             for (k, _) in items.iter().take(100) {
 322                 cache.remove(k);
 323             }
 324         }
 325         cache.insert(key, CachedBuffer {
 326             buffer: buf.clone(),
 327             last_accessed: std::time::Instant::now(),
 328         });
 329     });
 330 
 331     buf
 332 }
 333 
 334 /// Byte-offset → x mapping of single-line `text`, shaped exactly as the renderer draws it —
 335 /// same buffer cache as the draw, so this is a lookup when the text is already on screen.
 336 /// Returns ascending `(byte_idx, x)` pairs (one per cluster start, logical px, relative to
 337 /// the text origin), terminated by `(text.len(), total_advance)`. This is the correct
 338 /// source for caret placement and click→cursor mapping in hand-rolled text fields:
 339 /// `measure_text_width` reports SVG-rasterized inked extent through fontdb's family
 340 /// resolution, which disagrees with cosmic-text's advance and can even resolve a
 341 /// different face — a caret placed with it drifts off the drawn glyphs.
 342 pub fn shaped_cluster_offsets(
 343     fs: &mut FontSystem,
 344     text: &str,
 345     size: f32,
 346     font: Option<&str>,
 347 ) -> Vec<(usize, f32)> {
 348     let scale = crate::scale::scale_factor().max(1.0);
 349     let buffer = get_text_buffer(fs, text, size, font);
 350     let mut out: Vec<(usize, f32)> = Vec::new();
 351     let mut total: f32 = 0.0;
 352     for (start, x, w) in normalized_glyph_starts(&buffer, text) {
 353         if out.last().map_or(true, |&(b, _)| b != start) {
 354             out.push((start, x / scale));
 355         }
 356         total = total.max((x + w) / scale);
 357     }
 358     out.push((text.len(), total));
 359     out
 360 }
 361 
 362 /// Every glyph of `buffer`'s layout runs as `(start_byte, x, w)` (physical px),
 363 /// with `start` normalized to be text-relative.
 364 ///
 365 /// Exists because cosmic-text 0.12's `Shaping::Basic` path (`shape_skip`) emits
 366 /// `LayoutGlyph::start` relative to the shape SPAN — it resets to 0 at every
 367 /// word — while the Advanced path emits line-relative starts. `shaping_for`
 368 /// picks Basic exactly for ASCII text in a monospace family (the DE's default
 369 /// control font), so any multi-word value hit the bug: offsets keyed by those
 370 /// starts collide on the low columns and the caret/selection walk off the
 371 /// glyphs. A reset can ONLY come from that path, which shapes strictly one
 372 /// glyph per char in logical order — so when one is seen, byte starts are
 373 /// rebuilt by walking the text's chars. `text` must be the single line the
 374 /// buffer was shaped from.
 375 pub(crate) fn normalized_glyph_starts(buffer: &Buffer, text: &str) -> Vec<(usize, f32, f32)> {
 376     let mut glyphs: Vec<(usize, f32, f32)> = Vec::new();
 377     let mut monotonic = true;
 378     let mut prev = 0usize;
 379     for run in buffer.layout_runs() {
 380         for g in run.glyphs {
 381             if g.start < prev {
 382                 monotonic = false;
 383             }
 384             prev = g.start;
 385             glyphs.push((g.start, g.x, g.w));
 386         }
 387     }
 388     if !monotonic {
 389         let mut starts = text.char_indices().map(|(i, _)| i);
 390         for g in glyphs.iter_mut() {
 391             g.0 = starts.next().unwrap_or(text.len());
 392         }
 393     }
 394     glyphs
 395 }
 396 
 397 /// Shape a boxed [`Prim::Text`] (word-wrap + alignment) and return `(buffer, vertical_offset)`.
 398 /// Reuses [`get_text_buffer_attrs`] for all the family resolution — that returns a *clone* of the
 399 /// cached single-run buffer, so re-applying metrics/size/align here does not touch the cache — then
 400 /// re-lays-it-out: a 1.4 line-height (the placed-text convention), the wrap width, per-line
 401 /// horizontal alignment, and re-shapes. The vertical offset positions the shaped block inside the
 402 /// box per `align_v`. Uncached by construction (each box may differ in width/align).
 403 pub fn get_text_buffer_laid_out(
 404     fs: &mut FontSystem,
 405     text: &str,
 406     size: f32,
 407     font: Option<&str>,
 408     text_attrs: crate::scene::paint::TextAttrs,
 409     layout: crate::scene::paint::TextLayout,
 410 ) -> (Buffer, f32) {
 411     use crate::scene::paint::{AlignH, AlignV};
 412     let scale = crate::scale::scale_factor();
 413 
 414     // Resolved family + attrs come for free (a cache clone we are free to mutate).
 415     let mut buf = get_text_buffer_attrs(fs, text, size, font, text_attrs);
 416 
 417     // The font string may override the size ("family:size") — mirror get_text_buffer_attrs.
 418     let mut font_size = size;
 419     if let Some(font_str) = font {
 420         if let (_, Some(ps)) = crate::layout::parse_font_string(font_str) {
 421             font_size = ps;
 422         }
 423     }
 424     let physical_size = font_size * scale;
 425     let line_height = physical_size * 1.4;
 426     buf.set_metrics(fs, Metrics::new(physical_size, line_height));
 427     buf.set_size(fs, layout.wrap_width.map(|w| w * scale), Some(layout.box_height * scale));
 428 
 429     let align = match layout.align_h {
 430         AlignH::Left => cosmic_text::Align::Left,
 431         AlignH::Center => cosmic_text::Align::Center,
 432         AlignH::Right => cosmic_text::Align::Right,
 433     };
 434     for line in &mut buf.lines {
 435         line.set_align(Some(align));
 436     }
 437     buf.shape_until_scroll(fs, true);
 438 
 439     // Vertical offset (logical) from the shaped run count, matching the legacy per-app math.
 440     let runs = buf.layout_runs().count();
 441     let total_h = runs as f32 * font_size * 1.4;
 442     let voff = match layout.align_v {
 443         AlignV::Top => 0.0,
 444         AlignV::Middle => ((layout.box_height - total_h) / 2.0).max(0.0),
 445         AlignV::Bottom => (layout.box_height - total_h).max(0.0),
 446     };
 447     (buf, voff)
 448 }
 449 
 450 /// A text item's clip rect in physical pixels. This was `glyphon::TextBounds` — the one
 451 /// glyphon-owned type cce-ui ever used, everything else being a cosmic-text re-export — so
 452 /// it is defined here now that the dependency is cosmic-text directly. Same plain
 453 /// four-`i32` layout; it is only an intermediate on the way to `TextSpan::bounds`.
 454 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 455 pub struct TextBounds {
 456     pub left: i32,
 457     pub top: i32,
 458     pub right: i32,
 459     pub bottom: i32,
 460 }
 461 
 462 /// The popover-occlusion clamp shared by the default [`Application::text_areas`] mapping and
 463 /// the display-list text path: clip a text item's bounds so it does not bleed through an open
 464 /// popover's plate. A text item whose own bounds coincide with a popover rect IS that popover's
 465 /// text and is left alone; anything else that intersects gets clamped horizontally toward
 466 /// whichever side of the popover it starts on.
 467 /// Clamp a text item's bounds away from the registered popover rects it
 468 /// runs under, so page text does not bleed through a floating plate.
 469 ///
 470 /// A text item BELONGS to a popover when it carries exactly that popover's
 471 /// rect as its bounds (the convention every popover's own labels follow),
 472 /// and it is then clamped only against the popovers registered AFTER its
 473 /// own — `overlay_rects` is in stacking order, the shared context menu
 474 /// last. Before 2026-09-22 a popover's text was exempt from its own rect
 475 /// alone and clamped against every other, so a context menu opened over a
 476 /// modal dialog had its labels clipped by the dialog it was drawn on top
 477 /// of, and showed as a plate with no legible entries.
 478 fn popover_occlusion_clamp(
 479     overlay_rects: &[(f32, f32, f32, f32)],
 480     ti: &TextItem,
 481     scale_f32: f32,
 482     item_bounds: &mut TextBounds,
 483 ) {
 484     let owner = ti.bounds.and_then(|[l, t, r, b]| {
 485         overlay_rects.iter().position(|&(ox, oy, ow, oh)| {
 486             (l - ox).abs() < 1.0
 487                 && (t - oy).abs() < 1.0
 488                 && (r - (ox + ow)).abs() < 1.0
 489                 && (b - (oy + oh)).abs() < 1.0
 490         })
 491     });
 492     let first_above = owner.map_or(0, |k| k + 1);
 493     for &(ox, oy, ow, oh) in &overlay_rects[first_above..] {
 494         let ol = (ox * scale_f32).round() as i32;
 495         let ot = (oy * scale_f32).round() as i32;
 496         let or = ((ox + ow) * scale_f32).round() as i32;
 497         let ob = ((oy + oh) * scale_f32).round() as i32;
 498 
 499         let tx_pixel = ti.x * scale_f32;
 500         let ty_pixel = ti.y * scale_f32;
 501 
 502         let mut text_w = 0.0f32;
 503         let mut run_count = 0;
 504         for run in ti.buffer.layout_runs() {
 505             text_w = text_w.max(run.line_w);
 506             run_count += 1;
 507         }
 508         let text_h = run_count as f32 * ti.buffer.metrics().line_height;
 509 
 510         let actual_left = tx_pixel;
 511         let actual_right = tx_pixel + text_w;
 512         let actual_top = ty_pixel;
 513         let actual_bottom = ty_pixel + text_h;
 514 
 515         if actual_left < or as f32
 516             && actual_right > ol as f32
 517             && actual_top < ob as f32
 518             && actual_bottom > ot as f32
 519         {
 520             if tx_pixel < ol as f32 {
 521                 item_bounds.right = item_bounds.right.min(ol);
 522             } else {
 523                 item_bounds.left = item_bounds.left.max(or);
 524             }
 525         }
 526     }
 527 }
 528 
 529 #[repr(C)]
 530 #[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
 531 pub struct Vertex {
 532     pub position: [f32; 2],
 533     pub color: [f32; 4],
 534     pub clip_circle: [f32; 3], // [cx, cy, r]
 535 }
 536 
 537 pub fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
 538     let x0 = (x / sw) * 2.0 - 1.0;
 539     let y0 = 1.0 - (y / sh) * 2.0;
 540     let x1 = ((x + w) / sw) * 2.0 - 1.0;
 541     let y1 = 1.0 - ((y + h) / sh) * 2.0;
 542     [
 543         Vertex { position: [x0, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
 544         Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
 545         Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
 546         Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
 547         Vertex { position: [x1, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
 548         Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
 549     ]
 550 }
 551 
 552 pub fn quad_vertices_with_clip(
 553     x: f32, y: f32, w: f32, h: f32,
 554     sw: f32, sh: f32,
 555     color: [f32; 4],
 556     clip_circle: [f32; 3],
 557 ) -> [Vertex; 6] {
 558     let x0 = (x / sw) * 2.0 - 1.0;
 559     let y0 = 1.0 - (y / sh) * 2.0;
 560     let x1 = ((x + w) / sw) * 2.0 - 1.0;
 561     let y1 = 1.0 - ((y + h) / sh) * 2.0;
 562     [
 563         Vertex { position: [x0, y0], color, clip_circle },
 564         Vertex { position: [x1, y0], color, clip_circle },
 565         Vertex { position: [x0, y1], color, clip_circle },
 566         Vertex { position: [x1, y0], color, clip_circle },
 567         Vertex { position: [x1, y1], color, clip_circle },
 568         Vertex { position: [x0, y1], color, clip_circle },
 569     ]
 570 }
 571 
 572 /// A quad whose four corners each carry their own color, Gouraud-interpolated across both
 573 /// triangles by the shader (`@location(0) color` has no `flat` qualifier). Corner order is
 574 /// TL, TR, BR, BL. Keep the alpha equal on all four: negative alpha is the blur sentinel,
 575 /// so a gradient that crossed zero would tear the triangle in half.
 576 pub fn quad_vertices_shaded(
 577     x: f32, y: f32, w: f32, h: f32,
 578     sw: f32, sh: f32,
 579     c_tl: [f32; 4], c_tr: [f32; 4], c_br: [f32; 4], c_bl: [f32; 4],
 580     clip_circle: [f32; 3],
 581 ) -> [Vertex; 6] {
 582     let x0 = (x / sw) * 2.0 - 1.0;
 583     let y0 = 1.0 - (y / sh) * 2.0;
 584     let x1 = ((x + w) / sw) * 2.0 - 1.0;
 585     let y1 = 1.0 - ((y + h) / sh) * 2.0;
 586     [
 587         Vertex { position: [x0, y0], color: c_tl, clip_circle },
 588         Vertex { position: [x1, y0], color: c_tr, clip_circle },
 589         Vertex { position: [x0, y1], color: c_bl, clip_circle },
 590         Vertex { position: [x1, y0], color: c_tr, clip_circle },
 591         Vertex { position: [x1, y1], color: c_br, clip_circle },
 592         Vertex { position: [x0, y1], color: c_bl, clip_circle },
 593     ]
 594 }
 595 
 596 pub fn quad_vertices_clipped(
 597     x: f32, y: f32, w: f32, h: f32,
 598     surface_w: f32, surface_h: f32,
 599     color: [f32; 4],
 600     clip: (f32, f32, f32, f32),
 601     clip_circle: [f32; 3],
 602 ) -> Vec<Vertex> {
 603     let (cx0, cy0, cx1, cy1) = clip;
 604     let ix0 = x.max(cx0);
 605     let iy0 = y.max(cy0);
 606     let ix1 = (x + w).min(cx1);
 607     let iy1 = (y + h).min(cy1);
 608     if ix1 <= ix0 || iy1 <= iy0 {
 609         return Vec::new();
 610     }
 611     quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle).to_vec()
 612 }
 613 
 614 pub fn line_vertices(
 615     x1: f32, y1: f32, x2: f32, y2: f32,
 616     thickness: f32,
 617     sw: f32, sh: f32,
 618     c: [f32; 4]
 619 ) -> [Vertex; 6] {
 620     let dx = x2 - x1;
 621     let dy = y2 - y1;
 622     let len = (dx * dx + dy * dy).sqrt();
 623     if len < 0.001 {
 624         return quad_vertices(x1 - thickness/2.0, y1 - thickness/2.0, thickness, thickness, sw, sh, c);
 625     }
 626     let ux = dx / len;
 627     let uy = dy / len;
 628     let nx = -uy;
 629     let ny = ux;
 630     
 631     let half_t = thickness * 0.5;
 632     let p0x = x1 + nx * half_t;
 633     let p0y = y1 + ny * half_t;
 634     let p1x = x1 - nx * half_t;
 635     let p1y = y1 - ny * half_t;
 636     let p2x = x2 - nx * half_t;
 637     let p2y = y2 - ny * half_t;
 638     let p3x = x2 + nx * half_t;
 639     let p3y = y2 + ny * half_t;
 640 
 641     let ndc_p0x = (p0x / sw) * 2.0 - 1.0;
 642     let ndc_p0y = 1.0 - (p0y / sh) * 2.0;
 643     let ndc_p1x = (p1x / sw) * 2.0 - 1.0;
 644     let ndc_p1y = 1.0 - (p1y / sh) * 2.0;
 645     let ndc_p2x = (p2x / sw) * 2.0 - 1.0;
 646     let ndc_p2y = 1.0 - (p2y / sh) * 2.0;
 647     let ndc_p3x = (p3x / sw) * 2.0 - 1.0;
 648     let ndc_p3y = 1.0 - (p3y / sh) * 2.0;
 649 
 650     let clip_circle = [0.0, 0.0, 0.0];
 651     [
 652         Vertex { position: [ndc_p0x, ndc_p0y], color: c, clip_circle },
 653         Vertex { position: [ndc_p1x, ndc_p1y], color: c, clip_circle },
 654         Vertex { position: [ndc_p2x, ndc_p2y], color: c, clip_circle },
 655         Vertex { position: [ndc_p0x, ndc_p0y], color: c, clip_circle },
 656         Vertex { position: [ndc_p2x, ndc_p2y], color: c, clip_circle },
 657         Vertex { position: [ndc_p3x, ndc_p3y], color: c, clip_circle },
 658     ]
 659 }
 660 
 661 #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
 662 pub enum LineCap {
 663     Arrow,
 664     Round,
 665     Flat,
 666 }
 667 
 668 pub fn vector_vertices(
 669     x1: f32, y1: f32, x2: f32, y2: f32,
 670     thickness: f32,
 671     sw: f32, sh: f32,
 672     c: [f32; 4],
 673     line_cap: LineCap,
 674 ) -> Vec<Vertex> {
 675     let mut verts = Vec::new();
 676     let dx = x2 - x1;
 677     let dy = y2 - y1;
 678     let len = (dx * dx + dy * dy).sqrt();
 679     if len < 0.001 {
 680         return quad_vertices(x1 - thickness/2.0, y1 - thickness/2.0, thickness, thickness, sw, sh, c).to_vec();
 681     }
 682     
 683     match line_cap {
 684         LineCap::Arrow => {
 685             let ux = dx / len;
 686             let uy = dy / len;
 687             let nx = -uy;
 688             let ny = ux;
 689             
 690             let arrow_len = (thickness * 3.0).max(10.0).min(len);
 691             let arrow_width = (thickness * 2.5).max(8.0);
 692             
 693             let line_x2 = x2 - ux * arrow_len;
 694             let line_y2 = y2 - uy * arrow_len;
 695             
 696             if len > arrow_len {
 697                 verts.extend_from_slice(&line_vertices(x1, y1, line_x2, line_y2, thickness, sw, sh, c));
 698             }
 699             
 700             let bx = line_x2;
 701             let by = line_y2;
 702             
 703             let w1x = bx + nx * (arrow_width * 0.5);
 704             let w1y = by + ny * (arrow_width * 0.5);
 705             let w2x = bx - nx * (arrow_width * 0.5);
 706             let w2y = by - ny * (arrow_width * 0.5);
 707             
 708             let ndc_tip_x = (x2 / sw) * 2.0 - 1.0;
 709             let ndc_tip_y = 1.0 - (y2 / sh) * 2.0;
 710             let ndc_w1x = (w1x / sw) * 2.0 - 1.0;
 711             let ndc_w1y = 1.0 - (w1y / sh) * 2.0;
 712             let ndc_w2x = (w2x / sw) * 2.0 - 1.0;
 713             let ndc_w2y = 1.0 - (w2y / sh) * 2.0;
 714             
 715             let clip_circle = [0.0, 0.0, 0.0];
 716             verts.push(Vertex { position: [ndc_tip_x, ndc_tip_y], color: c, clip_circle });
 717             verts.push(Vertex { position: [ndc_w1x, ndc_w1y], color: c, clip_circle });
 718             verts.push(Vertex { position: [ndc_w2x, ndc_w2y], color: c, clip_circle });
 719         }
 720         LineCap::Round => {
 721             push_feathered_line_vertices(x1, y1, x2, y2, thickness, sw, sh, c, &mut verts);
 722             let clip_circle = [0.0, 0.0, 0.0];
 723             verts.extend(circle_vertices(x2, y2, thickness / 2.0, sw, sh, c, 16, clip_circle));
 724         }
 725         LineCap::Flat => {
 726             push_feathered_line_vertices(x1, y1, x2, y2, thickness, sw, sh, c, &mut verts);
 727         }
 728     }
 729 
 730     verts
 731 }
 732 
 733 /// `line_vertices` with a half-px alpha ramp along each long edge (the arc
 734 /// tessellator's poor-man's AA) — diagonal strokes resolve smoothly instead of
 735 /// stair-stepping. Axis-aligned strokes keep the crisp single-quad path:
 736 /// feathering a pixel-snapped hairline would only blur it.
 737 fn push_feathered_line_vertices(
 738     x1: f32, y1: f32, x2: f32, y2: f32,
 739     thickness: f32,
 740     sw: f32, sh: f32,
 741     c: [f32; 4],
 742     out: &mut Vec<Vertex>,
 743 ) {
 744     let dx = x2 - x1;
 745     let dy = y2 - y1;
 746     let len = (dx * dx + dy * dy).sqrt();
 747     if len < 0.001 || dx.abs() < 0.01 || dy.abs() < 0.01 {
 748         out.extend_from_slice(&line_vertices(x1, y1, x2, y2, thickness, sw, sh, c));
 749         return;
 750     }
 751     let (nx, ny) = (-dy / len, dx / len);
 752     let f = 0.5f32.min(thickness * 0.25);
 753     let half = thickness * 0.5;
 754     // (offset at band start, offset at band end, alpha at start, alpha at end)
 755     let bands = [
 756         (-half - f, -half + f, 0.0, c[3]),
 757         (-half + f, half - f, c[3], c[3]),
 758         (half - f, half + f, c[3], 0.0),
 759     ];
 760     for &(oa, ob, aa, ab) in &bands {
 761         let ca = [c[0], c[1], c[2], aa];
 762         let cb = [c[0], c[1], c[2], ab];
 763         let p = |x: f32, y: f32, o: f32| -> [f32; 2] {
 764             [((x + nx * o) / sw) * 2.0 - 1.0, 1.0 - ((y + ny * o) / sh) * 2.0]
 765         };
 766         let clip_circle = [0.0, 0.0, 0.0];
 767         let (a1, b1) = (p(x1, y1, oa), p(x1, y1, ob));
 768         let (a2, b2) = (p(x2, y2, oa), p(x2, y2, ob));
 769         out.push(Vertex { position: a1, color: ca, clip_circle });
 770         out.push(Vertex { position: b1, color: cb, clip_circle });
 771         out.push(Vertex { position: b2, color: cb, clip_circle });
 772         out.push(Vertex { position: a1, color: ca, clip_circle });
 773         out.push(Vertex { position: b2, color: cb, clip_circle });
 774         out.push(Vertex { position: a2, color: ca, clip_circle });
 775     }
 776 }
 777 
 778 pub fn rounded_rect_vertices_corners(
 779     x: f32, y: f32, ww: f32, h: f32,
 780     r: f32,
 781     sw: f32, sh: f32,
 782     color: [f32; 4],
 783     clip_circle: [f32; 3],
 784     corners: (bool, bool, bool, bool),
 785     clip_rect: Option<(f32, f32, f32, f32)>,
 786 ) -> Vec<Vertex> {
 787     let mut verts = Vec::new();
 788     let radii = crate::widget::CornerRadii::new(
 789         if corners.0 { r } else { 0.0 },
 790         if corners.1 { r } else { 0.0 },
 791         if corners.2 { r } else { 0.0 },
 792         if corners.3 { r } else { 0.0 },
 793     );
 794     push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, color, clip_circle, clip_rect, &mut verts);
 795     verts
 796 }
 797 
 798 /// Sample of the unit superellipse |x|^n + |y|^n = 1 at circle parameter θ —
 799 /// the (cos θ, sin θ) replacement the corner fans use. Exactly the circle at
 800 /// n = 2; higher `corner_shape` exponents give the DE's continuous-curvature
 801 /// corners, so widget silhouettes follow the same corner family as the
 802 /// SDF-lit plates. `e` is 2/n, hoisted by callers. Tangent points at the
 803 /// quadrant ends are unchanged, so fans still tile exactly against the body
 804 /// rects and edge strips.
 805 #[inline]
 806 fn superellipse_pt(theta: f32, e: f32) -> (f32, f32) {
 807     let (s, c) = theta.sin_cos();
 808     (c.signum() * c.abs().powf(e), s.signum() * s.abs().powf(e))
 809 }
 810 
 811 /// Feathered glow ([`Prim::Glow`]): the rounded rect's interior fills at the
 812 /// color's alpha and concentric outline rings fade it to zero across `reach`
 813 /// px outside the boundary. Alpha rides the VERTICES, so the GPU interpolates
 814 /// a per-pixel-smooth falloff between rings — stacked translucent layers band
 815 /// visibly; this cannot. Ring alphas sit on a quadratic ease-out, giving the
 816 /// vignette profile piecewise-linearly with kinks below visibility at glow
 817 /// alphas. Corners sample [`superellipse_pt`], so a glow's silhouette sits in
 818 /// the same corner family as the cells, nodes, and plates it highlights.
 819 pub fn push_glow_vertices(
 820     x: f32, y: f32, ww: f32, h: f32,
 821     radius: f32, reach: f32,
 822     sw: f32, sh: f32,
 823     color: [f32; 4],
 824     clip_circle: [f32; 3],
 825     out: &mut Vec<Vertex>,
 826 ) {
 827     if ww <= 0.0 || h <= 0.0 || color[3].abs() <= 0.0005 || sw <= 0.0 || sh <= 0.0 {
 828         return;
 829     }
 830     let r0 = radius.clamp(0.0, ww.min(h) * 0.5);
 831     let ctl = (x + r0, y + r0);
 832     let ctr = (x + ww - r0, y + r0);
 833     let cbr = (x + ww - r0, y + h - r0);
 834     let cbl = (x + r0, y + h - r0);
 835     const K: usize = 10;
 836     use std::f32::consts::PI;
 837     let corner_e = 2.0 / crate::layout::corner_shape();
 838     // One outline ring `off` px outside the boundary, clockwise from the
 839     // top-left arc; every ring shares the layout, so strips never twist.
 840     let ring = |off: f32| -> Vec<[f32; 2]> {
 841         let r = (r0 + off).max(0.0);
 842         let mut pts = Vec::with_capacity(4 * (K + 1));
 843         let corners = [
 844             (ctl, PI, 1.5 * PI),
 845             (ctr, 1.5 * PI, 2.0 * PI),
 846             (cbr, 0.0, 0.5 * PI),
 847             (cbl, 0.5 * PI, PI),
 848         ];
 849         for ((cx, cy), a0, a1) in corners {
 850             for k in 0..=K {
 851                 let a = a0 + (a1 - a0) * (k as f32 / K as f32);
 852                 let (ux, uy) = superellipse_pt(a, corner_e);
 853                 pts.push([cx + r * ux, cy + r * uy]);
 854             }
 855         }
 856         pts
 857     };
 858     let to_v = |p: [f32; 2], a: f32| Vertex {
 859         position: [(p[0] / sw) * 2.0 - 1.0, 1.0 - (p[1] / sh) * 2.0],
 860         color: [color[0], color[1], color[2], a],
 861         clip_circle,
 862     };
 863 
 864     let rings: Vec<(Vec<[f32; 2]>, f32)> = [0.0f32, 0.35, 0.7, 1.0]
 865         .iter()
 866         .map(|&t| (ring(reach * t), color[3] * (1.0 - t) * (1.0 - t)))
 867         .collect();
 868     let n = rings[0].0.len();
 869 
 870     // Interior: a fan from the rect center over the innermost ring (a rounded
 871     // rect is convex, so the fan covers it exactly), uniform core alpha.
 872     let center = [x + ww * 0.5, y + h * 0.5];
 873     for i in 0..n {
 874         let p1 = rings[0].0[i];
 875         let p2 = rings[0].0[(i + 1) % n];
 876         out.push(to_v(center, color[3]));
 877         out.push(to_v(p1, color[3]));
 878         out.push(to_v(p2, color[3]));
 879     }
 880     // The feather: strips between consecutive rings, each vertex carrying its
 881     // ring's alpha.
 882     for w in rings.windows(2) {
 883         let (inner, ia) = (&w[0].0, w[0].1);
 884         let (outer, oa) = (&w[1].0, w[1].1);
 885         for i in 0..n {
 886             let a1 = inner[i];
 887             let a2 = inner[(i + 1) % n];
 888             let b1 = outer[i];
 889             let b2 = outer[(i + 1) % n];
 890             out.push(to_v(a1, ia));
 891             out.push(to_v(b1, oa));
 892             out.push(to_v(a2, ia));
 893             out.push(to_v(a2, ia));
 894             out.push(to_v(b1, oa));
 895             out.push(to_v(b2, oa));
 896         }
 897     }
 898 }
 899 
 900 pub fn push_rounded_rect_vertices_corners(
 901     x: f32, y: f32, ww: f32, h: f32,
 902     radii: crate::widget::CornerRadii,
 903     sw: f32, sh: f32,
 904     color: [f32; 4],
 905     clip_circle: [f32; 3],
 906     clip_rect: Option<(f32, f32, f32, f32)>,
 907     out: &mut Vec<Vertex>,
 908 ) {
 909     let corner_e = 2.0 / crate::layout::corner_shape();
 910     let mut r_tl = radii.top_left.max(0.0);
 911     let mut r_tr = radii.top_right.max(0.0);
 912     let mut r_br = radii.bottom_right.max(0.0);
 913     let mut r_bl = radii.bottom_left.max(0.0);
 914 
 915     // Simple scale clamping
 916     let sum_top = r_tl + r_tr;
 917     if sum_top > ww {
 918         let f = ww / sum_top;
 919         r_tl *= f;
 920         r_tr *= f;
 921     }
 922     let sum_bottom = r_bl + r_br;
 923     if sum_bottom > ww {
 924         let f = ww / sum_bottom;
 925         r_bl *= f;
 926         r_br *= f;
 927     }
 928     let sum_left = r_tl + r_bl;
 929     if sum_left > h {
 930         let f = h / sum_left;
 931         r_tl *= f;
 932         r_bl *= f;
 933     }
 934     let sum_right = r_tr + r_br;
 935     if sum_right > h {
 936         let f = h / sum_right;
 937         r_tr *= f;
 938         r_br *= f;
 939     }
 940 
 941     let clamp_x = |val: f32| -> f32 {
 942         if let Some((cx0, _, cx1, _)) = clip_rect {
 943             val.max(cx0).min(cx1)
 944         } else {
 945             val
 946         }
 947     };
 948     let clamp_y = |val: f32| -> f32 {
 949         if let Some((_, cy0, _, cy1)) = clip_rect {
 950             val.max(cy0).min(cy1)
 951         } else {
 952             val
 953         }
 954     };
 955 
 956     let push_quad = |verts: &mut Vec<Vertex>, qx: f32, qy: f32, qw: f32, qh: f32| {
 957         let x0 = clamp_x(qx);
 958         let y0 = clamp_y(qy);
 959         let x1 = clamp_x(qx + qw);
 960         let y1 = clamp_y(qy + qh);
 961         
 962         if x1 <= x0 || y1 <= y0 {
 963             return;
 964         }
 965 
 966         let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
 967         let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
 968         let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
 969         let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
 970         
 971         verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
 972         verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
 973         verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
 974         verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
 975         verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
 976         verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
 977     };
 978 
 979     let has_corners = r_tl > 0.1 || r_tr > 0.1 || r_br > 0.1 || r_bl > 0.1;
 980     if !has_corners {
 981         push_quad(out, x, y, ww, h);
 982         return;
 983     }
 984 
 985     // Body rectangles
 986     let mid_x0 = r_tl.max(r_bl);
 987     let mid_x1 = ww - r_tr.max(r_br);
 988     if mid_x1 > mid_x0 {
 989         push_quad(out, x + mid_x0, y, mid_x1 - mid_x0, h);
 990     }
 991     if h > r_tl + r_bl {
 992         push_quad(out, x, y + r_tl, mid_x0, h - r_tl - r_bl);
 993     }
 994     if h > r_tr + r_br {
 995         push_quad(out, x + mid_x1, y + r_tr, ww - mid_x1, h - r_tr - r_br);
 996     }
 997 
 998     // Corner rendering. The fans are FEATHERED: the fan body stops half a
 999     // pixel short of the silhouette and a strip fades from opaque at
1000     // silhouette-0.5 to transparent at silhouette+0.5, so the arc
1001     // anti-aliases instead of rasterizing a hard staircase — invisible on
1002     // HiDPI widget buffers, glaring on the desktop grid's world-scale
1003     // cells. Perceived size is unchanged (the 50%-coverage line stays on
1004     // the exact silhouette). Radii too small to feather keep the hard fan.
1005     let segments = 16;
1006     let fade = [color[0], color[1], color[2], 0.0];
1007     let to_ndc = |px: f32, py: f32| -> [f32; 2] {
1008         [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0]
1009     };
1010     let push_corner = |out: &mut Vec<Vertex>, cx: f32, cy: f32, r: f32, start: f32, end: f32| {
1011         let feather = r > 1.5;
1012         let r_fan = if feather { r - 0.5 } else { r };
1013         let r_out = r + 0.5;
1014         for i in 0..segments {
1015             let theta1 = start + (i as f32) * (end - start) / (segments as f32);
1016             let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
1017 
1018             let (c1, s1) = superellipse_pt(theta1, corner_e);
1019             let (c2, s2) = superellipse_pt(theta2, corner_e);
1020             let p0 = to_ndc(clamp_x(cx), clamp_y(cy));
1021             let p1 = to_ndc(clamp_x(cx + r_fan * c1), clamp_y(cy + r_fan * s1));
1022             let p2 = to_ndc(clamp_x(cx + r_fan * c2), clamp_y(cy + r_fan * s2));
1023 
1024             out.push(Vertex { position: p0, color, clip_circle });
1025             out.push(Vertex { position: p1, color, clip_circle });
1026             out.push(Vertex { position: p2, color, clip_circle });
1027 
1028             if feather {
1029                 let q1 = to_ndc(clamp_x(cx + r_out * c1), clamp_y(cy + r_out * s1));
1030                 let q2 = to_ndc(clamp_x(cx + r_out * c2), clamp_y(cy + r_out * s2));
1031                 out.push(Vertex { position: p1, color, clip_circle });
1032                 out.push(Vertex { position: q1, color: fade, clip_circle });
1033                 out.push(Vertex { position: q2, color: fade, clip_circle });
1034                 out.push(Vertex { position: p1, color, clip_circle });
1035                 out.push(Vertex { position: q2, color: fade, clip_circle });
1036                 out.push(Vertex { position: p2, color, clip_circle });
1037             }
1038         }
1039     };
1040 
1041     // Top-Left
1042     if r_tl > 0.1 {
1043         push_corner(out, x + r_tl, y + r_tl, r_tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI);
1044         if mid_x0 > r_tl {
1045             push_quad(out, x + r_tl, y, mid_x0 - r_tl, r_tl);
1046         }
1047     }
1048 
1049     // Top-Right
1050     if r_tr > 0.1 {
1051         push_corner(out, x + ww - r_tr, y + r_tr, r_tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI);
1052         if ww - mid_x1 > r_tr {
1053             push_quad(out, x + mid_x1, y, ww - mid_x1 - r_tr, r_tr);
1054         }
1055     }
1056 
1057     // Bottom-Right
1058     if r_br > 0.1 {
1059         push_corner(out, x + ww - r_br, y + h - r_br, r_br, 0.0, 0.5 * std::f32::consts::PI);
1060         if ww - mid_x1 > r_br {
1061             push_quad(out, x + mid_x1, y + h - r_br, ww - mid_x1 - r_br, r_br);
1062         }
1063     }
1064 
1065     // Bottom-Left
1066     if r_bl > 0.1 {
1067         push_corner(out, x + r_bl, y + h - r_bl, r_bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI);
1068         if mid_x0 > r_bl {
1069             push_quad(out, x + r_bl, y + h - r_bl, mid_x0 - r_bl, r_bl);
1070         }
1071     }
1072 }
1073 
1074 pub fn rounded_rect_vertices(
1075     x: f32, y: f32, ww: f32, h: f32,
1076     r: f32,
1077     sw: f32, sh: f32,
1078     color: [f32; 4],
1079     clip_circle: [f32; 3],
1080 ) -> Vec<Vertex> {
1081     let mut verts = Vec::new();
1082     push_rounded_rect_vertices_corners(x, y, ww, h, crate::widget::CornerRadii::uniform(r), sw, sh, color, clip_circle, None, &mut verts);
1083     verts
1084 }
1085 
1086 pub fn push_rounded_rect_vertices(
1087     x: f32, y: f32, ww: f32, h: f32,
1088     r: f32,
1089     sw: f32, sh: f32,
1090     color: [f32; 4],
1091     clip_circle: [f32; 3],
1092     out: &mut Vec<Vertex>,
1093 ) {
1094     push_rounded_rect_vertices_corners(x, y, ww, h, crate::widget::CornerRadii::uniform(r), sw, sh, color, clip_circle, None, out);
1095 }
1096 
1097 pub fn plate_bevel_vertices(
1098     x: f32, y: f32, ww: f32, h: f32,
1099     r: f32,
1100     t: f32,
1101     sw: f32, sh: f32,
1102     base_color: [f32; 4],
1103     clip_circle: [f32; 3],
1104 ) -> Vec<Vertex> {
1105     let mut verts = Vec::new();
1106     push_plate_bevel_vertices(x, y, ww, h, r, t, sw, sh, base_color, clip_circle, &mut verts);
1107     verts
1108 }
1109 
1110 pub fn push_plate_bevel_vertices(
1111     x: f32, y: f32, ww: f32, h: f32,
1112     r: f32,
1113     t: f32,
1114     sw: f32, sh: f32,
1115     base_color: [f32; 4],
1116     clip_circle: [f32; 3],
1117     out: &mut Vec<Vertex>,
1118 ) {
1119     push_bevel_edge_vertices(x, y, ww, h, r, t, sw, sh, base_color, clip_circle, 1.0, out);
1120 }
1121 
1122 /// The bevel edge shading, with the light direction selectable: `light_sign` is `1.0`
1123 /// for a raised plate (edges facing `light_source_position` are lit) and `-1.0` for a
1124 /// recess (those same edges fall into shadow instead, and the far edges catch the
1125 /// light). Negating the whole light vector flips every edge and every corner segment
1126 /// consistently, because both the flat-edge factors and the arc-normal dot product
1127 /// below are linear in it.
1128 pub fn push_bevel_edge_vertices(
1129     x: f32, y: f32, ww: f32, h: f32,
1130     r: f32,
1131     t: f32,
1132     sw: f32, sh: f32,
1133     base_color: [f32; 4],
1134     clip_circle: [f32; 3],
1135     light_sign: f32,
1136     out: &mut Vec<Vertex>,
1137 ) {
1138     push_bevel_edge_vertices_radii(
1139         x, y, ww, h, (r, r, r, r), t, sw, sh, base_color, clip_circle, light_sign, out,
1140     );
1141 }
1142 
1143 /// As [`push_bevel_edge_vertices`], but with a per-corner radius (TL, TR, BR, BL) so the
1144 /// lip can follow a shape whose corners differ — a recess carved along the top of a
1145 /// rounded plate needs the plate's radius on its top corners and square ones where it
1146 /// meets the content below. A uniform radius there would either square off the plate's
1147 /// arc (painting a notch outside it) or wrongly round the inner corners.
1148 pub fn push_bevel_edge_vertices_radii(
1149     x: f32, y: f32, ww: f32, h: f32,
1150     radii: (f32, f32, f32, f32),
1151     t: f32,
1152     sw: f32, sh: f32,
1153     base_color: [f32; 4],
1154     clip_circle: [f32; 3],
1155     light_sign: f32,
1156     out: &mut Vec<Vertex>,
1157 ) {
1158     push_bevel_edge_vertices_banded(
1159         x, y, ww, h, radii, t, sw, sh, base_color, clip_circle, light_sign,
1160         default_bevel_bands(t), (true, true, true, true), EdgeKind::Rim, out,
1161     );
1162 }
1163 
1164 /// What kind of height change an edge represents. The two shade differently because they
1165 /// are different shapes, and using one where the other belongs is what makes a bevel read
1166 /// as a drawn line instead of a surface.
1167 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1168 pub enum EdgeKind {
1169     /// The surface *ends* here: a quarter-round rolling from face-on at the inner edge of
1170     /// the lip to fully in-plane at the outer boundary, where it drops away. The shading
1171     /// therefore peaks exactly at the boundary and dies inward. This is a plate's outer
1172     /// perimeter.
1173     Rim,
1174     /// The surface *continues* at a different height: one plateau steps down to another.
1175     /// A height field that falls monotonically across the transition has its normal tilted
1176     /// toward the low side the whole way, steepest in the middle and flat at both ends —
1177     /// so the shading is a bump straddling the boundary, not a band butted against it.
1178     /// Hanging the band on one side instead leaves the seam the eye reads as a drawn line.
1179     Step,
1180 }
1181 
1182 /// Shading across an edge at signed distance `d` from the boundary (positive = toward the
1183 /// shape's interior), for a transition of width `t`. Returns the light term as a fraction
1184 /// of full tilt.
1185 #[inline]
1186 fn bevel_profile(kind: EdgeKind, d: f32, t: f32) -> f32 {
1187     if t <= 0.0 {
1188         return 0.0;
1189     }
1190     match kind {
1191         // Normal rotates from in-plane (d = 0) to face-on (d = t): sine of what tilt is
1192         // left. A linear ramp here reads as a flat 45° chamfer instead of a roll.
1193         EdgeKind::Rim => ((1.0 - (d / t).clamp(0.0, 1.0)) * std::f32::consts::FRAC_PI_2).sin(),
1194         // Symmetric bump over [-t/2, +t/2], zero at both ends so the transition blends into
1195         // both plateaus with no seam.
1196         EdgeKind::Step => {
1197             let s = (d / t + 0.5).clamp(0.0, 1.0);
1198             (s * std::f32::consts::PI).sin()
1199         }
1200     }
1201 }
1202 
1203 /// The light-independent curvature term at signed distance `d` — the second depth cue,
1204 /// on top of the directional one. Curvature shading is what ambient light does: convex
1205 /// surface catches it from everywhere (bright), concave is self-occluded (dark). Because
1206 /// it does not rotate with the light, it survives exactly where the directional term
1207 /// dies — walls parallel to the light vector — so no edge ever vanishes entirely.
1208 ///
1209 /// `high_sign` is +1 when the rect interior is the HIGH side of the transition and -1
1210 /// when it is the low side (a recess). Geometry, not lighting: it does not flip with
1211 /// `light_sign`... except that for these 2.5D shapes the two are the same number, since
1212 /// a raised shape is lit like a plateau and shaded like one.
1213 #[inline]
1214 fn bevel_curvature(kind: EdgeKind, d: f32, t: f32, high_sign: f32) -> f32 {
1215     if t <= 0.0 {
1216         return 0.0;
1217     }
1218     match kind {
1219         // A rim is convex everywhere, tightest right at the silhouette: a bright crest
1220         // line hugging the boundary and dying fast inward. This is the line that makes
1221         // glass read as glass — the edge catches ambient light all the way around, even
1222         // (dimmer, via the gain asymmetry below) on the side facing away from the light.
1223         EdgeKind::Rim => {
1224             let u = (d / t).clamp(0.0, 1.0);
1225             let f = 1.0 - u;
1226             CREST_RATIO * f * f * f
1227         }
1228         // An S-curve step is convex on its high half (the shoulder) and concave on its
1229         // low half (the fillet, where the wall meets the floor): antisymmetric, zero at
1230         // the ends (no seam against either plateau) and at the midpoint.
1231         EdgeKind::Step => {
1232             let s = (d / t + 0.5).clamp(0.0, 1.0);
1233             let outer_is_high = -high_sign; // d < 0 is outside the rect
1234             // sin(2πs) is positive on the outer half — the shoulder when the outside is
1235             // the high side — and negative on the inner (fillet) half.
1236             AO_RATIO * outer_is_high * (s * std::f32::consts::TAU).sin()
1237         }
1238     }
1239 }
1240 
1241 /// Crest amplitude as a fraction of `bevel_depth` — how much brighter a rim's silhouette
1242 /// line is than flat surface under even light. Must stay clearly below ~0.7 (the
1243 /// projection of a 135° light onto an axis edge), or it cancels the directional shadow
1244 /// on the dark side and the rim goes flat there instead of showing a faint bright line
1245 /// over a shadowed roll.
1246 const CREST_RATIO: f32 = 0.4;
1247 /// Shoulder/fillet amplitude as a fraction of `bevel_depth`.
1248 const AO_RATIO: f32 = 0.6;
1249 /// Per-sign overlay gains. These are asymmetric the opposite way from intuition: on the
1250 /// dark bases this DE runs, white-over blending (`b + a(1-b)`) moves the pixel far more
1251 /// per unit alpha than black-over (`b(1-a)`) — a dark surface has little brightness for
1252 /// black to take away. The old subtractive shading effectively crushed shadow sides to
1253 /// black in linear space; the black overlay needs a high gain to keep shadows reading
1254 /// at all, while white needs damping to keep highlights from blowing out.
1255 const LIGHT_GAIN: f32 = 0.7;
1256 const DARK_GAIN: f32 = 3.0;
1257 
1258 /// A shading value (already scaled by `bevel_depth`) as the two overlay passes: the lit
1259 /// pass is translucent white, the shadow pass translucent black. Painting the
1260 /// *modulation* instead of a resolved surface color is what lets relief primitives compose — a step
1261 /// crossing a rim shades the rim's gradient instead of stamping a flat band over it, a
1262 /// lip on a translucent plate no longer doubles its opacity, and a recess needs no
1263 /// knowledge of the surface color it carves.
1264 ///
1265 /// Why two passes with fixed RGB rather than one signed color: a primitive whose value
1266 /// crosses zero inside a band would interpolate white→black through mid-gray at
1267 /// non-negligible alpha — on a dark base a *brightening* artifact right where the
1268 /// shading should vanish. With per-pass alphas clamped at the crossing, each pass fades
1269 /// to zero there and the hue can never be wrong. Alphas also stay non-negative on every
1270 /// vertex, which the renderer requires (negative alpha is the blur sentinel).
1271 #[inline]
1272 fn overlay_light(v: f32) -> [f32; 4] {
1273     [1.0, 1.0, 1.0, (v.max(0.0) * LIGHT_GAIN).min(1.0)]
1274 }
1275 #[inline]
1276 fn overlay_dark(v: f32) -> [f32; 4] {
1277     [0.0, 0.0, 0.0, ((-v).max(0.0) * DARK_GAIN).min(1.0)]
1278 }
1279 
1280 /// The signed distance range an edge's shading occupies, relative to the boundary.
1281 #[inline]
1282 fn bevel_span(kind: EdgeKind, t: f32) -> (f32, f32) {
1283     match kind {
1284         EdgeKind::Rim => (0.0, t),
1285         EdgeKind::Step => (-0.5 * t, 0.5 * t),
1286     }
1287 }
1288 
1289 /// How many gradient bands to slice a lip of thickness `t` into. Vertex colors interpolate
1290 /// linearly, so each band is a chord of the shading curve; one band per ~1.25px keeps the
1291 /// error under a shade step without emitting geometry finer than the display resolves.
1292 /// The cap rose with the curvature term: a step now has two features across its width
1293 /// (shoulder and fillet), so it needs double the samples a single bump did.
1294 fn default_bevel_bands(t: f32) -> usize {
1295     ((t / 1.25).ceil() as usize).clamp(1, 12)
1296 }
1297 
1298 /// As [`push_bevel_edge_vertices_radii`], with the band count forced and the walls
1299 /// selectable — for callers that want a coarser or finer roll-off than thickness alone
1300 /// implies, or that are shading a step rather than a closed shape.
1301 ///
1302 /// `edges` is (top, right, bottom, left). Suppressing a wall matters for a region that
1303 /// runs flush to the surface's own edge: a full-width menubar sunk into the top of a plate
1304 /// is a *plateau one step down*, not a trough, so its only real wall is the one facing the
1305 /// content. Drawing the other three would carve a lip along the plate's outer edge, where
1306 /// the plate's own roll already lives, and the two would fight.
1307 pub fn push_bevel_edge_vertices_banded(
1308     x: f32, y: f32, ww: f32, h: f32,
1309     radii: (f32, f32, f32, f32),
1310     t: f32,
1311     sw: f32, sh: f32,
1312     base_color: [f32; 4],
1313     clip_circle: [f32; 3],
1314     light_sign: f32,
1315     bands: usize,
1316     edges: (bool, bool, bool, bool),
1317     kind: EdgeKind,
1318     out: &mut Vec<Vertex>,
1319 ) {
1320     // Floored for the same reason as `plate_push_raised`'s cap: a negative
1321     // extent must degrade to no ring, not panic in `clamp`.
1322     let cap = (ww.min(h) * 0.5).max(0.0);
1323     let (tl, tr, br, bl) = (
1324         radii.0.clamp(0.0, cap),
1325         radii.1.clamp(0.0, cap),
1326         radii.2.clamp(0.0, cap),
1327         radii.3.clamp(0.0, cap),
1328     );
1329     let t = t.clamp(0.0, cap);
1330     if t <= 0.0 {
1331         return;
1332     }
1333     let bands = bands.max(1);
1334 
1335     let rad = crate::layout::light_source_position();
1336     let lx = rad.cos() * light_sign;
1337     let ly = -rad.sin() * light_sign;
1338     let depth = crate::layout::bevel_depth();
1339 
1340     // `base_color` is no longer painted: shading is an overlay (see `overlay_color`), so
1341     // the surface below shows through with its own gradients and translucency intact.
1342     let _ = base_color;
1343     // Shading (directional + curvature, scaled by bevel_depth) at signed distance `d`,
1344     // for an edge whose outward flat normal is `dir`. A `Step` band runs negative — it
1345     // straddles the boundary into the plateau outside the rect, which is exactly what
1346     // removes the seam.
1347     let value = |dot: f32, d: f32| {
1348         depth * (bevel_profile(kind, d, t) * dot + bevel_curvature(kind, d, t, light_sign))
1349     };
1350     // The (up to two) overlay color pairs for a band running from value `v0` to `v1`:
1351     // one white pair and/or one black pair, each pass fading to zero alpha wherever the
1352     // value has the other sign. Both fire only when the band straddles the terminator.
1353     let passes = |v0: f32, v1: f32| -> [Option<([f32; 4], [f32; 4])>; 2] {
1354         [
1355             (v0 > 0.0 || v1 > 0.0).then(|| (overlay_light(v0), overlay_light(v1))),
1356             (v0 < 0.0 || v1 < 0.0).then(|| (overlay_dark(v0), overlay_dark(v1))),
1357         ]
1358     };
1359     let (span_lo, span_hi) = bevel_span(kind, t);
1360 
1361     // Each flat edge spans between its two adjoining corner radii, not a single uniform
1362     // inset — that is what lets the corners differ. At a square corner there is no arc to
1363     // cover the t×t patch where two edges meet, so the horizontal edges claim it (they run
1364     // the full span) and the vertical ones inset by `t`; overlapping them instead would
1365     // double-blend that patch, which shows as a dark notch on a translucent surface.
1366     let (left_top, left_bot) = (if tl > 0.0 { tl } else { t }, if bl > 0.0 { bl } else { t });
1367     let (right_top, right_bot) = (if tr > 0.0 { tr } else { t }, if br > 0.0 { br } else { t });
1368     let top_w = ww - tl - tr;
1369     let bottom_w = ww - bl - br;
1370     let left_h = h - left_top - left_bot;
1371     let right_h = h - right_top - right_bot;
1372 
1373     for k in 0..bands {
1374         let d0 = span_lo + (span_hi - span_lo) * (k as f32 / bands as f32);
1375         let d1 = span_lo + (span_hi - span_lo) * ((k + 1) as f32 / bands as f32);
1376         let bw = d1 - d0;
1377 
1378         // Top: outward normal (0,-1); the gradient runs downward, into the surface.
1379         if top_w > 0.0 && edges.0 {
1380             let (v0, v1) = (value(-ly, d0), value(-ly, d1));
1381             for (c0, c1) in passes(v0, v1).into_iter().flatten() {
1382                 out.extend_from_slice(&quad_vertices_shaded(
1383                     x + tl, y + d0, top_w, bw, sw, sh, c0, c0, c1, c1, clip_circle,
1384                 ));
1385             }
1386         }
1387         // Bottom: outward normal (0,1); gradient runs upward.
1388         if bottom_w > 0.0 && edges.2 {
1389             let (v0, v1) = (value(ly, d0), value(ly, d1));
1390             for (c0, c1) in passes(v0, v1).into_iter().flatten() {
1391                 out.extend_from_slice(&quad_vertices_shaded(
1392                     x + bl, y + h - d1, bottom_w, bw, sw, sh, c1, c1, c0, c0, clip_circle,
1393                 ));
1394             }
1395         }
1396         // Left: outward normal (-1,0); gradient runs rightward.
1397         if left_h > 0.0 && edges.3 {
1398             let (v0, v1) = (value(-lx, d0), value(-lx, d1));
1399             for (c0, c1) in passes(v0, v1).into_iter().flatten() {
1400                 out.extend_from_slice(&quad_vertices_shaded(
1401                     x + d0, y + left_top, bw, left_h, sw, sh, c0, c1, c1, c0, clip_circle,
1402                 ));
1403             }
1404         }
1405         // Right: outward normal (1,0); gradient runs leftward.
1406         if right_h > 0.0 && edges.1 {
1407             let (v0, v1) = (value(lx, d0), value(lx, d1));
1408             for (c0, c1) in passes(v0, v1).into_iter().flatten() {
1409                 out.extend_from_slice(&quad_vertices_shaded(
1410                     x + ww - d1, y + right_top, bw, right_h, sw, sh, c1, c0, c0, c1, clip_circle,
1411                 ));
1412             }
1413         }
1414     }
1415 
1416     // A corner arc belongs to both of its adjoining walls, so it is drawn only when both
1417     // are — otherwise a suppressed wall would still get a quarter of a lip.
1418     let corners = [
1419         (x + tl, y + tl, tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI, edges.0 && edges.3), // Top-Left
1420         (x + ww - tr, y + tr, tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI, edges.0 && edges.1), // Top-Right
1421         (x + ww - br, y + h - br, br, 0.0, 0.5 * std::f32::consts::PI, edges.2 && edges.1), // Bottom-Right
1422         (x + bl, y + h - bl, bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI, edges.2 && edges.3), // Bottom-Left
1423     ];
1424 
1425     for &(cx, cy, r, start_angle, end_angle, enabled) in &corners {
1426         // A square corner has no arc to sweep — the flat edges already met there.
1427         if r <= 0.0 || !enabled {
1428             continue;
1429         }
1430         // The corner is a quarter of a torus: shading varies along the sweep (the normal
1431         // swings through 90° of the light) *and* across the lip (the roll-off). Both come
1432         // out of the vertex colors, so one quad per (segment × band) cell is enough — no
1433         // faceting, unlike the 16 flat wedges this replaced.
1434         let segments = ((r * 0.75) as usize).clamp(8, 48);
1435         let ct = t.min(r);
1436         for j in 0..segments {
1437             let theta0 = start_angle + (j as f32) * (end_angle - start_angle) / (segments as f32);
1438             let theta1 = start_angle + ((j + 1) as f32) * (end_angle - start_angle) / (segments as f32);
1439             let (cos0, sin0) = (theta0.cos(), theta0.sin());
1440             let (cos1, sin1) = (theta1.cos(), theta1.sin());
1441             for k in 0..bands {
1442                 let d0 = span_lo + (span_hi - span_lo) * (k as f32 / bands as f32);
1443                 let d1 = span_lo + (span_hi - span_lo) * ((k + 1) as f32 / bands as f32);
1444                 // Inward along the corner's radius is the same signed distance as inward
1445                 // from a flat edge, so the arc scales the span the same way.
1446                 let (r0, r1) = (r - ct * (d0 / t), r - ct * (d1 / t));
1447                 let p = |rho: f32, c: f32, s: f32| -> [f32; 2] {
1448                     [
1449                         ((cx + rho * c) / sw) * 2.0 - 1.0,
1450                         1.0 - ((cy + rho * s) / sh) * 2.0,
1451                     ]
1452                 };
1453                 // Outer/inner × the two sweep ends; each vertex gets its own value, and
1454                 // the cell is drawn once per overlay pass that has any coverage.
1455                 let vals = [
1456                     value(cos0 * lx + sin0 * ly, d0),
1457                     value(cos1 * lx + sin1 * ly, d0),
1458                     value(cos1 * lx + sin1 * ly, d1),
1459                     value(cos0 * lx + sin0 * ly, d1),
1460                 ];
1461                 let geo = [
1462                     p(r0, cos0, sin0),
1463                     p(r0, cos1, sin1),
1464                     p(r1, cos1, sin1),
1465                     p(r1, cos0, sin0),
1466                 ];
1467                 let mut cells: [Option<fn(f32) -> [f32; 4]>; 2] = [None, None];
1468                 if vals.iter().any(|&v| v > 0.0) {
1469                     cells[0] = Some(overlay_light);
1470                 }
1471                 if vals.iter().any(|&v| v < 0.0) {
1472                     cells[1] = Some(overlay_dark);
1473                 }
1474                 for f in cells.into_iter().flatten() {
1475                     let c: Vec<Vertex> = (0..4)
1476                         .map(|i| Vertex { position: geo[i], color: f(vals[i]), clip_circle })
1477                         .collect();
1478                     out.extend_from_slice(&[c[0], c[1], c[2], c[0], c[2], c[3]]);
1479                 }
1480             }
1481         }
1482     }
1483 }
1484 
1485 /// How strong the face gradient is, as a fraction of `bevel_depth` at the corner nearest
1486 /// the light. Deliberately well below the edge amplitude: the face is a plane, not a
1487 /// roll — it only *leans* toward the light.
1488 const FACE_RATIO: f32 = 0.35;
1489 
1490 /// The face lighting of a plate: a single diagonal luminance gradient across the whole
1491 /// surface, brightest at the corner facing `light_source_position` and darkest at the
1492 /// opposite one. This is the difference between an object and a sticker: a real surface
1493 /// under directional light is never uniform, and a perfectly flat fill makes the eye
1494 /// read the (much smaller) edge shading as frame decoration rather than shape.
1495 ///
1496 /// Emitted as the same two-pass white/black overlays as the relief primitives (see
1497 /// [`overlay_light`]/[`overlay_dark`]): fixed RGB per pass, per-corner alphas clamped at
1498 /// the terminator, bilinear across the quad. The quad is square — its corners poke past
1499 /// a rounded plate's arcs — but the compositor clips the window surface to the same
1500 /// radius, so the overhang never reaches the screen.
1501 pub fn push_plate_face_vertices(
1502     x: f32, y: f32, ww: f32, h: f32,
1503     sw: f32, sh: f32,
1504     clip_circle: [f32; 3],
1505     out: &mut Vec<Vertex>,
1506 ) {
1507     let rad = crate::layout::light_source_position();
1508     let (lx, ly) = (rad.cos(), -rad.sin());
1509     let amp = crate::layout::bevel_depth() * FACE_RATIO;
1510     // Corner value = how much its outward diagonal faces the light.
1511     let inv = std::f32::consts::FRAC_1_SQRT_2;
1512     let v_tl = amp * inv * (-lx - ly);
1513     let v_tr = amp * inv * (lx - ly);
1514     let v_br = amp * inv * (lx + ly);
1515     let v_bl = amp * inv * (-lx + ly);
1516     let vs = [v_tl, v_tr, v_br, v_bl];
1517     if vs.iter().any(|&v| v > 0.0) {
1518         out.extend_from_slice(&quad_vertices_shaded(
1519             x, y, ww, h, sw, sh,
1520             overlay_light(v_tl), overlay_light(v_tr), overlay_light(v_br), overlay_light(v_bl),
1521             clip_circle,
1522         ));
1523     }
1524     if vs.iter().any(|&v| v < 0.0) {
1525         out.extend_from_slice(&quad_vertices_shaded(
1526             x, y, ww, h, sw, sh,
1527             overlay_dark(v_tl), overlay_dark(v_tr), overlay_dark(v_br), overlay_dark(v_bl),
1528             clip_circle,
1529         ));
1530     }
1531 }
1532 
1533 pub fn push_plate_solid_border_vertices(
1534     x: f32, y: f32, ww: f32, h: f32,
1535     radii: crate::widget::CornerRadii,
1536     t: f32,
1537     sw: f32, sh: f32,
1538     color: [f32; 4],
1539     clip_circle: [f32; 3],
1540     out: &mut Vec<Vertex>,
1541 ) {
1542     let mut r_tl = radii.top_left.max(0.0);
1543     let mut r_tr = radii.top_right.max(0.0);
1544     let mut r_br = radii.bottom_right.max(0.0);
1545     let mut r_bl = radii.bottom_left.max(0.0);
1546 
1547     // Simple scale clamping
1548     let sum_top = r_tl + r_tr;
1549     if sum_top > ww {
1550         let f = ww / sum_top;
1551         r_tl *= f;
1552         r_tr *= f;
1553     }
1554     let sum_bottom = r_bl + r_br;
1555     if sum_bottom > ww {
1556         let f = ww / sum_bottom;
1557         r_bl *= f;
1558         r_br *= f;
1559     }
1560     let sum_left = r_tl + r_bl;
1561     if sum_left > h {
1562         let f = h / sum_left;
1563         r_tl *= f;
1564         r_bl *= f;
1565     }
1566     let sum_right = r_tr + r_br;
1567     if sum_right > h {
1568         let f = h / sum_right;
1569         r_tr *= f;
1570         r_br *= f;
1571     }
1572 
1573     out.extend_from_slice(&quad_vertices_with_clip(x + r_tl, y, ww - r_tl - r_tr, t, sw, sh, color, clip_circle));
1574     out.extend_from_slice(&quad_vertices_with_clip(x, y + r_tl, t, h - r_tl - r_bl, sw, sh, color, clip_circle));
1575     out.extend_from_slice(&quad_vertices_with_clip(x + r_bl, y + h - t, ww - r_bl - r_br, t, sw, sh, color, clip_circle));
1576     out.extend_from_slice(&quad_vertices_with_clip(x + ww - t, y + r_tr, t, h - r_tr - r_br, sw, sh, color, clip_circle));
1577 
1578     let segments = 16;
1579     let corner_e = 2.0 / crate::layout::corner_shape();
1580 
1581     // Corner strokes as annulus strips between the outer superellipse (radius
1582     // r) and its inner scaled copy (r - t): at 1px thickness the scaled inner
1583     // curve is indistinguishable from the true parallel curve, and at
1584     // corner_shape 2 this is exactly the circular arc annulus. NOT
1585     // push_arc_background_vertices — that stays circular for genuine arcs.
1586     let corner = |cx: f32, cy: f32, r: f32, start: f32, end: f32, out: &mut Vec<Vertex>| {
1587         let r_in = (r - t).max(0.0);
1588         let ndc = |px: f32, py: f32| [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0];
1589         for i in 0..segments {
1590             let t1 = start + (i as f32) * (end - start) / segments as f32;
1591             let t2 = start + ((i + 1) as f32) * (end - start) / segments as f32;
1592             let (c1, s1) = superellipse_pt(t1, corner_e);
1593             let (c2, s2) = superellipse_pt(t2, corner_e);
1594             let o1 = ndc(cx + r * c1, cy + r * s1);
1595             let o2 = ndc(cx + r * c2, cy + r * s2);
1596             let i1 = ndc(cx + r_in * c1, cy + r_in * s1);
1597             let i2 = ndc(cx + r_in * c2, cy + r_in * s2);
1598             out.push(Vertex { position: o1, color, clip_circle });
1599             out.push(Vertex { position: o2, color, clip_circle });
1600             out.push(Vertex { position: i1, color, clip_circle });
1601             out.push(Vertex { position: o2, color, clip_circle });
1602             out.push(Vertex { position: i2, color, clip_circle });
1603             out.push(Vertex { position: i1, color, clip_circle });
1604         }
1605     };
1606 
1607     if r_tl > 0.1 {
1608         corner(x + r_tl, y + r_tl, r_tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI, out);
1609     }
1610     if r_tr > 0.1 {
1611         corner(x + ww - r_tr, y + r_tr, r_tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI, out);
1612     }
1613     if r_br > 0.1 {
1614         corner(x + ww - r_br, y + h - r_br, r_br, 0.0, 0.5 * std::f32::consts::PI, out);
1615     }
1616     if r_bl > 0.1 {
1617         corner(x + r_bl, y + h - r_bl, r_bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI, out);
1618     }
1619 }
1620 
1621 pub fn push_plate_solid_border_vertices_legacy(
1622     x: f32, y: f32, ww: f32, h: f32,
1623     r: f32,
1624     t: f32,
1625     sw: f32, sh: f32,
1626     color: [f32; 4],
1627     clip_circle: [f32; 3],
1628     out: &mut Vec<Vertex>,
1629 ) {
1630     let radii = crate::widget::CornerRadii::uniform(r);
1631     push_plate_solid_border_vertices(x, y, ww, h, radii, t, sw, sh, color, clip_circle, out);
1632 }
1633 
1634 pub fn widget_vertices(w: &dyn crate::widget::WidgetHost, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
1635     let mut verts = Vec::new();
1636     push_widget_vertices(w, sw, sh, clip_circle, &mut verts);
1637     verts
1638 }
1639 
1640 pub fn push_widget_vertices(w: &dyn crate::widget::WidgetHost, sw: f32, sh: f32, clip_circle: [f32; 3], out: &mut Vec<Vertex>) {
1641     let (x, y, ww, h) = w.rect();
1642     let radii = w.corner_radii();
1643     if let Some(thickness) = w.plate_bevel() {
1644         let t = thickness;
1645         // Full-size fill: the bevel lip is a shading overlay now, not a paint of the
1646         // outer ring, so an inset fill would leave the ring unfilled.
1647         push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, w.color(), clip_circle, None, out);
1648         push_plate_bevel_vertices(x, y, ww, h, radii.top_left, t, sw, sh, w.color(), clip_circle, out);
1649     } else {
1650         push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, w.color(), clip_circle, None, out);
1651         if let Some((color, thickness)) = w.solid_border() {
1652             push_plate_solid_border_vertices(x, y, ww, h, radii, thickness, sw, sh, color, clip_circle, out);
1653         }
1654     }
1655 
1656     for (cx, cy, r, t, start, end, qc) in w.extra_arcs() {
1657         push_arc_background_vertices(cx, cy, r, t, start, end, sw, sh, qc, 16, clip_circle, out);
1658     }
1659 }
1660 
1661 /// A contiguous run of vertices sharing one scissor rect (Phase 3 single paint path) and one
1662 /// rounded-rect clip. `scissor` is a logical-pixel clip (`None` = unclipped); `clip_rrect` is
1663 /// the paint walk's `[cx, cy, bx, by, r]` rounded clip in logical px (`None` = unclipped),
1664 /// applied as per-draw push-constant state; `start..end` indexes the flat vertex buffer.
1665 pub struct DlBatch {
1666     pub scissor: Option<crate::scene::layout::Rect>,
1667     pub clip_rrect: Option<[f32; 5]>,
1668     pub start: u32,
1669     pub end: u32,
1670     /// When set, this batch is one SDF-lit plate cover quad (see
1671     /// [`crate::vk::PlatePush`]; already in physical px). Never merged.
1672     pub plate: Option<crate::vk::PlatePush>,
1673     /// A blur-behind plate (negative-alpha color): before drawing this batch
1674     /// the renderer snapshots the swapchain-so-far into its snapshot image, so
1675     /// the blur samples everything painted beneath the plate — not just the 3D
1676     /// scene backdrop. Never merged.
1677     pub blur_behind: bool,
1678 }
1679 
1680 /// An image draw from the display list: `at` is the vertex index it sorts
1681 /// before (its position in the tessellated stream); `clip` is the item's
1682 /// paint-walk clip. Logical coordinates throughout.
1683 pub struct DlImage {
1684     pub image: u32,
1685     pub rect: crate::scene::layout::Rect,
1686     pub alpha: f32,
1687     pub at: u32,
1688     pub clip: Option<crate::scene::layout::Rect>,
1689 }
1690 
1691 /// Tessellate a `scene::paint::DisplayList`'s geometry into a flat vertex buffer plus per-clip draw
1692 /// batches, reusing the same tessellators as the legacy path so vertices are identical. `Text`
1693 /// prims are skipped here — text is still rendered via the app's `text_areas()` path. `sw`/`sh` are
1694 /// logical surface dimensions (as everywhere else); `scale` is the HiDPI factor, needed because an
1695 /// item's circular clip rides the vertices in PHYSICAL pixels. Consecutive prims sharing a clip are
1696 /// merged into one batch (the circle clip is per-vertex, so it never splits batches).
1697 /// `CCE_PLATE_DEBUG=1` — trace which carves group into their host plate as exact
1698 /// CSG features and which fall back to the standalone overlay shading.
1699 ///
1700 /// The two paths do NOT look the same: a grouped carve is part of the plate's
1701 /// single height field, so its wall meets the plate's rolled perimeter as a real
1702 /// junction, while the fallback approximates that with the host-box fade. Six
1703 /// conditions decide it, three of them dynamic (draw order, neighbouring plates,
1704 /// whether another carve already claimed the host's feature run), so the SAME
1705 /// widget can render either way depending on what is around it — and it does so
1706 /// silently. That has already shipped as a bug once: a hovered button's opaque
1707 /// fill used to sever every later button from the root plate they carve into,
1708 /// which is why `plate_stack` is a stack (see its comment below).
1709 ///
1710 /// Off by default and read once; the classification below runs only when set.
1711 /// Prim discriminant name, for `CCE_PLATE_DEBUG` reporting only.
1712 fn prim_kind(p: &crate::scene::paint::Prim) -> &'static str {
1713     use crate::scene::paint::Prim as P;
1714     match p {
1715         P::Quad { .. } => "Quad", P::RoundedRect { .. } => "RoundedRect",
1716         P::Border { .. } => "Border", P::Bevel { .. } => "Bevel",
1717         P::Recess { .. } => "Recess", P::Boss { .. } => "Boss",
1718         P::Ridge { .. } => "Ridge", P::Trough { .. } => "Trough", P::Plate { .. } => "Plate",
1719         P::Arc { .. } => "Arc", P::ArcShaded { .. } => "ArcShaded",
1720         P::Vector { .. } => "Vector", P::Circle { .. } => "Circle",
1721         P::Sphere { .. } => "Sphere", P::Droplet { .. } => "Droplet",
1722         P::DropletScrim { .. } => "DropletScrim",
1723         P::ConcaveFillet { .. } => "ConcaveFillet",
1724         P::Groove { .. } => "Groove", P::Lattice { .. } => "Lattice", P::Grout { .. } => "Grout", P::Fill { .. } => "Fill",
1725         P::CarveUnion { .. } => "CarveUnion", P::Glow { .. } => "Glow",
1726         P::Text { .. } => "Text", P::Image { .. } => "Image",
1727     }
1728 }
1729 
1730 fn plate_debug() -> bool {
1731     static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1732     *ON.get_or_init(|| std::env::var("CCE_PLATE_DEBUG").is_ok_and(|v| v != "0"))
1733 }
1734 
1735 /// Debug builds make one kind of fallback LOUD without `CCE_PLATE_DEBUG`: a
1736 /// carve that could group (full ring, untinted) failing to while a still-open
1737 /// plate encloses it and the carve's shaded region reaches that plate's
1738 /// perimeter roll. There the grouped and overlay paths shade the junction
1739 /// differently, and the rejection is one of the dynamic rules — so the SAME
1740 /// widget can flip looks frame to frame with nothing on stderr. Not an
1741 /// assert/panic: every rejection is conservative-CORRECT (the audit that
1742 /// shipped CCE_PLATE_DEBUG found no misgrouping; a later plate overlapping the
1743 /// carve genuinely must be shaded over, not under) — it is the frame-to-frame
1744 /// LOOK that flips, so the right loudness is an unmissable warning, not a
1745 /// crash. The ubiquitous quiet case stays quiet by construction: ordinary
1746 /// geometry closing every grouping window empties `plate_stack`, so no
1747 /// enclosing OPEN plate exists and this never runs — that is draw-order
1748 /// design, not a flip.
1749 ///
1750 /// Returns the dynamic rule to report, or `None` when the fallback is not the
1751 /// loud case. Pure so the classification is unit-testable; `later_plates` are
1752 /// the open plates emitted after the enclosing host.
1753 #[cfg(debug_assertions)]
1754 fn near_roll_fallback_reason(
1755     carve: &crate::scene::layout::Rect,
1756     depth: f32,
1757     host: &crate::scene::layout::Rect,
1758     roll: f32,
1759     later_plates: &[crate::scene::layout::Rect],
1760     budget_full: bool,
1761 ) -> Option<&'static str> {
1762     // The carve's shaded region — the overlay path's cover-quad inflation.
1763     let infl = depth * 0.5 + 2.0;
1764     let (sx0, sy0) = (carve.x - infl, carve.y - infl);
1765     let (sx1, sy1) = (carve.x + carve.width + infl, carve.y + carve.height + infl);
1766     // "Near the roll" = the shaded region leaves the host rect deflated by the
1767     // host's own roll width on any side.
1768     let near = sx0 < host.x + roll
1769         || sy0 < host.y + roll
1770         || sx1 > host.x + host.width - roll
1771         || sy1 > host.y + host.height - roll;
1772     if !near {
1773         return None;
1774     }
1775     // The dynamic rules, in the order the grouping guard tests them.
1776     if budget_full {
1777         return Some("the feature budget is full");
1778     }
1779     if later_plates
1780         .iter()
1781         .any(|o| sx0 < o.x + o.width && sx1 > o.x && sy0 < o.y + o.height && sy1 > o.y)
1782     {
1783         return Some("a later plate overlaps the carve's shaded region");
1784     }
1785     Some("the host's feature run is closed (another plate appended features since)")
1786 }
1787 
1788 /// Print a near-roll fallback warning once per distinct message — a carve in a
1789 /// steady layout would otherwise repeat it every frame.
1790 #[cfg(debug_assertions)]
1791 fn plate_carve_warn_once(msg: String) {
1792     use std::sync::{Mutex, OnceLock};
1793     static SEEN: OnceLock<Mutex<std::collections::HashSet<String>>> = OnceLock::new();
1794     let seen = SEEN.get_or_init(|| Mutex::new(std::collections::HashSet::new()));
1795     if seen.lock().unwrap().insert(msg.clone()) {
1796         eprintln!("{msg}");
1797     }
1798 }
1799 
1800 pub fn tessellate_display_list(
1801     dl: &crate::scene::paint::DisplayList,
1802     sw: f32,
1803     sh: f32,
1804     scale: f32,
1805 ) -> (Vec<Vertex>, Vec<DlBatch>, Vec<DlImage>, Vec<[f32; 12]>) {
1806     use crate::scene::material::PlateRole;
1807     use crate::scene::paint::{Cap, Prim};
1808     let mut verts: Vec<Vertex> = Vec::new();
1809     let mut batches: Vec<DlBatch> = Vec::new();
1810     let mut images: Vec<DlImage> = Vec::new();
1811     // Carves CSG'd into plates (see Frame2D::plate_features), plus the plate
1812     // they group into: the most recent Plate/Bevel batch, provided only Text
1813     // and Image prims (which draw through separate paths anyway) intervene.
1814     let mut features: Vec<[f32; 12]> = Vec::new();
1815     // Open carve-host plates, in emission order (innermost candidates last).
1816     // A STACK, not a single slot: a sibling plate emitted between a root plate
1817     // and its later carves (a hovered button's opaque fill among transparent
1818     // ones) must not sever those carves from the root plate they are carved
1819     // into — that severing rendered every button after the hovered one
1820     // through the visually-different overlay fallback. Ordinary geometry
1821     // still closes every open plate (the draw-order rule below).
1822     let mut plate_stack: Vec<(usize, crate::scene::layout::Rect)> = Vec::new();
1823     // Which plate last appended a carve feature: a plate's features are
1824     // addressed as one contiguous [offset, count] run (PlatePush::host), so a
1825     // plate may only receive MORE features while no other plate has appended
1826     // any since.
1827     let mut last_feature_plate: Option<usize> = None;
1828     // `CCE_PLATE_DEBUG` bookkeeping — see `plate_debug`.
1829     let dbg_plates = plate_debug();
1830     let mut dbg_grouped = 0usize;
1831     let mut dbg_fell_back: Vec<String> = Vec::new();
1832     let mut dbg_opened = 0usize;
1833     // Which prim kind closed a still-open grouping window, and how many plates
1834     // it closed — the answer to "why was there no enclosing plate?".
1835     let mut dbg_closed_by: std::collections::BTreeMap<&'static str, usize> =
1836         std::collections::BTreeMap::new();
1837 
1838     // SDF-lit plate path (shader2d's plate branch) vs the legacy banded vertex
1839     // shading, plus the frame-constant lighting inputs it pushes per plate.
1840     let shader_plates = crate::layout::bevel_shader();
1841     // Light and material come from `scene::relief_shade`, which is also what
1842     // cce-relief predicts pixels with — one definition, so the editor cannot
1843     // draw a different material than the renderer applies.
1844     let plate_light = crate::scene::relief_shade::light_vector();
1845     // [shading strength (1.0 at the default bevel_depth), specular strength,
1846     // shininess, curvature/AO strength] — the DE's finish, for the CARVES,
1847     // which shade whatever is beneath them and so take the host's. A prim
1848     // that carries a Material (Plate, Bevel, Sphere, Droplet) pushes its own
1849     // `material.finish` instead. Curvature is kept near the raised path's
1850     // crest amplitude: the recess shoulder's brightening lands on the same
1851     // pixels as its specular line, and the two stack — at 0.5 the step read
1852     // several times hotter than a plate roll.
1853     let plate_mat = crate::scene::material::Finish::from_style().to_array();
1854 
1855     for item in &dl.items {
1856         let mut start = verts.len() as u32;
1857         let mut plate: Option<crate::vk::PlatePush> = None;
1858         // A frosted flat fill promoted to a zero-depth plate batch (below):
1859         // it carries a recipe like any plate, but it is ordinary geometry to
1860         // the carve grouping — it opens no host and closes the open ones.
1861         let mut promoted = false;
1862         let mut made_plate: Option<crate::scene::layout::Rect> = None;
1863         // Blur-behind marker: a prim whose FILL alpha is negative asks the
1864         // renderer to snapshot the frame-so-far before it draws. Every
1865         // fill-bearing prim counts — the shader's a<0 branch runs for all of
1866         // them, and a variant missing here still frosts, but against the
1867         // stale scene backdrop instead of the frame: a flat tint with no
1868         // content and no blur, which is how the Dropdown popover (Border)
1869         // and the menubar panels (Quad) shipped visibly unfrosted while the
1870         // context menu (Plate) worked.
1871         let mut blur_behind = matches!(
1872             &item.prim,
1873             crate::scene::paint::Prim::Quad { color, .. }
1874             | crate::scene::paint::Prim::RoundedRect { color, .. } if color[3] < 0.0
1875         ) || matches!(
1876             &item.prim,
1877             crate::scene::paint::Prim::Bevel { material, .. }
1878             | crate::scene::paint::Prim::Plate { material, .. }
1879             | crate::scene::paint::Prim::Droplet { material, .. }
1880                 if material.fill(PlateRole::Nested)[3] < 0.0
1881         ) || matches!(
1882             &item.prim,
1883             crate::scene::paint::Prim::Border { fill, .. } if fill[3] < 0.0
1884         ) || matches!(
1885             &item.prim,
1886             crate::scene::paint::Prim::Fill { material, .. } if material.fill(PlateRole::Nested)[3] < 0.0
1887         );
1888         // Logical [cx, cy, r] → the physical-pixel triple the vertex attribute carries.
1889         let no = item
1890             .clip_circle
1891             .map(|c| [c[0] * scale, c[1] * scale, c[2] * scale])
1892             .unwrap_or([0.0f32, 0.0, 0.0]);
1893         // Fixed 16-segment fans read as polygons once a circle/arc is pane-sized; scale
1894         // the fan with the PHYSICAL radius (capped — beyond 128 the chord error is
1895         // subpixel even on HiDPI).
1896         let segs = |radius: f32| -> usize { ((radius * scale) as usize).clamp(16, 128) };
1897         match &item.prim {
1898             Prim::Text { .. } => continue, // text goes through the glyph/text-span path
1899             Prim::Image { image, rect, alpha } => {
1900                 images.push(DlImage {
1901                     image: *image,
1902                     rect: *rect,
1903                     alpha: *alpha,
1904                     at: verts.len() as u32,
1905                     clip: item.clip,
1906                 });
1907                 continue;
1908             }
1909             // A frosted FLAT fill — a `Flat` control face, a menu panel, a
1910             // popover, an inset plate's face — is a zero-depth plate batch
1911             // (RFC material § 6.2): the same shader path as every plate, so
1912             // it carries its own frost recipe instead of a window-wide one,
1913             // with circular corners (shape 2) and no roll, which is what the
1914             // tessellated fill drew. The display list is untouched, so the
1915             // legacy bridges that extract RoundedRects still see one.
1916             Prim::Quad { rect, color } if shader_plates && color[3] < 0.0 => {
1917                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
1918                 plate = Some(flat_frost_push(rect, (0.0, 0.0, 0.0, 0.0), *color, scale, plate_light, plate_mat));
1919                 promoted = true;
1920             }
1921             Prim::RoundedRect { rect, radius, corners, color } if shader_plates && color[3] < 0.0 => {
1922                 let radii = (
1923                     if corners.0 { *radius } else { 0.0 },
1924                     if corners.1 { *radius } else { 0.0 },
1925                     if corners.2 { *radius } else { 0.0 },
1926                     if corners.3 { *radius } else { 0.0 },
1927                 );
1928                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
1929                 plate = Some(flat_frost_push(rect, radii, *color, scale, plate_light, plate_mat));
1930                 promoted = true;
1931             }
1932             Prim::Fill { rect, radii, material } if shader_plates && material.frost.is_frosted() => {
1933                 // A material's flat fill: the frosted promotion above with
1934                 // the MATERIAL's recipe (compression, refraction, radius)
1935                 // instead of the DE default's. Zero depth, circular
1936                 // corners, no host — exactly a promoted RoundedRect.
1937                 let color = material.fill(PlateRole::Nested);
1938                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
1939                 let mut p = plate_push_raised(rect, *radii, 0.0, scale, plate_light, plate_mat, false, Some(2.0));
1940                 let [fz, fw] = material.frost.pack(scale);
1941                 p.host[2] = fz;
1942                 p.host[3] = fw;
1943                 plate = Some(p);
1944                 promoted = true;
1945             }
1946             Prim::Fill { rect, radii, material } => {
1947                 // Opaque (or the legacy path): a plain rounded fill.
1948                 let cr = crate::widget::CornerRadii::new(radii.0, radii.1, radii.2, radii.3);
1949                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, cr, sw, sh, material.fill(PlateRole::Nested), no, None, &mut verts);
1950             }
1951             Prim::Border { rect, radii, fill, border, thickness } if shader_plates && fill[3] < 0.0 => {
1952                 // The fill as its own plate batch, closed here; the stroke
1953                 // follows as ordinary geometry in the batch the tail makes.
1954                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *fill));
1955                 let p = flat_frost_push(rect, *radii, *fill, scale, plate_light, plate_mat);
1956                 let end = verts.len() as u32;
1957                 plate_stack.clear();
1958                 batches.push(DlBatch { scissor: item.clip, clip_rrect: item.clip_rrect, start, end, plate: Some(p), blur_behind: true });
1959                 start = end;
1960                 blur_behind = false;
1961                 let cr = crate::widget::CornerRadii::new(radii.0, radii.1, radii.2, radii.3);
1962                 push_plate_solid_border_vertices(rect.x, rect.y, rect.width, rect.height, cr, *thickness, sw, sh, *border, no, &mut verts);
1963             }
1964             Prim::Quad { rect, color } => {
1965                 // Quads honor an active circle clip like circles/arcs do (the
1966                 // Ramp's foam-cell fills draw as clipped strips).
1967                 verts.extend(quad_vertices_with_clip(rect.x, rect.y, rect.width, rect.height, sw, sh, *color, no));
1968             }
1969             Prim::RoundedRect { rect, radius, corners, color } => {
1970                 let radii = crate::widget::CornerRadii::new(
1971                     if corners.0 { *radius } else { 0.0 },
1972                     if corners.1 { *radius } else { 0.0 },
1973                     if corners.2 { *radius } else { 0.0 },
1974                     if corners.3 { *radius } else { 0.0 },
1975                 );
1976                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, *color, no, None, &mut verts);
1977             }
1978             Prim::Border { rect, radii, fill, border, thickness } => {
1979                 let cr = crate::widget::CornerRadii::new(radii.0, radii.1, radii.2, radii.3);
1980                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, cr, sw, sh, *fill, no, None, &mut verts);
1981                 push_plate_solid_border_vertices(rect.x, rect.y, rect.width, rect.height, cr, *thickness, sw, sh, *border, no, &mut verts);
1982             }
1983             Prim::Glow { rect, radius, reach, color } => {
1984                 push_glow_vertices(rect.x, rect.y, rect.width, rect.height, *radius, *reach, sw, sh, *color, no, &mut verts);
1985             }
1986             Prim::Bevel { rect, radii, material, depth, tint } if shader_plates => {
1987                 let color = material.fill(PlateRole::Nested);
1988                 let mat = material.finish.to_array();
1989                 // SDF-lit raised plate: one cover quad; the shader owns fill,
1990                 // roll shading, corners, and silhouette AA. Nominal corner
1991                 // radii (scale_corners false): a Bevel is a WIDGET-scale plate
1992                 // whose silhouette must match the nominal-radius squircles of
1993                 // the controls around it — only window-scale `Plate`s get the
1994                 // curvature-matched span.
1995                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
1996                 let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, mat, false, None);
1997                 // The plate's own frost recipe rides host.zw (see PlatePush).
1998                 let [fz, fw] = material.frost.pack(scale);
1999                 p.host[2] = fz;
2000                 p.host[3] = fw;
2001                 // w = 1 marks an accent-tinted plate (the focused-pane
2002                 // treatment): the shader then colors the WHOLE rolled edge
2003                 // with the tint, not just the specular glint — matching the
2004                 // free-carve path's tinted-well convention. Neutral white
2005                 // keeps w = 0 (spec-only, a no-op multiply).
2006                 let full = if *tint == [1.0, 1.0, 1.0] { 0.0 } else { 1.0 };
2007                 p.specular_tint = [tint[0], tint[1], tint[2], full];
2008                 plate = Some(p);
2009                 made_plate = Some(*rect);
2010             }
2011             Prim::Plate { rect, radii, material, depth, shape } if shader_plates => {
2012                 let color = material.fill(PlateRole::Nested);
2013                 let mat = material.finish.to_array();
2014                 if *depth < 0.0 {
2015                     // Negative depth = fill-less roll overlay (MODE_ROLL): the
2016                     // window-edge roll shading alone, screened over whatever is
2017                     // beneath — for a root plate whose face is not a fill (the
2018                     // designer's 3D canvas). The cover quad carries no color,
2019                     // and the batch is NOT opened as a carve host: an overlay
2020                     // owns no surface for a CSG feature to cut into.
2021                     verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, [0.0; 4]));
2022                     let mut p = plate_push_raised(rect, *radii, -*depth, scale, plate_light, mat, true, *shape);
2023                     p.mode = 11.0; // MODE_ROLL
2024                     plate = Some(p);
2025                 } else {
2026                     // Same lit-plate branch; the cover quad is the exact rect so the
2027                     // silhouette and the compositor's rounded window corners agree.
2028                     verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
2029                     let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, mat, true, *shape);
2030                     let [fz, fw] = material.frost.pack(scale);
2031                     p.host[2] = fz;
2032                     p.host[3] = fw;
2033                     plate = Some(p);
2034                     made_plate = Some(*rect);
2035                 }
2036             }
2037             Prim::Recess { rect, radii, depth, edges, .. }
2038             | Prim::Boss { rect, radii, depth, edges, .. }
2039             | Prim::Ridge { rect, radii, depth, edges }
2040             | Prim::Trough { rect, radii, depth, edges, .. }
2041                 if shader_plates =>
2042             {
2043                 let tint = match &item.prim {
2044                     Prim::Recess { tint, .. } => *tint,
2045                     Prim::Boss { tint, .. } => *tint,
2046                     Prim::Trough { tint, .. } => *tint,
2047                     _ => None,
2048                 };
2049                 // Recess carves down into the surface; Boss raises a plateau out
2050                 // of it (same machinery, depth sign flipped); Ridge is a raised
2051                 // rim straddling the boundary and Trough the sunken valley twin
2052                 // (their own overlay profiles — never grouped, the CSG features
2053                 // only model monotonic steps).
2054                 let mode = match &item.prim {
2055                     Prim::Boss { .. } => 3.0f32,
2056                     Prim::Ridge { .. } => 4.0,
2057                     Prim::Trough { .. } => 9.0,
2058                     _ => 2.0,
2059                 };
2060                 let raised = mode > 2.5;
2061                 // Grouped into the enclosing plate whenever one is live: the
2062                 // carve becomes a CSG feature of that plate's single draw —
2063                 // exact composite shading, real junctions at the plate's rolled
2064                 // perimeter — instead of a shading overlay (the fallback below).
2065                 //
2066                 // Edge-suppressed carves NEVER group: a suppressed wall's rect
2067                 // extends past the carve (below), relying on the overlay cover
2068                 // quad to keep that shading out of the drawn pixels — a clip
2069                 // the plate's whole-surface draw does not have, so grouped it
2070                 // smears the extended walls across the plate. Union pieces
2071                 // (section wells, a spinbox's field and button run) are
2072                 // exactly these.
2073                 // A tinted carve also never groups: a CSG feature is geometry only,
2074                 // so the tint could only land on the whole plate's specular.
2075                 let full_ring = *edges == (true, true, true, true);
2076                 let host_plate = if mode < 3.5 && full_ring && tint.is_none() && features.len() < crate::vk::MAX_PLATE_FEATURES {
2077                     // The carve's shaded region, for the occlusion test below
2078                     // (the overlay path's cover-quad inflation).
2079                     let infl = *depth * 0.5 + 2.0;
2080                     let (sx0, sy0) = (rect.x - infl, rect.y - infl);
2081                     let (sx1, sy1) = (rect.x + rect.width + infl, rect.y + rect.height + infl);
2082                     plate_stack
2083                         .iter()
2084                         .enumerate()
2085                         .rev()
2086                         .find(|(si, (bi, prect))| {
2087                             let inside = rect.x >= prect.x - 0.5
2088                                 && rect.y >= prect.y - 0.5
2089                                 && rect.x + rect.width <= prect.x + prect.width + 0.5
2090                                 && rect.y + rect.height <= prect.y + prect.height + 0.5;
2091                             if !inside {
2092                                 return false;
2093                             }
2094                             // Pixels drawn since this plate (a LATER plate in the
2095                             // stack) must not overlap the carve — its shading would
2096                             // land beneath them in this plate's earlier draw.
2097                             if plate_stack[si + 1..].iter().any(|(_, orect)| {
2098                                 sx0 < orect.x + orect.width
2099                                     && sx1 > orect.x
2100                                     && sy0 < orect.y + orect.height
2101                                     && sy1 > orect.y
2102                             }) {
2103                                 return false;
2104                             }
2105                             // Contiguity: only the last feature-receiving plate (or
2106                             // one with no features yet) may take another.
2107                             batches[*bi].plate.as_ref().map_or(false, |p| p.host[1] == 0.0)
2108                                 || last_feature_plate == Some(*bi)
2109                         })
2110                         .map(|(_, &(bi, _))| bi)
2111                 } else {
2112                     None
2113                 };
2114                 // Debug-build loudness for the silent grouped→overlay flip —
2115                 // see `near_roll_fallback_reason` on what qualifies and why
2116                 // this warns instead of panicking.
2117                 #[cfg(debug_assertions)]
2118                 if host_plate.is_none() && mode < 3.5 && full_ring && tint.is_none() {
2119                     let enclosing = plate_stack.iter().enumerate().rev().find(|(_, (_, p))| {
2120                         rect.x >= p.x - 0.5
2121                             && rect.y >= p.y - 0.5
2122                             && rect.x + rect.width <= p.x + p.width + 0.5
2123                             && rect.y + rect.height <= p.y + p.height + 0.5
2124                     });
2125                     if let Some((si, &(bi, prect))) = enclosing {
2126                         // Host roll width rides the push's light.w (physical px).
2127                         let roll = batches[bi].plate.as_ref().map_or(0.0, |p| p.light[3]) / scale;
2128                         let later: Vec<crate::scene::layout::Rect> =
2129                             plate_stack[si + 1..].iter().map(|&(_, r)| r).collect();
2130                         let budget_full = features.len() >= crate::vk::MAX_PLATE_FEATURES;
2131                         if let Some(why) =
2132                             near_roll_fallback_reason(rect, *depth, &prect, roll, &later, budget_full)
2133                         {
2134                             let kind = if mode > 2.5 { "boss" } else { "recess" };
2135                             plate_carve_warn_once(format!(
2136                                 "plate-carve: near-roll {kind} ({:.0},{:.0} {:.0}x{:.0}) lost grouping — {why}; \
2137                                  its junction with the host plate's roll shades through the overlay fallback, \
2138                                  visually different from grouped frames (CCE_PLATE_DEBUG=1 traces verdicts) \
2139                                  [debug-build warning, printed once]",
2140                                 rect.x, rect.y, rect.width, rect.height
2141                             ));
2142                         }
2143                     }
2144                 }
2145                 if dbg_plates {
2146                     match host_plate {
2147                         Some(_) => dbg_grouped += 1,
2148                         None => {
2149                             // Re-derive WHY, in the same order the guard tests
2150                             // them. Debug-only: the hot path above is untouched.
2151                             let kind = match &item.prim {
2152                                 Prim::Boss { .. } => "boss",
2153                                 Prim::Ridge { .. } => "ridge",
2154                                 Prim::Trough { .. } => "trough",
2155                                 _ => "recess",
2156                             };
2157                             let infl = *depth * 0.5 + 2.0;
2158                             let (sx0, sy0) = (rect.x - infl, rect.y - infl);
2159                             let (sx1, sy1) = (rect.x + rect.width + infl, rect.y + rect.height + infl);
2160                             let enclosing: Vec<usize> = plate_stack
2161                                 .iter()
2162                                 .enumerate()
2163                                 .filter(|(_, (_, p))| {
2164                                     rect.x >= p.x - 0.5
2165                                         && rect.y >= p.y - 0.5
2166                                         && rect.x + rect.width <= p.x + p.width + 0.5
2167                                         && rect.y + rect.height <= p.y + p.height + 0.5
2168                                 })
2169                                 .map(|(si, _)| si)
2170                                 .collect();
2171                             let occluded = |si: usize| {
2172                                 plate_stack[si + 1..].iter().any(|(_, o)| {
2173                                     sx0 < o.x + o.width && sx1 > o.x && sy0 < o.y + o.height && sy1 > o.y
2174                                 })
2175                             };
2176                             let why = if mode >= 3.5 {
2177                                 "ridge — never groups (its bump profile is not a monotonic step)".into()
2178                             } else if !full_ring {
2179                                 format!("edge-suppressed {edges:?} — the extended wall would smear across the host")
2180                             } else if tint.is_some() {
2181                                 "tinted — a CSG feature is geometry only, it carries no color".into()
2182                             } else if features.len() >= crate::vk::MAX_PLATE_FEATURES {
2183                                 format!("feature budget full ({} used)", features.len())
2184                             } else if enclosing.is_empty() {
2185                                 format!("no enclosing plate ({} open)", plate_stack.len())
2186                             } else if enclosing.iter().all(|&si| occluded(si)) {
2187                                 "a later plate overlaps this carve's shaded region".into()
2188                             } else {
2189                                 "host plate's feature run is closed (another carve appended since)".into()
2190                             };
2191                             dbg_fell_back.push(format!(
2192                                 "  overlay: {kind} ({:.0},{:.0} {:.0}x{:.0}) — {why}",
2193                                 rect.x, rect.y, rect.width, rect.height
2194                             ));
2195                         }
2196                     }
2197                 }
2198                 if let Some(bi) = host_plate {
2199                     {
2200                         // A wall the carve shares with the plate's edge extends
2201                         // past the plate, so the carve has no wall there.
2202                         let ext = *depth + 4.0;
2203                         let (mut x0, mut y0) = (rect.x, rect.y);
2204                         let (mut x1, mut y1) = (rect.x + rect.width, rect.y + rect.height);
2205                         if !edges.0 { y0 -= ext; }
2206                         if !edges.1 { x1 += ext; }
2207                         if !edges.2 { y1 += ext; }
2208                         if !edges.3 { x0 -= ext; }
2209                         let t_px = *depth * scale;
2210                         // The carve's drop: the material's pinned height, else
2211                         // the analytic ratio of the wall saturating at the DE's
2212                         // roll width (`layout::carve_depth_px` states the rule
2213                         // once for this path and the shader's free carves).
2214                         let k_mag = crate::layout::carve_depth_px(*depth) * scale;
2215                         // Negative depth = raised (Boss); the shader's summed
2216                         // slope vectors and curvature sign follow it.
2217                         let k_px = if raised { -k_mag } else { k_mag };
2218                         if let Some(p) = batches[bi].plate.as_mut() {
2219                             if p.host[1] == 0.0 {
2220                                 p.host[0] = features.len() as f32;
2221                             }
2222                             p.host[1] += 1.0;
2223                         }
2224                         last_feature_plate = Some(bi);
2225                         features.push([
2226                             (x0 + x1) * 0.5 * scale,
2227                             (y0 + y1) * 0.5 * scale,
2228                             (x1 - x0) * 0.5 * scale,
2229                             (y1 - y0) * 0.5 * scale,
2230                             radii.0 * scale,
2231                             radii.1 * scale,
2232                             radii.2 * scale,
2233                             radii.3 * scale,
2234                             t_px,
2235                             k_px,
2236                             0.0,
2237                             0.0,
2238                         ]);
2239                         continue;
2240                     }
2241                 }
2242                 // Overlay-only carve: the cover quad inflates by half the roll
2243                 // width (the step straddles the boundary) and carries no color —
2244                 // the shader emits translucent white/black over what's beneath.
2245                 let infl = *depth * 0.5 + 2.0;
2246                 verts.extend(quad_vertices(
2247                     rect.x - infl, rect.y - infl,
2248                     rect.width + 2.0 * infl, rect.height + 2.0 * infl,
2249                     sw, sh, [0.0; 4],
2250                 ));
2251                 // A suppressed wall is pushed past the cover quad, so its
2252                 // shading falls outside the drawn pixels (see Prim::Recess on
2253                 // why a flush region is a step, not a trough).
2254                 let ext = *depth + 4.0;
2255                 let (mut x0, mut y0) = (rect.x, rect.y);
2256                 let (mut x1, mut y1) = (rect.x + rect.width, rect.y + rect.height);
2257                 if !edges.0 { y0 -= ext; }
2258                 if !edges.1 { x1 += ext; }
2259                 if !edges.2 { y1 += ext; }
2260                 if !edges.3 { x0 -= ext; }
2261                 let sdf_rect = crate::scene::layout::Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
2262                 let mut p = plate_push_raised(&sdf_rect, *radii, *depth, scale, plate_light, plate_mat, false, None);
2263                 p.mode = mode;
2264                 // w = 1.0 flags the free-carve shader path to mix its white
2265                 // highlight screen toward the tint (plates leave w at 0.0).
2266                 if let Some(t) = tint {
2267                     p.specular_tint = [t[0], t[1], t[2], 1.0];
2268                 }
2269                 // Host-plate box for the roll fade: a suppressed wall means the
2270                 // recess runs flush to the host's edge there, so that side of
2271                 // the box sits at the original rect edge; enabled walls face
2272                 // host interior, pushed to ±1e5 so no fade applies.
2273                 const FAR: f32 = 1e5;
2274                 let (hx0, hy0) = (
2275                     if edges.3 { rect.x - FAR } else { rect.x },
2276                     if edges.0 { rect.y - FAR } else { rect.y },
2277                 );
2278                 let (hx1, hy1) = (
2279                     if edges.1 { rect.x + rect.width + FAR } else { rect.x + rect.width },
2280                     if edges.2 { rect.y + rect.height + FAR } else { rect.y + rect.height },
2281                 );
2282                 p.host = [
2283                     (hx0 + hx1) * 0.5 * scale,
2284                     (hy0 + hy1) * 0.5 * scale,
2285                     (hx1 - hx0) * 0.5 * scale,
2286                     (hy1 - hy0) * 0.5 * scale,
2287                 ];
2288                 plate = Some(p);
2289             }
2290             Prim::Bevel { rect, radii, material, depth, tint: _ } => {
2291                 let color = material.fill(PlateRole::Nested);
2292                 // Full-size fill: the lip is now a shading overlay, not a paint of the
2293                 // outer ring, so the fill must cover the whole rect (the old inset fill
2294                 // would leave the ring showing whatever lay beneath).
2295                 let corners = crate::widget::CornerRadii {
2296                     top_left: radii.0, top_right: radii.1,
2297                     bottom_right: radii.2, bottom_left: radii.3,
2298                 };
2299                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, corners, sw, sh, color, no, None, &mut verts);
2300                 push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, *depth, sw, sh, color, no, &mut verts);
2301             }
2302             Prim::Plate { rect, radii, material, depth, .. } => {
2303                 let color = material.fill(PlateRole::Nested);
2304                 if *depth < 0.0 {
2305                     // Fill-less roll overlay (negative-depth sentinel): the banded
2306                     // legacy tessellation has no overlay compositing, so the roll
2307                     // is simply absent here — the A/B path draws nothing rather
2308                     // than a wrong fill.
2309                     continue;
2310                 }
2311                 // Fill at full size (no inset — see Prim::Plate), then light the face,
2312                 // then roll the perimeter. The lip rides on top of the fill's outer band
2313                 // rather than replacing it, so the plate's silhouette and the
2314                 // compositor's rounded window corners still agree exactly.
2315                 let corners = crate::widget::CornerRadii {
2316                     top_left: radii.0, top_right: radii.1,
2317                     bottom_right: radii.2, bottom_left: radii.3,
2318                 };
2319                 push_rounded_rect_vertices_corners(
2320                     rect.x, rect.y, rect.width, rect.height, corners, sw, sh, color, no, None, &mut verts,
2321                 );
2322                 push_plate_face_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, no, &mut verts);
2323                 push_bevel_edge_vertices_radii(
2324                     rect.x, rect.y, rect.width, rect.height, *radii, *depth,
2325                     sw, sh, color, no, 1.0, &mut verts,
2326                 );
2327             }
2328             Prim::Recess { rect, radii, depth, edges, .. } => {
2329                 // Edges only — no fill: the shading is an overlay, so whatever is painted
2330                 // below (fill, rim gradient, blur) shows through the carve modulated
2331                 // rather than repainted. `light_sign = -1.0` shadows the lit-facing edges,
2332                 // which is the raised->recessed inversion.
2333                 push_bevel_edge_vertices_banded(
2334                     rect.x, rect.y, rect.width, rect.height, *radii, *depth,
2335                     sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), *edges,
2336                     EdgeKind::Step, &mut verts,
2337                 );
2338             }
2339             Prim::Boss { rect, radii, depth, edges, .. } => {
2340                 // Legacy raised step: the recess overlay with the light sign upright.
2341                 push_bevel_edge_vertices_banded(
2342                     rect.x, rect.y, rect.width, rect.height, *radii, *depth,
2343                     sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(*depth), *edges,
2344                     EdgeKind::Step, &mut verts,
2345                 );
2346             }
2347             Prim::Ridge { rect, radii, depth, edges } => {
2348                 // Legacy approximation: a raised step up at the boundary plus a
2349                 // recessed step down half a width in (the banded machinery has no
2350                 // bump profile; the double-pass hot crest is accepted here — the
2351                 // legacy path exists only for A/B comparison).
2352                 let half = *depth * 0.5;
2353                 push_bevel_edge_vertices_banded(
2354                     rect.x, rect.y, rect.width, rect.height, *radii, half,
2355                     sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), *edges,
2356                     EdgeKind::Step, &mut verts,
2357                 );
2358                 let ir = (radii.0 - half).max(0.0);
2359                 push_bevel_edge_vertices_banded(
2360                     rect.x + half, rect.y + half,
2361                     rect.width - *depth, rect.height - *depth,
2362                     (ir, ir, ir, ir), half,
2363                     sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), *edges,
2364                     EdgeKind::Step, &mut verts,
2365                 );
2366             }
2367             Prim::Trough { rect, radii, depth, edges, .. } => {
2368                 // Legacy approximation, the Ridge arm's two steps with the light
2369                 // signs swapped: down at the boundary, back up half a width in.
2370                 // The banded machinery has no valley profile, so this is the old
2371                 // stacked look — accepted here, as the legacy path exists only
2372                 // for A/B comparison against the SDF one.
2373                 let half = *depth * 0.5;
2374                 push_bevel_edge_vertices_banded(
2375                     rect.x, rect.y, rect.width, rect.height, *radii, half,
2376                     sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), *edges,
2377                     EdgeKind::Step, &mut verts,
2378                 );
2379                 let ir = (radii.0 - half).max(0.0);
2380                 push_bevel_edge_vertices_banded(
2381                     rect.x + half, rect.y + half,
2382                     rect.width - *depth, rect.height - *depth,
2383                     (ir, ir, ir, ir), half,
2384                     sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), *edges,
2385                     EdgeKind::Step, &mut verts,
2386                 );
2387             }
2388             Prim::Arc { cx, cy, radius, thickness, start: sa, end: ea, color } => {
2389                 push_arc_background_vertices(*cx, *cy, *radius, *thickness, *sa, *ea, sw, sh, *color, segs(*radius), no, &mut verts);
2390             }
2391             Prim::ArcShaded { cx, cy, radius, thickness, start: sa, end: ea, inner, crest, outer } => {
2392                 push_arc_shaded_vertices(*cx, *cy, *radius, *thickness, *sa, *ea, sw, sh, *inner, *crest, *outer, segs(*radius), no, &mut verts);
2393             }
2394             Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => {
2395                 let lc = match cap {
2396                     Cap::Flat => LineCap::Flat,
2397                     Cap::Round => LineCap::Round,
2398                     Cap::Arrow => LineCap::Arrow,
2399                 };
2400                 verts.extend(vector_vertices(*x1, *y1, *x2, *y2, *thickness, sw, sh, *color, lc));
2401             }
2402             Prim::Circle { cx, cy, radius, color } => {
2403                 if item.clip_circle.is_none() && *radius > 1.5 {
2404                     // Cover quad with the disc itself as the (feathered) circle
2405                     // clip: a per-pixel smooth silhouette instead of a hard-edged
2406                     // fan. The quad overhangs by 1px for the feather. Only when
2407                     // no ancestor clip holds the slot — then it's the fan path.
2408                     let own = [cx * scale, cy * scale, radius * scale];
2409                     let d = *radius + 1.0;
2410                     verts.extend(quad_vertices_with_clip(
2411                         cx - d, cy - d, 2.0 * d, 2.0 * d, sw, sh, *color, own,
2412                     ));
2413                 } else {
2414                     verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, *color, segs(*radius), no));
2415                 }
2416             }
2417             Prim::Sphere { cx, cy, radius, material } if shader_plates => {
2418                 let color = material.fill(PlateRole::Nested);
2419                 let mat = material.finish.to_array();
2420                 // A hemisphere lit per pixel by the plate branch (mode 5): one
2421                 // cover quad, its own never-merged batch. The quad overhangs
2422                 // the disc by 1px for the shader's silhouette anti-aliasing.
2423                 let d = *radius + 1.0;
2424                 verts.extend(quad_vertices(cx - d, cy - d, 2.0 * d, 2.0 * d, sw, sh, color));
2425                 plate = Some(crate::vk::PlatePush {
2426                     // Center + radius in physical px; the SDF box machinery is
2427                     // unused in this mode, so .w is free.
2428                     rect: [cx * scale, cy * scale, radius * scale, 0.0],
2429                     radii: [0.0; 4],
2430                     light: [plate_light[0], plate_light[1], plate_light[2], 0.0],
2431                     material: mat,
2432                     host: [0.0; 4],
2433                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2434                     mode: 5.0,
2435                     shape: 2.0,
2436                 });
2437             }
2438             Prim::Sphere { cx, cy, radius, material } => {
2439                 let color = material.fill(PlateRole::Nested);
2440                 // Legacy path: the flat disc, exactly a Circle.
2441                 verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, color, segs(*radius), no));
2442             }
2443             Prim::DropletScrim { rect, material, spec, feather } if shader_plates => {
2444                 let color = material.fill(PlateRole::Nested);
2445                 let mat = material.finish.to_array();
2446                 // Shader mode 12: the droplet's own SDF, filled flat and
2447                 // feathered inward. No contact shadow, so unlike the lit drop
2448                 // the cover quad is exactly the box — a scrim never draws
2449                 // outside the silhouette.
2450                 let g = droplet_geom(rect, spec);
2451                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
2452                 plate = Some(crate::vk::PlatePush {
2453                     rect: [
2454                         (rect.x + rect.width * 0.5) * scale,
2455                         (rect.y + rect.height * 0.5) * scale,
2456                         g.hx * scale,
2457                         g.hy * scale,
2458                     ],
2459                     radii: [g.sag * scale, g.br * scale, g.bw * scale, g.k * scale],
2460                     // p_light.w carries the FEATHER here; mode 12 returns
2461                     // before the shading band it otherwise holds is read.
2462                     light: [plate_light[0], plate_light[1], plate_light[2], feather.max(0.001) * scale],
2463                     material: [mat[0], 0.0, 0.0, 0.0],
2464                     host: [g.sr * scale, 0.0, 0.0, g.ar * scale],
2465                     specular_tint: [0.0, 0.0, 0.0, g.bow * scale],
2466                     mode: 12.0,
2467                     shape: spec.curve.clamp(2.0, 6.0),
2468                 });
2469             }
2470             Prim::Droplet { rect, material, spec } if shader_plates => {
2471                 let color = material.fill(PlateRole::Nested);
2472                 let mat = material.finish.to_array();
2473                 // A water droplet lit by shader mode 10: one cover quad; the
2474                 // shader owns silhouette (sheet ∪smin belly), dome shading,
2475                 // fresnel rim and thin-edge clarity. The spec's height
2476                 // fractions resolve against the concrete rect here, clamped so
2477                 // small or narrow boxes stay well-formed (a belly wider than
2478                 // the box would turn the SDF interior inside out).
2479                 // The cover quad grows sideways and BELOW the box by the
2480                 // contact shadow's reach — shadow fragments live outside the
2481                 // silhouette, so they need covered pixels to shade.
2482                 let g = droplet_geom(rect, spec);
2483                 let (hx, hy, sag, br, bw, k, sr, ar, band, bow, sh_reach) =
2484                     (g.hx, g.hy, g.sag, g.br, g.bw, g.k, g.sr, g.ar, g.band, g.bow, g.sh_reach);
2485                 verts.extend(quad_vertices(
2486                     rect.x - sh_reach,
2487                     rect.y,
2488                     rect.width + 2.0 * sh_reach,
2489                     rect.height + sh_reach,
2490                     sw, sh, color,
2491                 ));
2492                 plate = Some(crate::vk::PlatePush {
2493                     rect: [
2494                         (rect.x + rect.width * 0.5) * scale,
2495                         (rect.y + rect.height * 0.5) * scale,
2496                         hx * scale,
2497                         hy * scale,
2498                     ],
2499                     radii: [sag * scale, br * scale, bw * scale, k * scale],
2500                     light: [plate_light[0], plate_light[1], plate_light[2], band * scale],
2501                     // Slots y/z/w feed roll_spec and the rim term directly:
2502                     // a droplet's material carries its own gleam/shine/rim
2503                     // there (`DropletSpec::finish`; a drop is wetter than the
2504                     // DE's plates), so this is the material's finish like any
2505                     // plate's.
2506                     material: mat,
2507                     host: [sr * scale, spec.clarity.clamp(0.0, 1.0), spec.dome, ar * scale],
2508                     // Droplet glints are always white, so the tint RGB slots
2509                     // carry droplet params instead: x = core density,
2510                     // y = contact-shadow reach px, z = shadow strength.
2511                     specular_tint: [
2512                         spec.core.clamp(0.0, 2.0),
2513                         sh_reach * scale,
2514                         spec.shadow.clamp(0.0, 1.0),
2515                         bow * scale,
2516                     ],
2517                     mode: 10.0,
2518                     shape: spec.curve.clamp(2.0, 6.0),
2519                 });
2520             }
2521             Prim::DropletScrim { rect, material, spec, .. } => {
2522                 let color = material.fill(PlateRole::Nested);
2523                 // Legacy banded path: no SDF to feather against, so the scrim
2524                 // degrades to the same flat outline the drop itself does —
2525                 // hard-edged, but present. A prim with no arm here VANISHES.
2526                 let cap = (rect.height * 0.5).min(rect.width * 0.5);
2527                 let sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(cap);
2528                 let ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(cap);
2529                 let radii = crate::widget::CornerRadii::new(ar, ar, sr, sr);
2530                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, color, no, None, &mut verts);
2531             }
2532             Prim::Droplet { rect, material, spec } => {
2533                 let color = material.fill(PlateRole::Nested);
2534                 // Legacy banded path: the flat drop outline — attach-tapered
2535                 // top, round bottom. Degrades the material but keeps the
2536                 // silhouette (a prim with no arm here VANISHES, it doesn't
2537                 // degrade — see Ridge/Groove above).
2538                 let cap = (rect.height * 0.5).min(rect.width * 0.5);
2539                 let sr = (spec.sheet_r.clamp(0.0, 1.0) * rect.height).min(cap);
2540                 let ar = (spec.attach.clamp(0.0, 1.0) * rect.height).min(cap);
2541                 let radii = crate::widget::CornerRadii::new(ar, ar, sr, sr);
2542                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, radii, sw, sh, color, no, None, &mut verts);
2543             }
2544             Prim::ConcaveFillet { cx, cy, radius, depth, start: a0, raised } if shader_plates => {
2545                 // A quarter-arc carve wall (shader mode 6/7): one cover quad
2546                 // over the wedge's reach; the wall straddles the arc by ±t/2
2547                 // like every carve boundary. p_rect carries centre + radius,
2548                 // p_radii.x the wedge start angle. Host box pushed far out —
2549                 // an inside-corner fillet never fades.
2550                 let m = *depth * 0.5 + 2.0;
2551                 let r = *radius + m;
2552                 verts.extend(quad_vertices(cx - r, cy - r, 2.0 * r, 2.0 * r, sw, sh, [0.0; 4]));
2553                 plate = Some(crate::vk::PlatePush {
2554                     rect: [cx * scale, cy * scale, *radius * scale, 0.0],
2555                     radii: [*a0, 0.0, 0.0, 0.0],
2556                     light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
2557                     material: plate_mat,
2558                     host: [0.0, 0.0, 1e6, 1e6],
2559                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2560                     mode: if *raised { 7.0 } else { 6.0 },
2561                     shape: crate::layout::corner_shape(),
2562                 });
2563             }
2564             // Legacy banded path has no radial wall — the composed corner
2565             // stays square there (A/B comparison path only).
2566             Prim::ConcaveFillet { .. } => {}
2567             Prim::Groove { a, b, width, depth, host } if shader_plates => {
2568                 // A slab carve about the line a–b (shader mode 8): the cover
2569                 // quad is the segment's bounding box grown by the groove's own
2570                 // half-width plus the wall's reach. Off-band corners of that
2571                 // box sit at u = 1 (plateau), so the box overhang shades
2572                 // nothing — the slab is what bounds the mark, not the quad.
2573                 let m = *width * 0.5 + *depth * 0.5 + 2.0;
2574                 let (x0, x1) = (a.0.min(b.0) - m, a.0.max(b.0) + m);
2575                 let (y0, y1) = (a.1.min(b.1) - m, a.1.max(b.1) + m);
2576                 verts.extend(quad_vertices(x0, y0, x1 - x0, y1 - y0, sw, sh, [0.0; 4]));
2577                 // Unit normal of the line — the direction the slab's distance is
2578                 // measured along. A degenerate segment falls back to vertical so
2579                 // a zero-length groove is a no-op wall rather than a NaN.
2580                 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
2581                 let len = (dx * dx + dy * dy).sqrt();
2582                 let n = if len > 1e-4 { (-dy / len, dx / len) } else { (1.0, 0.0) };
2583                 plate = Some(crate::vk::PlatePush {
2584                     // Centre + slab half-width in physical px; .w unused.
2585                     rect: [
2586                         (a.0 + b.0) * 0.5 * scale,
2587                         (a.1 + b.1) * 0.5 * scale,
2588                         *width * 0.5 * scale,
2589                         0.0,
2590                     ],
2591                     radii: [n.0, n.1, 0.0, 0.0],
2592                     light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
2593                     material: plate_mat,
2594                     host: [
2595                         (host.x + host.width * 0.5) * scale,
2596                         (host.y + host.height * 0.5) * scale,
2597                         host.width * 0.5 * scale,
2598                         host.height * 0.5 * scale,
2599                     ],
2600                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2601                     mode: 8.0,
2602                     shape: crate::layout::corner_shape(),
2603                 });
2604             }
2605             Prim::Groove { a, b, width, depth, host: _ } => {
2606                 // Legacy approximation. The banded tessellators walk BOX edges —
2607                 // exactly the axis-aligned assumption a groove exists to escape —
2608                 // so the walls are drawn directly as two feathered lines meeting
2609                 // at the centerline: the engraved-line fake, one half in shadow
2610                 // and one lit. Coarser than the SDF (no profile curve, no host
2611                 // fade), but this path exists for A/B comparison, and drawing
2612                 // NOTHING would silently delete the mark rather than degrade it
2613                 // — see `Prim::Ridge` above, which accepts a hot crest for the
2614                 // same reason.
2615                 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
2616                 let len = (dx * dx + dy * dy).sqrt();
2617                 if len < 0.001 {
2618                     continue;
2619                 }
2620                 let n = (-dy / len, dx / len);
2621                 // Same convention as `push_bevel_edge_vertices_banded`: the
2622                 // light folded through `light_sign` (-1.0 — a groove is a
2623                 // carve), dotted with each wall's OUTWARD normal, amplitude on
2624                 // `bevel_depth`. So a groove re-lights with the DE's light
2625                 // instead of hardcoding which side is dark.
2626                 let rad = crate::layout::light_source_position();
2627                 let (lx, ly) = (-rad.cos(), rad.sin());
2628                 let v = crate::layout::bevel_depth() * (n.0 * lx + n.1 * ly);
2629                 // Each wall covers its own half, centreline to outer edge —
2630                 // abutting rather than overlapping. The SDF gets away with
2631                 // walls that overlap across a sub-pixel floor because it is one
2632                 // evaluation of |distance|; two opposite-signed overlays would
2633                 // just blend to mud.
2634                 let half = (*width * 0.5 + *depth * 0.5).max(0.5);
2635                 for side in [1.0f32, -1.0] {
2636                     let sv = v * side;
2637                     let c = if sv >= 0.0 { overlay_light(sv) } else { overlay_dark(sv) };
2638                     if c[3] <= 0.0 {
2639                         continue;
2640                     }
2641                     let off = side * half * 0.5;
2642                     push_feathered_line_vertices(
2643                         a.0 + n.0 * off, a.1 + n.1 * off,
2644                         b.0 + n.0 * off, b.1 + n.1 * off,
2645                         half, sw, sh, c, &mut verts,
2646                     );
2647                 }
2648             }
2649             Prim::Lattice { rect, period, origin, cell, radius, depth } if shader_plates => {
2650                 // A periodic well field (shader mode 13): one cover quad over
2651                 // `rect`; the shader folds each pixel into the period and
2652                 // measures the nearest cell, so the whole lattice is a single
2653                 // evaluation. p_rect = one cell's centre + half-extents,
2654                 // p_radii = the corner radius, p_host.xy = the period; the
2655                 // host-box fade sides are pushed far out (a lattice never
2656                 // fades against a host — its own rect bounds it).
2657                 let (pw, ph) = (period.0.max(1e-3), period.1.max(1e-3));
2658                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, [0.0; 4]));
2659                 plate = Some(crate::vk::PlatePush {
2660                     rect: [origin.0 * scale, origin.1 * scale, cell.0 * 0.5 * scale, cell.1 * 0.5 * scale],
2661                     radii: [*radius * scale; 4],
2662                     light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
2663                     material: plate_mat,
2664                     host: [pw * scale, ph * scale, 1e6, 1e6],
2665                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2666                     mode: 13.0,
2667                     shape: crate::layout::corner_shape(),
2668                 });
2669             }
2670             Prim::Grout { rect, period, origin, cell, radius, color } if shader_plates => {
2671                 // The lattice's fold, painted flat (shader mode 15): one cover
2672                 // quad in the grout colour; the shader keeps it outside the
2673                 // cells. Same push layout as the lattice; light/material are
2674                 // carried but unread.
2675                 let (pw, ph) = (period.0.max(1e-3), period.1.max(1e-3));
2676                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
2677                 plate = Some(crate::vk::PlatePush {
2678                     rect: [origin.0 * scale, origin.1 * scale, cell.0 * 0.5 * scale, cell.1 * 0.5 * scale],
2679                     radii: [*radius * scale; 4],
2680                     light: [plate_light[0], plate_light[1], plate_light[2], 0.0],
2681                     material: plate_mat,
2682                     host: [pw * scale, ph * scale, 1e6, 1e6],
2683                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2684                     mode: 15.0,
2685                     shape: crate::layout::corner_shape(),
2686                 });
2687             }
2688             // Legacy banded path: no periodic wall — the lattice and the grout
2689             // draw nothing there, like the fillet (A/B comparison path only).
2690             Prim::Lattice { .. } | Prim::Grout { .. } => {}
2691             Prim::CarveUnion { boxes, depth, raised } if shader_plates => {
2692                 // The union of several boxes as ONE wall (shader mode 14): the
2693                 // boxes go into the frame's feature buffer as a contiguous run
2694                 // and the shader takes the nearest one per pixel. The cover
2695                 // quad is the union's bounding box grown by the wall's reach;
2696                 // off-shape corners of it sit at the plateau and shade nothing.
2697                 let budget = crate::vk::MAX_PLATE_FEATURES.saturating_sub(features.len());
2698                 let take = boxes.len().min(budget);
2699                 if take < boxes.len() && plate_debug() {
2700                     eprintln!(
2701                         "plate-carve: union of {} boxes gets {} — feature budget full ({} used)",
2702                         boxes.len(), take, features.len()
2703                     );
2704                 }
2705                 if take == 0 {
2706                     continue;
2707                 }
2708                 let kept = &boxes[..take];
2709                 let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
2710                 for (r, _) in kept {
2711                     x0 = x0.min(r.x);
2712                     y0 = y0.min(r.y);
2713                     x1 = x1.max(r.x + r.width);
2714                     y1 = y1.max(r.y + r.height);
2715                 }
2716                 let infl = *depth * 0.5 + 2.0;
2717                 verts.extend(quad_vertices(
2718                     x0 - infl, y0 - infl,
2719                     (x1 - x0) + 2.0 * infl, (y1 - y0) + 2.0 * infl,
2720                     sw, sh, [0.0; 4],
2721                 ));
2722                 let off = features.len() as f32;
2723                 for (r, radii) in kept {
2724                     features.push([
2725                         (r.x + r.width * 0.5) * scale,
2726                         (r.y + r.height * 0.5) * scale,
2727                         r.width * 0.5 * scale,
2728                         r.height * 0.5 * scale,
2729                         radii.0 * scale,
2730                         radii.1 * scale,
2731                         radii.2 * scale,
2732                         radii.3 * scale,
2733                         *depth * scale,
2734                         0.0,
2735                         0.0,
2736                         0.0,
2737                     ]);
2738                 }
2739                 // The run is complete: a plate with an open feature run must
2740                 // not append past it (its features would no longer be
2741                 // contiguous), so it is closed here like any other appender.
2742                 last_feature_plate = None;
2743                 plate = Some(crate::vk::PlatePush {
2744                     rect: [
2745                         (x0 + x1) * 0.5 * scale,
2746                         (y0 + y1) * 0.5 * scale,
2747                         (x1 - x0) * 0.5 * scale,
2748                         (y1 - y0) * 0.5 * scale,
2749                     ],
2750                     // x: the raised flag; the shader reads nothing else here.
2751                     radii: [if *raised { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0],
2752                     light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
2753                     material: plate_mat,
2754                     // Feature run [offset, count] (the renderer rebases the
2755                     // offset onto the frame slot, as for mode 1); zw far out
2756                     // so the host-box fade never applies.
2757                     host: [off, take as f32, 1e6, 1e6],
2758                     specular_tint: [1.0, 1.0, 1.0, 0.0],
2759                     mode: 14.0,
2760                     shape: crate::layout::corner_shape(),
2761                 });
2762             }
2763             // Legacy banded path: no union — nothing is drawn there, like the
2764             // fillet and the lattice (A/B comparison path only).
2765             Prim::CarveUnion { .. } => {}
2766         }
2767         let end = verts.len() as u32;
2768         if end == start {
2769             continue;
2770         }
2771         // Some tessellators (quad_vertices, vector_vertices) don't thread the circle clip —
2772         // stamp the whole emitted range so every prim kind honors it uniformly.
2773         if item.clip_circle.is_some() {
2774             for v in verts[start as usize..].iter_mut() {
2775                 v.clip_circle = no;
2776             }
2777         }
2778         // Merge into the previous batch if it shares this clip pair and is contiguous.
2779         // Plate batches carry per-draw push constants, and blur-behind batches
2780         // trigger the renderer's snapshot copy, so neither ever merges.
2781         if plate.is_none() && !blur_behind {
2782             // Ordinary geometry painted after a plate ends its carve-grouping
2783             // window: a recess emitted later must overlay this geometry (the
2784             // fallback path), not shade beneath it inside the plate's draw.
2785             if dbg_plates && !plate_stack.is_empty() {
2786                 *dbg_closed_by.entry(prim_kind(&item.prim)).or_insert(0) += plate_stack.len();
2787             }
2788             plate_stack.clear();
2789             if let Some(last) = batches.last_mut() {
2790                 if last.plate.is_none()
2791                     && last.scissor == item.clip
2792                     && last.clip_rrect == item.clip_rrect
2793                     && last.end == start
2794                 {
2795                     last.end = end;
2796                     continue;
2797                 }
2798             }
2799         }
2800         if promoted {
2801             plate_stack.clear();
2802         }
2803         batches.push(DlBatch { scissor: item.clip, clip_rrect: item.clip_rrect, start, end, plate, blur_behind });
2804         if let Some(prect) = made_plate {
2805             plate_stack.push((batches.len() - 1, prect));
2806             if dbg_plates {
2807                 dbg_opened += 1;
2808             }
2809         }
2810     }
2811 
2812     if dbg_plates && (dbg_grouped > 0 || !dbg_fell_back.is_empty()) {
2813         eprintln!(
2814             "plate-dbg: {} carves — {dbg_grouped} grouped (exact CSG), {} overlay fallback",
2815             dbg_grouped + dbg_fell_back.len(),
2816             dbg_fell_back.len(),
2817         );
2818         eprintln!(
2819             "plate-dbg:   {dbg_opened} grouping window(s) opened by a filled plate; closed early by {}",
2820             if dbg_closed_by.is_empty() {
2821                 "nothing".to_string()
2822             } else {
2823                 dbg_closed_by
2824                     .iter()
2825                     .map(|(k, n)| format!("{k}x{n}"))
2826                     .collect::<Vec<_>>()
2827                     .join(", ")
2828             }
2829         );
2830         for line in &dbg_fell_back {
2831             eprintln!("plate-dbg: {line}");
2832         }
2833     }
2834 
2835     (verts, batches, images, features)
2836 }
2837 
2838 /// The push-constant block for a raised SDF-lit plate over `rect` (logical px in,
2839 /// physical px out). Corner radii clamp to the half-extent cap the SDF needs.
2840 ///
2841 /// `shape` is a per-plate corner exponent (`Prim::Plate`'s override); `None`
2842 /// follows the DE-wide `layout::corner_shape`. The span factor follows the
2843 /// exponent actually used, so a circular override (2.0) spans nothing and a
2844 /// half-extent radius lands on a true circle.
2845 #[allow(clippy::too_many_arguments)]
2846 /// The push block of a frosted flat fill promoted to a zero-depth plate: a
2847 /// mode-1 plate with no roll (`t` = 0.001, so the face is exactly the fill),
2848 /// circular corners at the nominal radii, and the fill's own frost recipe in
2849 /// `host.zw` (`Material::from_fill` decodes the sentinel).
2850 fn flat_frost_push(
2851     rect: &crate::scene::layout::Rect,
2852     radii: (f32, f32, f32, f32),
2853     fill: [f32; 4],
2854     scale: f32,
2855     light: [f32; 3],
2856     material: [f32; 4],
2857 ) -> crate::vk::PlatePush {
2858     let mut p = plate_push_raised(rect, radii, 0.0, scale, light, material, false, Some(2.0));
2859     let [fz, fw] = crate::scene::material::Material::from_fill(fill).frost.pack(scale);
2860     p.host[2] = fz;
2861     p.host[3] = fw;
2862     p
2863 }
2864 
2865 fn plate_push_raised(
2866     rect: &crate::scene::layout::Rect,
2867     radii: (f32, f32, f32, f32),
2868     width: f32,
2869     scale: f32,
2870     light: [f32; 3],
2871     material: [f32; 4],
2872     scale_corners: bool,
2873     shape: Option<f32>,
2874 ) -> crate::vk::PlatePush {
2875     // Floored: a rect already shrunk past its padding (a window dragged
2876     // below what its layout can hold) has a NEGATIVE extent here, and
2877     // `clamp(0.0, cap)` with a negative cap is a panic, not a zero radius.
2878     let cap = (rect.width.min(rect.height) * 0.5).max(0.0);
2879     let shape = shape.map_or_else(crate::layout::corner_shape, |n| n.clamp(2.0, 16.0));
2880     // For PLATES (`scale_corners`), widen the corner span by the
2881     // curvature-match factor (see `layout::corner_span_factor`): the diagonal
2882     // curvature radius equals the configured radius, the corner reads as the
2883     // same size as a circular one, and every roll inset ≤ r stays crease-free
2884     // (past the diagonal curvature radius the offset curve the specular band
2885     // follows creases into a visible square corner). Widget-scale overlay
2886     // reliefs (recess/boss/ridge fallbacks) pass false: their radii must MATCH
2887     // the nominal-radius squircles of the widget silhouettes around them, and
2888     // at their few-px roll widths the offset crease is subpixel.
2889     let rscale = if scale_corners { crate::layout::corner_span_factor_for(shape) } else { 1.0 };
2890     crate::vk::PlatePush {
2891         rect: [
2892             (rect.x + rect.width * 0.5) * scale,
2893             (rect.y + rect.height * 0.5) * scale,
2894             rect.width * 0.5 * scale,
2895             rect.height * 0.5 * scale,
2896         ],
2897         radii: [
2898             (radii.0 * rscale).clamp(0.0, cap) * scale,
2899             (radii.1 * rscale).clamp(0.0, cap) * scale,
2900             (radii.2 * rscale).clamp(0.0, cap) * scale,
2901             (radii.3 * rscale).clamp(0.0, cap) * scale,
2902         ],
2903         light: [light[0], light[1], light[2], width * scale],
2904         material,
2905         // Mode-1 semantics: [feature offset, feature count] — no carves yet;
2906         // the tessellator fills these in as recesses group into this plate.
2907         host: [0.0, 0.0, 0.0, 0.0],
2908         specular_tint: [1.0, 1.0, 1.0, 0.0],
2909         mode: 1.0,
2910         shape,
2911     }
2912 }
2913 
2914 pub fn extra_quad_vertices(
2915     w: &dyn crate::widget::WidgetHost,
2916     qx: f32, qy: f32, qw: f32, qh: f32,
2917     sw: f32, sh: f32,
2918     qc: [f32; 4],
2919     clip_circle: [f32; 3],
2920 ) -> Vec<Vertex> {
2921     let mut verts = Vec::new();
2922     push_extra_quad_vertices(w, qx, qy, qw, qh, sw, sh, qc, clip_circle, &mut verts);
2923     verts
2924 }
2925 
2926 fn get_child_widget_for_quad<'a>(
2927     w: &'a dyn crate::widget::WidgetHost,
2928     qx: f32, qy: f32, qw: f32, qh: f32,
2929 ) -> &'a dyn crate::widget::WidgetHost {
2930     if let Some(pbg) = w.as_any().downcast_ref::<crate::widget::ParametersBg>() {
2931         for s_opt in &pbg.sliders {
2932             if let Some(s) = s_opt {
2933                 let (sx, sy, sww, shh) = s.rect();
2934                 if qx >= sx - 0.1 && qx + qw <= sx + sww + 0.1 && qy >= sy - 0.1 && qy + qh <= sy + shh + 0.1 {
2935                     return s;
2936                 }
2937             }
2938         }
2939         for f_opt in &pbg.float3s {
2940             if let Some(f) = f_opt {
2941                 let (fx, fy, fww, fhh) = f.rect();
2942                 if qx >= fx - 0.1 && qx + qw <= fx + fww + 0.1 && qy >= fy - 0.1 && qy + qh <= fy + fhh + 0.1 {
2943                     return f;
2944                 }
2945             }
2946         }
2947         for sb_opt in &pbg.spinboxes {
2948             if let Some(sb) = sb_opt {
2949                 let (sx, sy, sww, shh) = sb.rect();
2950                 if qx >= sx - 0.1 && qx + qw <= sx + sww + 0.1 && qy >= sy - 0.1 && qy + qh <= sy + shh + 0.1 {
2951                     return sb;
2952                 }
2953             }
2954         }
2955         for btn_opt in &pbg.buttons {
2956             if let Some(btn) = btn_opt {
2957                 let (bx, by, bww, bhh) = btn.rect();
2958                 if qx >= bx - 0.1 && qx + qw <= bx + bww + 0.1 && qy >= by - 0.1 && qy + qh <= by + bhh + 0.1 {
2959                     return btn;
2960                 }
2961             }
2962         }
2963         for ch_opt in &pbg.choices {
2964             if let Some(ch) = ch_opt {
2965                 let (cx, cy, cww, chh) = ch.rect();
2966                 if qx >= cx - 0.1 && qx + qw <= cx + cww + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + chh + 0.1 {
2967                     return ch;
2968                 }
2969             }
2970         }
2971         for t_opt in &pbg.texts {
2972             if let Some(t) = t_opt {
2973                 let (tx, ty, tww, thh) = t.rect();
2974                 if qx >= tx - 0.1 && qx + qw <= tx + tww + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + thh + 0.1 {
2975                     return t;
2976                 }
2977             }
2978         }
2979         for cb_opt in &pbg.toggles {
2980             if let Some(cb) = cb_opt {
2981                 let (cx, cy, cww, chh) = cb.rect();
2982                 if qx >= cx - 0.1 && qx + qw <= cx + cww + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + chh + 0.1 {
2983                     return cb;
2984                 }
2985             }
2986         }
2987         for c_opt in &pbg.colors {
2988             if let Some(c) = c_opt {
2989                 let (cx, cy, cww, chh) = c.rect();
2990                 if qx >= cx - 0.1 && qx + qw <= cx + cww + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + chh + 0.1 {
2991                     return c;
2992                 }
2993             }
2994         }
2995     }
2996     w
2997 }
2998 
2999 pub fn push_extra_quad_vertices(
3000     w: &dyn crate::widget::WidgetHost,
3001     qx: f32, qy: f32, qw: f32, qh: f32,
3002     sw: f32, sh: f32,
3003     qc: [f32; 4],
3004     clip_circle: [f32; 3],
3005     out: &mut Vec<Vertex>,
3006 ) {
3007     if let Some(graph) = w.as_any().downcast_ref::<crate::widget::display::Graph>() {
3008         if graph.is_node_rect(qx, qy, qw, qh) {
3009             let r = crate::layout::graph_node_corner_radius();
3010             let extra_radii = crate::widget::CornerRadii::new(r, r, r, r);
3011             push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, None, out);
3012             return;
3013         }
3014     }
3015 
3016     let target_w = get_child_widget_for_quad(w, qx, qy, qw, qh);
3017     let radii = target_w.corner_radii();
3018     if radii.top_left <= 0.1 && radii.top_right <= 0.1 && radii.bottom_right <= 0.1 && radii.bottom_left <= 0.1 {
3019         out.extend_from_slice(&quad_vertices_with_clip(qx, qy, qw, qh, sw, sh, qc, clip_circle));
3020         if let Some((color, thickness)) = target_w.solid_border() {
3021             let (wx, wy, ww, wh) = target_w.rect();
3022             if (qx - wx).abs() < 0.1 && (qy - wy).abs() < 0.1 && (qw - ww).abs() < 0.1 && (qh - wh).abs() < 0.1 {
3023                 push_plate_solid_border_vertices(qx, qy, qw, qh, radii, thickness, sw, sh, color, clip_circle, out);
3024             }
3025         }
3026         return;
3027     }
3028 
3029     let (wx, mut wy, ww, mut wh) = target_w.rect();
3030     let top_room = target_w.label_strip();
3031     wy += top_room;
3032     wh -= top_room;
3033     let extra_radii = crate::widget::CornerRadii::new(
3034         if qx <= wx + 1.5 && qy <= wy + 1.5 { radii.top_left } else { 0.0 },
3035         if qx + qw >= wx + ww - 1.5 && qy <= wy + 1.5 { radii.top_right } else { 0.0 },
3036         if qx + qw >= wx + ww - 1.5 && qy + qh >= wy + wh - 1.5 { radii.bottom_right } else { 0.0 },
3037         if qx <= wx + 1.5 && qy + qh >= wy + wh - 1.5 { radii.bottom_left } else { 0.0 },
3038     );
3039 
3040     push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, None, out);
3041 
3042     if let Some((color, thickness)) = target_w.solid_border() {
3043         let (rx, mut ry, rw, mut rh) = target_w.rect();
3044         let top = target_w.label_strip();
3045         ry += top;
3046         rh -= top;
3047         if (qx - rx).abs() < 0.1 && (qy - ry).abs() < 0.1 && (qw - rw).abs() < 0.1 && (qh - rh).abs() < 0.1 {
3048             push_plate_solid_border_vertices(qx, qy, qw, qh, radii, thickness, sw, sh, color, clip_circle, out);
3049         }
3050     }
3051 }
3052 
3053 pub fn extra_quad_vertices_clipped(
3054     w: &dyn crate::widget::WidgetHost,
3055     qx: f32, qy: f32, qw: f32, qh: f32,
3056     sw: f32, sh: f32,
3057     qc: [f32; 4],
3058     clip: (f32, f32, f32, f32),
3059     clip_circle: [f32; 3],
3060 ) -> Vec<Vertex> {
3061     let mut verts = Vec::new();
3062     push_extra_quad_vertices_clipped(w, qx, qy, qw, qh, sw, sh, qc, clip, clip_circle, &mut verts);
3063     verts
3064 }
3065 
3066 pub fn push_extra_quad_vertices_clipped(
3067     w: &dyn crate::widget::WidgetHost,
3068     qx: f32, qy: f32, qw: f32, qh: f32,
3069     sw: f32, sh: f32,
3070     qc: [f32; 4],
3071     clip: (f32, f32, f32, f32),
3072     clip_circle: [f32; 3],
3073     out: &mut Vec<Vertex>,
3074 ) {
3075     if let Some(graph) = w.as_any().downcast_ref::<crate::widget::display::Graph>() {
3076         if graph.is_node_rect(qx, qy, qw, qh) {
3077             let r = crate::layout::graph_node_corner_radius();
3078             let extra_radii = crate::widget::CornerRadii::new(r, r, r, r);
3079             push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, Some(clip), out);
3080             return;
3081         }
3082     }
3083 
3084     let target_w = get_child_widget_for_quad(w, qx, qy, qw, qh);
3085     let radii = target_w.corner_radii();
3086     if radii.top_left <= 0.1 && radii.top_right <= 0.1 && radii.bottom_right <= 0.1 && radii.bottom_left <= 0.1 {
3087         let (cx0, cy0, cx1, cy1) = clip;
3088         let ix0 = qx.max(cx0);
3089         let iy0 = qy.max(cy0);
3090         let ix1 = (qx + qw).min(cx1);
3091         let iy1 = (qy + qh).min(cy1);
3092         if ix1 <= ix0 || iy1 <= iy0 {
3093             return;
3094         }
3095         out.extend_from_slice(&quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, sw, sh, qc, clip_circle));
3096         if let Some((color, thickness)) = target_w.solid_border() {
3097             let (wx, wy, ww, wh) = target_w.rect();
3098             if (qx - wx).abs() < 0.1 && (qy - wy).abs() < 0.1 && (qw - ww).abs() < 0.1 && (qh - wh).abs() < 0.1 {
3099                 push_plate_solid_border_vertices(qx, qy, qw, qh, radii, thickness, sw, sh, color, clip_circle, out);
3100             }
3101         }
3102         return;
3103     }
3104 
3105     let (wx, mut wy, ww, mut wh) = target_w.rect();
3106     let top_room = target_w.label_strip();
3107     wy += top_room;
3108     wh -= top_room;
3109     let extra_radii = crate::widget::CornerRadii::new(
3110         if qx <= wx + 1.5 && qy <= wy + 1.5 { radii.top_left } else { 0.0 },
3111         if qx + qw >= wx + ww - 1.5 && qy <= wy + 1.5 { radii.top_right } else { 0.0 },
3112         if qx + qw >= wx + ww - 1.5 && qy + qh >= wy + wh - 1.5 { radii.bottom_right } else { 0.0 },
3113         if qx <= wx + 1.5 && qy + qh >= wy + wh - 1.5 { radii.bottom_left } else { 0.0 },
3114     );
3115 
3116     push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, Some(clip), out);
3117 
3118     if let Some((color, thickness)) = target_w.solid_border() {
3119         let (rx, mut ry, rw, mut rh) = target_w.rect();
3120         let top = target_w.label_strip();
3121         ry += top;
3122         rh -= top;
3123         if (qx - rx).abs() < 0.1 && (qy - ry).abs() < 0.1 && (qw - rw).abs() < 0.1 && (qh - rh).abs() < 0.1 {
3124             push_plate_solid_border_vertices(qx, qy, qw, qh, radii, thickness, sw, sh, color, clip_circle, out);
3125         }
3126     }
3127 }
3128 
3129 pub fn circle_vertices(
3130     cx: f32, cy: f32, r: f32,
3131     sw: f32, sh: f32,
3132     color: [f32; 4],
3133     segments: usize,
3134     clip_circle: [f32; 3],
3135 ) -> Vec<Vertex> {
3136     let mut verts = Vec::new();
3137     for i in 0..segments {
3138         let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
3139         let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
3140         let x0 = cx;
3141         let y0 = cy;
3142         let x1 = cx + r * theta1.cos();
3143         let y1 = cy + r * theta1.sin();
3144         let x2 = cx + r * theta2.cos();
3145         let y2 = cy + r * theta2.sin();
3146         
3147         let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
3148         let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
3149         let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
3150         let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
3151         let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
3152         let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
3153         
3154         verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
3155         verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
3156         verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
3157     }
3158     verts
3159 }
3160 
3161 pub fn circle_border_vertices(
3162     cx: f32, cy: f32, r: f32,
3163     thickness: f32,
3164     sw: f32, sh: f32,
3165     color: [f32; 4],
3166     segments: usize,
3167     clip_circle: [f32; 3],
3168 ) -> Vec<Vertex> {
3169     let mut verts = Vec::new();
3170     for i in 0..segments {
3171         let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
3172         let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
3173         
3174         let x0 = cx + (r - thickness) * theta1.cos();
3175         let y0 = cy + (r - thickness) * theta1.sin();
3176         let x1 = cx + r * theta1.cos();
3177         let y1 = cy + r * theta1.sin();
3178         
3179         let x2 = cx + r * theta2.cos();
3180         let y2 = cy + r * theta2.sin();
3181         let x3 = cx + (r - thickness) * theta2.cos();
3182         let y3 = cy + (r - thickness) * theta2.sin();
3183         
3184         let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
3185         let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
3186         let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
3187         let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
3188         
3189         verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
3190         verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
3191         verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
3192         
3193         verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
3194         verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
3195         verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
3196     }
3197     verts
3198 }
3199 
3200 pub fn arc_background_vertices(
3201     cx: f32, cy: f32, r: f32,
3202     thickness: f32,
3203     start_angle: f32, end_angle: f32,
3204     sw: f32, sh: f32,
3205     color: [f32; 4],
3206     segments: usize,
3207     clip_circle: [f32; 3],
3208 ) -> Vec<Vertex> {
3209     let mut verts = Vec::new();
3210     push_arc_background_vertices(cx, cy, r, thickness, start_angle, end_angle, sw, sh, color, segments, clip_circle, &mut verts);
3211     verts
3212 }
3213 
3214 /// A ring band with radial Gouraud shading: two sub-bands (inner rim → crest
3215 /// centerline, crest → outer rim) whose vertex colors interpolate across the
3216 /// stroke — the rounded-bevel profile — plus the half-px alpha feathers at
3217 /// both true rims (colors matched to the adjacent band, so no seams).
3218 #[allow(clippy::too_many_arguments)]
3219 pub fn push_arc_shaded_vertices(
3220     cx: f32, cy: f32, r: f32,
3221     thickness: f32,
3222     start_angle: f32, end_angle: f32,
3223     sw: f32, sh: f32,
3224     inner: [f32; 4], crest: [f32; 4], outer: [f32; 4],
3225     segments: usize,
3226     clip_circle: [f32; 3],
3227     out: &mut Vec<Vertex>,
3228 ) {
3229     let f = 0.5f32.min(thickness * 0.25);
3230     let r_out = r;
3231     let r_in = (r - thickness).max(0.0);
3232     let r_mid = (r_in + r_out) / 2.0;
3233     let fade_in = [inner[0], inner[1], inner[2], 0.0];
3234     let fade_out = [outer[0], outer[1], outer[2], 0.0];
3235     // (inner radius, outer radius, color at inner edge, color at outer edge)
3236     let bands = [
3237         ((r_in - f).max(0.0), r_in + f, fade_in, inner),
3238         (r_in + f, r_mid, inner, crest),
3239         (r_mid, r_out - f, crest, outer),
3240         (r_out - f, r_out + f, outer, fade_out),
3241     ];
3242     for i in 0..segments {
3243         let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
3244         let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
3245         let (c1, s1) = (theta1.cos(), theta1.sin());
3246         let (c2, s2) = (theta2.cos(), theta2.sin());
3247         for &(ra, rb, ca, cb) in &bands {
3248             if rb <= ra {
3249                 continue;
3250             }
3251             let p = |rad: f32, c: f32, s: f32| -> [f32; 2] {
3252                 [((cx + rad * c) / sw) * 2.0 - 1.0, 1.0 - ((cy + rad * s) / sh) * 2.0]
3253             };
3254             let (i1, o1) = (p(ra, c1, s1), p(rb, c1, s1));
3255             let (i2, o2) = (p(ra, c2, s2), p(rb, c2, s2));
3256             out.push(Vertex { position: i1, color: ca, clip_circle });
3257             out.push(Vertex { position: o1, color: cb, clip_circle });
3258             out.push(Vertex { position: o2, color: cb, clip_circle });
3259             out.push(Vertex { position: i1, color: ca, clip_circle });
3260             out.push(Vertex { position: o2, color: cb, clip_circle });
3261             out.push(Vertex { position: i2, color: ca, clip_circle });
3262         }
3263     }
3264 }
3265 
3266 pub fn push_arc_background_vertices(
3267     cx: f32, cy: f32, r: f32,
3268     thickness: f32,
3269     start_angle: f32, end_angle: f32,
3270     sw: f32, sh: f32,
3271     color: [f32; 4],
3272     segments: usize,
3273     clip_circle: [f32; 3],
3274     out: &mut Vec<Vertex>,
3275 ) {
3276     // The stroke band [r - thickness, r], with a half-px alpha ramp on each rim
3277     // (Gouraud across thin edge bands) so curved edges resolve smoothly instead
3278     // of hard-stepping — the poor-man's AA the flat pipeline doesn't provide.
3279     let f = 0.5f32.min(thickness * 0.25);
3280     let r_in = (r - thickness).max(0.0);
3281     // (inner radius, outer radius, alpha at inner rim, alpha at outer rim)
3282     let bands = [
3283         ((r_in - f).max(0.0), r_in + f, 0.0, color[3]),
3284         (r_in + f, r - f, color[3], color[3]),
3285         (r - f, r + f, color[3], 0.0),
3286     ];
3287     for i in 0..segments {
3288         let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
3289         let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
3290         let (c1, s1) = (theta1.cos(), theta1.sin());
3291         let (c2, s2) = (theta2.cos(), theta2.sin());
3292         for &(ra, rb, aa, ab) in &bands {
3293             if rb <= ra {
3294                 continue;
3295             }
3296             let ca = [color[0], color[1], color[2], aa];
3297             let cb = [color[0], color[1], color[2], ab];
3298             let p = |rad: f32, c: f32, s: f32| -> [f32; 2] {
3299                 [((cx + rad * c) / sw) * 2.0 - 1.0, 1.0 - ((cy + rad * s) / sh) * 2.0]
3300             };
3301             let (i1, o1) = (p(ra, c1, s1), p(rb, c1, s1));
3302             let (i2, o2) = (p(ra, c2, s2), p(rb, c2, s2));
3303             out.push(Vertex { position: i1, color: ca, clip_circle });
3304             out.push(Vertex { position: o1, color: cb, clip_circle });
3305             out.push(Vertex { position: o2, color: cb, clip_circle });
3306             out.push(Vertex { position: i1, color: ca, clip_circle });
3307             out.push(Vertex { position: o2, color: cb, clip_circle });
3308             out.push(Vertex { position: i2, color: ca, clip_circle });
3309         }
3310     }
3311 }
3312 
3313 #[derive(Debug, Clone)]
3314 pub struct WindowSettings {
3315     pub title: String,
3316     pub app_id: String,
3317     pub width: u32,
3318     pub height: u32,
3319     pub fullscreen: bool,
3320     pub min_size: Option<(u32, u32)>,
3321 }
3322 
3323 /// A compositor-side window operation requested by the app: an interactive
3324 /// move or resize grab. Returned from [`Application::take_window_action`];
3325 /// the runner executes it with the serial of the most recent pointer press.
3326 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
3327 pub enum WindowAction {
3328     Move,
3329     Resize(xdg_toplevel::ResizeEdge),
3330 }
3331 
3332 // Re-export the wlr-layer-shell types apps need to describe a layer surface.
3333 pub use smithay_client_toolkit::shell::wlr_layer::{
3334     Anchor as LayerAnchor, KeyboardInteractivity as LayerKeyboardInteractivity, Layer as LayerKind,
3335 };
3336 
3337 /// Opt-in configuration for running an [`Application`] on a wlr-layer-shell
3338 /// surface (panels, overlays, notifications) instead of an xdg toplevel.
3339 /// Return one from [`Application::layer`] to select layer-shell.
3340 #[derive(Debug, Clone)]
3341 pub struct LayerSettings {
3342     pub layer: LayerKind,
3343     pub anchor: LayerAnchor,
3344     pub exclusive_zone: i32,
3345     pub keyboard_interactivity: LayerKeyboardInteractivity,
3346     /// (top, right, bottom, left) margins in logical pixels.
3347     pub margin: (i32, i32, i32, i32),
3348     pub namespace: String,
3349 }
3350 
3351 #[derive(Debug, Clone, Copy, PartialEq)]
3352 pub struct LogicalPosition {
3353     pub x: f32,
3354     pub y: f32,
3355 }
3356 
3357 impl LogicalPosition {
3358     pub fn new(x: f32, y: f32) -> Self {
3359         Self { x, y }
3360     }
3361 }
3362 
3363 #[derive(Debug, Clone, Copy, PartialEq)]
3364 pub struct LogicalSize {
3365     pub width: f32,
3366     pub height: f32,
3367 }
3368 
3369 impl LogicalSize {
3370     pub fn new(width: f32, height: f32) -> Self {
3371         Self { width, height }
3372     }
3373 }
3374 
3375 pub struct RenderContext<'a> {
3376     pub font_system: &'a mut FontSystem,
3377 }
3378 
3379 pub trait Application: Sized + 'static {
3380     type Message: Send + Clone + 'static;
3381 
3382     fn new(qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self;
3383     fn settings(&self) -> WindowSettings;
3384     /// Return `Some(..)` to run on a wlr-layer-shell surface (overlay/panel)
3385     /// instead of an xdg toplevel. Defaults to `None` (a normal window).
3386     fn layer(&self) -> Option<LayerSettings> {
3387         None
3388     }
3389     /// Declare the window a UTILITY window: a tool whose shape is decided by
3390     /// its contents. The compositor then never dictates a size to it (every
3391     /// configure is the "you choose" 0x0 — [`WindowSettings::width`]/`height`
3392     /// become the surface's own initial size), offers no resize affordance
3393     /// (the whole border band moves the window), and never saves geometry
3394     /// for it, so a stale remembered size can't be restored over what the
3395     /// app asks for. Declared over the cce window-management protocol at
3396     /// window creation; on a compositor too old to know the request this is
3397     /// silently a plain floating window. Defaults to `false`.
3398     fn utility(&self) -> bool {
3399         false
3400     }
3401     /// Declare the window the DESKTOP-GRID layer (zcce set_grid): the
3402     /// compositor world-anchors the surface to the virtual desktop and
3403     /// pans/zooms it per frame like window content; the app renders only
3404     /// when handed a patch (see [`Application::grid_patch`]). The surface
3405     /// becomes input-transparent and lives behind all windows. Needs
3406     /// manager v6; on an older compositor the declaration is skipped.
3407     /// Defaults to `false`.
3408     fn grid(&self) -> bool {
3409         false
3410     }
3411     /// A grid patch to render (grid apps only): virtual origin (`x`, `y`),
3412     /// virtual size (`w`, `h`), and `scale` surface px per virtual unit.
3413     /// Called right before the frame that must show it; the runner has
3414     /// already resized the surface to `(w*scale, h*scale)` and acks the
3415     /// patch so the coming commit is latched at the new anchor.
3416     fn grid_patch(&mut self, _x: f64, _y: f64, _w: f64, _h: f64, _scale: f64) {}
3417     fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool);
3418     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool);
3419     /// How long the runner may sleep between `tick`s while the window is
3420     /// idle — nothing to draw, no animation, no key held, no frame callback
3421     /// outstanding. `None` (the default) lets it sleep until a Wayland
3422     /// event or a message on the app's calloop `Sender` arrives, bounded by
3423     /// [`IDLE_DISPATCH`]. Override with `Some` ONLY if your `tick` polls
3424     /// something the loop cannot see — a `std::sync::mpsc` receiver drained
3425     /// in `tick`, say — because with the default that poll waits for the
3426     /// next unrelated event. The better fix is to send through the calloop
3427     /// `Sender` handed to `new`, which wakes the loop by itself.
3428     fn idle_poll_interval(&self) -> Option<std::time::Duration> {
3429         None
3430     }
3431     /// On-top overlay quads drawn after the display list and its text (e.g. the status bar's
3432     /// tray-hover highlights). Deliberately separate from the single paint path.
3433     fn overlay_quads(&mut self, _quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: LogicalSize, _scale: f64) {}
3434     fn input_regions(&self) -> Option<Vec<(i32, i32, i32, i32)>> {
3435         None
3436     }
3437 
3438     /// Transparent overflow rim, in logical px, on the RIGHT and BOTTOM of
3439     /// the window. Non-zero opts into buffer-larger-than-geometry mode: the
3440     /// runner sizes the surface `margin` wider/taller than the configured
3441     /// window size, publishes the top-left rect as the xdg window geometry
3442     /// (what the compositor tiles, borders, and snaps) and an input region of
3443     /// the frame plus any open popover rects — an overhanging menu stays
3444     /// clickable while empty rim falls through to whatever is behind.
3445     ///
3446     /// Right/bottom ONLY, deliberately: the surface grows away from its
3447     /// origin, so the frame never moves relative to the surface and pointer
3448     /// coordinates stay valid across the resize (a leading rim shifts the
3449     /// surface under an unmoved cursor, and the compositor's stale pointer
3450     /// state then drops the very next click). Frame coords == surface coords:
3451     /// no input translation, no paint shift — the app's only obligation is to
3452     /// lay out against the frame (`display_list`'s `size` minus the margin);
3453     /// content emitted past the frame edge renders in the rim instead of
3454     /// clipping at the buffer edge.
3455     ///
3456     /// The value may change at runtime (return the popover overhang while a
3457     /// menu is open, 0 otherwise): the engine re-derives the surface from the
3458     /// stored frame and resizes on drift. Quantize the answer (e.g. 64px
3459     /// steps) so an animating popover doesn't resize the surface per frame.
3460     /// xdg toplevels only (layer surfaces ignore it).
3461     fn overflow_margin(&self) -> u32 {
3462         0
3463     }
3464 
3465     fn desired_size(&self) -> Option<(u32, u32)> {
3466         None
3467     }
3468     
3469     fn ui_context(&self) -> Option<&crate::context::UiContext> {
3470         None
3471     }
3472 
3473     fn ui_context_mut(&mut self) -> Option<&mut crate::context::UiContext> {
3474         None
3475     }
3476 
3477     /// Whether a left-press at (px, py) should start a compositor window drag. Every root
3478     /// root plate container is dissolved (Phase 6), so the default is "no" — apps that want
3479     /// drag-anywhere override this with `ctx.drag_allowed_at(px, py)`.
3480     fn is_movable_root_plate_at(&self, _px: f32, _py: f32) -> bool {
3481         false
3482     }
3483     
3484     fn clear_color(&self) -> [f32; 4] {
3485         [0.0, 0.0, 0.0, 0.0]
3486     }
3487 
3488     fn register_sources(&mut self, _handle: &calloop::LoopHandle<'_, EngineState<Self>>) {}
3489 
3490     fn adjust_size(&self, width: f32, height: f32) -> (f32, f32) {
3491         (width, height)
3492     }
3493     
3494     /// Mime types this app accepts from a drag, in the app's own preference
3495     /// order (the source's order is ignored — a browser lists `text/html`
3496     /// before `text/uri-list` and which is more useful is the app's call).
3497     /// The default is empty: the app accepts nothing and drags over it read
3498     /// as "can't drop here", which is what every client did before drops
3499     /// existed. Opting in also requires [`Application::handle_drop`].
3500     fn drop_mimes(&self) -> &'static [&'static str] {
3501         &[]
3502     }
3503 
3504     /// A completed drop: `data` is everything the source wrote for `mime`,
3505     /// and `pos` is where it was released in the app's logical coordinates.
3506     /// Runs on the main loop, after the transfer finished — this is not the
3507     /// place to block, since the compositor is waiting on the next frame.
3508     fn handle_drop(
3509         &mut self,
3510         _mime: &str,
3511         _data: &[u8],
3512         _pos: LogicalPosition,
3513         _needs_rebuild: &mut bool,
3514     ) {
3515     }
3516 
3517     fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool);
3518     fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message>;
3519     fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool);
3520     /// Trackpad pinch (zwp_pointer_gestures pinch). `factor` is the scale
3521     /// change SINCE THE LAST update (1.0 = no change, >1 = fingers spreading),
3522     /// so direct-manipulation zoom is `content_scale *= factor`. Return true
3523     /// to consume; returning false falls back to the engine's legacy
3524     /// synthesis — a ctrl+wheel PixelDelta sized for the graph's zoom mapping
3525     /// (`y = (factor-1)/0.015`) — so ctrl-scroll-zoom surfaces keep working
3526     /// without implementing this.
3527     fn handle_pinch(&mut self, _factor: f32, _pos: LogicalPosition, _needs_rebuild: &mut bool) -> bool {
3528         false
3529     }
3530     fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message>;
3531 
3532     /// Undo, after the focused widget declined the chord (a text box that is
3533     /// editing takes it for its own typing). Return true when something was
3534     /// undone; false lets the key fall through to `handle_key_input` like any
3535     /// other. The chords are `undo` / `redo` in `input.kdl` (cce-ui domain
3536     /// defaults `ctrl+z` / `ctrl+shift+z`), resolved once at startup. Build
3537     /// the history on `cce_ui::history::History`.
3538     fn undo(&mut self, _needs_rebuild: &mut bool) -> bool {
3539         false
3540     }
3541 
3542     /// Redo — see [`undo`](Self::undo).
3543     fn redo(&mut self, _needs_rebuild: &mut bool) -> bool {
3544         false
3545     }
3546 
3547     /// Opt into the toolkit's keyboard navigation in plate terms: Tab and
3548     /// Shift+Tab move focus to the next / previous plate or well in reading
3549     /// order (`UiContext::focus_step`), a press (Enter / Space) acts on the
3550     /// focused plate, a well opens for typing when focused. Default false: an
3551     /// app that routes Tab itself (a terminal, a web view, its own field
3552     /// order) is undisturbed. See "Plates, wells and seams" in `CLAUDE.md`.
3553     fn plate_navigation(&self) -> bool {
3554         false
3555     }
3556 
3557     /// Keyboard focus just moved by the toolkit's Tab traversal. An app that
3558     /// caches its geometry until its own rebuild flag (relief carves collected
3559     /// in a view pass, widget lists built on layout) raises that flag here, so
3560     /// the new ring is drawn; an app that paints fresh every frame needs
3561     /// nothing. Default: nothing.
3562     fn focus_stepped(&mut self) {}
3563     /// Keyboard focus entered/left the window (the compositor keyboard-focuses
3564     /// the focused window, so this is the "am I the focused window" signal —
3565     /// e.g. for focus-dependent chrome). Default: ignore.
3566     fn handle_focus_change(&mut self, _focused: bool, _needs_rebuild: &mut bool) {}
3567 
3568     fn custom_vertices(&mut self, _verts: &mut Vec<Vertex>, _size: LogicalSize, _scale: f64) {}
3569 
3570     /// The frame's geometry, drawn via one batched, GPU-scissor-clipped pass (the single
3571     /// paint path). Every rendering app implements this — the legacy `view*` sinks are gone;
3572     /// `None` yields an empty frame. Overlays ([`overlay_quads`](Application::overlay_quads))
3573     /// and [`custom_vertices`](Application::custom_vertices) still go through their own paths;
3574     /// text renders from the list when [`display_list_text`](Application::display_list_text)
3575     /// opts in. Receives the frame's logical size and HiDPI scale. Typically implemented as
3576     /// `Some(cce_ui::scene::painter::paint_tree(&self.ui_context, &self.root))`.
3577     fn display_list(&mut self, _size: LogicalSize, _scale: f64) -> Option<crate::scene::paint::DisplayList> {
3578         None
3579     }
3580 
3581     /// Opt in to render the display list's `Prim::Text` items through the glyph pass
3582     /// (shaped via the shared buffer cache, clipped to the item clip ∩ the prim bounds). An
3583     /// app's ENTIRE frame — geometry and text — is then one
3584     /// [`display_list`](Application::display_list). Default `false` draws no text (an app that
3585     /// only draws geometry, or none at all).
3586     ///
3587     /// Display-list text gets the same popover-occlusion clamp as the legacy `text_areas`
3588     /// mapping (`popover_occlusion_clamp`, driven by `ui_context().active_popovers`), so an
3589     /// open popover's plate clips list text beneath it on both paths.
3590     fn display_list_text(&self) -> bool {
3591         false
3592     }
3593 
3594     /// Opt into system fonts in the ENGINE's render `FontSystem` (the one that shapes
3595     /// display-list text and rasterizes every glyph at prepare time). Default `false`: the
3596     /// render FontSystem loads only the bundled CCE fonts, and text asking for a family that
3597     /// exists only among installed system fonts is silently invisible — buffers shaped
3598     /// app-side against a system-fonts `FontSystem` carry fontdb face IDs the engine's
3599     /// database doesn't have (the cce-colors Phase 6e bug). An app whose UI must render
3600     /// arbitrary installed families (the font picker) returns `true`; its own `FontSystem`,
3601     /// if it keeps one for measurement, should be `create_font_system_with_system_fonts()`
3602     /// so both databases load identically. Consulted once, at GPU init.
3603     fn load_system_fonts(&self) -> bool {
3604         false
3605     }
3606 
3607     /// Called once, right after the renderer is created and before the first
3608     /// frame: create persistent renderer resources here (3D meshes via
3609     /// [`VkRenderer::create_mesh`]). Most 2D apps never need this.
3610     fn renderer_init(&mut self, _renderer: &mut VkRenderer) {}
3611 
3612     /// Direct renderer staging, called every frame after the engine's own text
3613     /// prep and immediately before the frame is drawn: stage 3D scene panes
3614     /// (`stage_scene`), path-traced panes (`stage_rt`), flush mesh updates, or
3615     /// prepare app-shaped text (`prepare_text` — an app that returns `false`
3616     /// from [`display_list_text`](Application::display_list_text) fully owns
3617     /// the renderer's text state, the engine never touches it). Return `true`
3618     /// to request another frame immediately (e.g. while a path tracer is still
3619     /// accumulating samples).
3620     fn stage_renderer(&mut self, _renderer: &mut VkRenderer, _size: LogicalSize, _scale: f64) -> bool {
3621         false
3622     }
3623 
3624     /// The surface was resized (or the scale factor changed): `width`/`height`
3625     /// are the new logical size. The renderer has already been resized; use
3626     /// this for stateful relayout that can't wait for the next paint callback.
3627     fn handle_resize(&mut self, _width: f32, _height: f32, _scale: f64) {}
3628 
3629     /// Whether the runner's built-in client-side decorations apply: the
3630     /// titlebar move band, the movable-root plate drag regions, and — when
3631     /// [`csd_resize_borders`](Application::csd_resize_borders) is also on —
3632     /// the rect-edge resize grabs and their edge cursors. Return `false` for a
3633     /// window whose chrome doesn't follow its rect (e.g. a circular pane) and
3634     /// drive moves/resizes yourself via
3635     /// [`take_window_action`](Application::take_window_action).
3636     fn standard_csd(&self) -> bool {
3637         true
3638     }
3639 
3640     /// Whether the standard CSD claims the outer 8px of the surface as resize
3641     /// grabs (with matching edge cursors). Off by default: under the cce
3642     /// compositor the server already provides a resize band just *outside* the
3643     /// window, so enabling this gives a window two adjacent 8px gutters driven
3644     /// by different code paths — and only the compositor's snaps to the
3645     /// desktop grid. It also costs the app clicks, since a press inside the
3646     /// band starts a grab and never reaches the widgets underneath.
3647     ///
3648     /// Turn it on for a window that must be resizable by its own edges under a
3649     /// compositor that provides no such affordance. Only consulted when
3650     /// [`standard_csd`](Application::standard_csd) is on.
3651     fn csd_resize_borders(&self) -> bool {
3652         false
3653     }
3654 
3655     /// Whether the standard CSD reserves an implicit title-bar strip (`y` in `[8, 32)`) as a
3656     /// drag-to-move handle. Opt-in: off by default, so a window has no title bar and is moved
3657     /// through the compositor (or via explicitly-declared handles —
3658     /// [`is_movable_root_plate_at`](Application::is_movable_root_plate_at)); nothing is
3659     /// implicitly draggable. An app with an actual title bar returns `true`. Separate from
3660     /// [`standard_csd`](Application::standard_csd), which also gates the resize borders, and
3661     /// only consulted when `standard_csd()` is on.
3662     fn csd_titlebar_move(&self) -> bool {
3663         false
3664     }
3665 
3666     /// Override the pointer cursor at (x, y). `None` falls back to the
3667     /// runner's standard CSD edge cursors (or `Default` when
3668     /// [`standard_csd`](Application::standard_csd) is off).
3669     fn cursor_icon(&self, _x: f32, _y: f32) -> Option<CursorIcon> {
3670         None
3671     }
3672 
3673     /// Polled after each pointer frame is dispatched: return a
3674     /// [`WindowAction`] to start an interactive move/resize grab with the
3675     /// serial of the most recent pointer press. This is take-semantics — the
3676     /// implementation should clear its pending action when returning it.
3677     fn take_window_action(&mut self) -> Option<WindowAction> {
3678         None
3679     }
3680 
3681     /// Called once when the event loop ends (window closed, app-requested
3682     /// exit): last-chance work like autosave. The surface is still alive.
3683     fn on_exit(&mut self) {}
3684 }
3685 
3686 pub struct PressedKey {
3687     pub logical_key: Key,
3688     pub text: Option<String>,
3689     pub first_pressed: Instant,
3690     pub last_repeated: Instant,
3691 }
3692 
3693 fn is_repeatable_key(key: &Key) -> bool {
3694     match key {
3695         Key::Named(NamedKey::Backspace) |
3696         Key::Named(NamedKey::Delete) |
3697         Key::Named(NamedKey::ArrowLeft) |
3698         Key::Named(NamedKey::ArrowRight) |
3699         Key::Named(NamedKey::ArrowUp) |
3700         Key::Named(NamedKey::ArrowDown) |
3701         Key::Named(NamedKey::Home) |
3702         Key::Named(NamedKey::End) |
3703         Key::Character(_) => true,
3704         _ => false,
3705     }
3706 }
3707 
3708 /// Default cap on the runner's idle sleep — see `Application::idle_poll_interval`.
3709 pub const IDLE_DISPATCH: std::time::Duration = std::time::Duration::from_millis(1000);
3710 
3711 pub struct EngineState<A: Application> {
3712     pub registry_state: RegistryState,
3713     pub compositor_state: CompositorState,
3714     pub xdg_shell_state: XdgShell,
3715     pub layer_shell_state: Option<LayerShell>,
3716     pub shm_state: Shm,
3717     pub seat_state: SeatState,
3718     pub output_state: OutputState,
3719     pub seats: Vec<wl_seat::WlSeat>,
3720     pub pointer: Option<ThemedPointer>,
3721     pub keyboard: Option<wl_keyboard::WlKeyboard>,
3722 
3723     pub window: Option<XdgWindow>,
3724     pub layer_surface: Option<LayerSurface>,
3725     pub surface: Option<wl_surface::WlSurface>,
3726     
3727     pub inner: Option<A>,
3728     
3729     pub renderer: Option<VkRenderer>,
3730     pub font_system: Option<FontSystem>,
3731     pub swash_cache: cosmic_text::SwashCache,
3732     
3733     pub scale_factor: f64,
3734     /// The buffer scale last sent to the surface. Updated in [`Self::render`],
3735     /// paired with the present that commits a matching-size buffer — never on
3736     /// the scale event itself, which races in-flight presents of old buffers.
3737     pub committed_buffer_scale: i32,
3738     /// Outputs the surface has entered and not left. Used by
3739     /// `scale_factor_changed` to reject the SCTK no-outputs fallback: on
3740     /// suspend/resume the DRM connector is destroyed and re-created, the
3741     /// surface briefly sits on zero (live) outputs, and SCTK reports scale 1.
3742     /// Acting on that report rebuilds the buffer at scale-1 size while the
3743     /// surface's latched scale can still be 2 — a fatal `invalid_size`
3744     /// protocol error for odd-sized surfaces (the status bar crash-loop on
3745     /// every resume) and a silently HALF-SIZE window for even-sized ones
3746     /// (the compositor reads buffer/scale as a self-resize and the halving
3747     /// sticks, compounding per resume).
3748     pub entered_outputs: Vec<wl_output::WlOutput>,
3749     pub logical_width: f32,
3750     pub logical_height: f32,
3751     /// The window-frame logical size (surface minus the overflow rim) as of
3752     /// the last configure/desired-size — what the surface is re-derived from
3753     /// when [`Application::overflow_margin`] changes at runtime.
3754     pub frame_logical: (f32, f32),
3755     /// The overflow margin the current surface was actually sized with. Input
3756     /// translation and the dl-text overlay offsets use THIS, never a live
3757     /// `overflow_margin()` read — the app may have changed its answer since.
3758     pub applied_margin: f32,
3759     /// True while the previous frame ran with a nonzero margin — lets the
3760     /// per-frame geometry publish reset state exactly once on deactivation.
3761     pub overflow_was_active: bool,
3762     /// The popover-union rect last sent via zcce set_popover_region, logical
3763     /// surface px; None once a clear has been sent (or never anything).
3764     pub sent_popover_region: Option<(i32, i32, i32, i32)>,
3765 
3766     pub exit: bool,
3767     pub redraw: bool,
3768     pub frame_callback_pending: bool,
3769     /// When the pending frame callback was armed — the starvation fallback's
3770     /// clock (see the render gate in `run`).
3771     pub frame_callback_armed_at: Option<std::time::Instant>,
3772     /// Keep rendering (vsync-paced) briefly after the last genuine dirty frame.
3773     /// Sparse, isolated commits get their frame callbacks serviced multiple
3774     /// compositor frames late (measured 22-128ms on cce-fx, growing per sparse
3775     /// commit), while a continuously committing surface is serviced in one
3776     /// frame (~16ms). A short warm-down keeps interactive sequences (hover,
3777     /// typing, scrolling) in the healthy continuous regime; idle still idles.
3778     pub warm_until: Option<std::time::Instant>,
3779     /// Consecutive renders skipped by the extent gate (pending swapchain size
3780     /// != the size the current logical size and scale call for). Normally 0 or
3781     /// 1; a persistent count means no frame is presenting and deserves a warn.
3782     pub extent_gate_skips: u32,
3783     pub first_configure_received: bool,
3784     pub ctrl_pressed: bool,
3785     /// The `undo` / `redo` chords, resolved from `input.kdl` at startup.
3786     pub undo_chord: String,
3787     /// `focus_next_group` / `focus_prev_group` (input.kdl, cce-ui domain):
3788     /// the plate-navigation group jump, for apps that opt in.
3789     pub group_next_chord: String,
3790     pub group_prev_chord: String,
3791     pub redo_chord: String,
3792     pub shift_pressed: bool,
3793     pub alt_pressed: bool,
3794     pub logo_pressed: bool,
3795     pub pressed_key: Option<PressedKey>,
3796     pub sender: calloop::channel::Sender<A::Message>,
3797     pub current_cursor_icon: Option<CursorIcon>,
3798     pub qh: QueueHandle<EngineState<A>>,
3799     pub just_configured: bool,
3800     pub pointer_gestures: Option<ZwpPointerGesturesV1>,
3801     pub pinch_gesture: Option<ZwpPointerGesturePinchV1>,
3802     /// The cce window-management toplevel handle, held for the window's
3803     /// lifetime once [`Application::utility`] declared the mode.
3804     pub cce_toplevel: Option<crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1>,
3805     /// Latest unrendered grid_patch (serial, x, y, w, h, scale) — a newer
3806     /// event supersedes an unconsumed older one, per protocol.
3807     pub pending_grid_patch: Option<(u32, f64, f64, f64, f64, f64)>,
3808     pub last_pinch_scale: f32,
3809     pub cursor_pos: (f32, f32),
3810     /// Serial of the most recent pointer press, kept for
3811     /// [`Application::take_window_action`] move/resize grabs.
3812     pub last_press_serial: Option<u32>,
3813     /// Mouse buttons currently held, as a bitmask (1 Left / 2 Right /
3814     /// 4 Middle). On pointer Leave mid-gesture the real Release goes to
3815     /// whatever surface takes the pointer next (fullscreen switches, layout
3816     /// animations), so Leave synthesizes releases for the held set — a drag
3817     /// must end, not stay armed and steered by later motion — and only then
3818     /// runs the off-screen hover-clear (which would otherwise corrupt the
3819     /// drag: a ramp key snapped to the graph corner).
3820     pub buttons_down: u32,
3821     /// This frame's display-list text, shaped and held here so the `TextSpan`s built
3822     /// in the render pass can borrow the buffers (Phase 6 —
3823     /// [`Application::display_list_text`]).
3824     pub dl_text_items: Vec<TextItem>,
3825 
3826     /// Drag-and-drop destination state (see [`crate::backend::dnd`]). The
3827     /// manager is absent when the compositor exposes no wl_data_device_manager;
3828     /// every drop path then no-ops.
3829     pub data_device_manager: Option<smithay_client_toolkit::data_device_manager::DataDeviceManagerState>,
3830     pub data_devices: Vec<smithay_client_toolkit::data_device_manager::data_device::DataDevice>,
3831     /// Mime type accepted for the in-flight drag; `None` means the app wants
3832     /// nothing this offer carries, so the drop is declined.
3833     pub drag_mime: Option<String>,
3834     /// Surface-local logical position of the last drag enter/motion — the
3835     /// drop point handed to [`Application::handle_drop`].
3836     pub drag_pos: LogicalPosition,
3837     /// Reader threads post completed drops here; the main loop drains it.
3838     pub drop_tx: Option<calloop::channel::Sender<crate::backend::dnd::DroppedData>>,
3839     /// The offer being read right now, held so it can be finished only once
3840     /// the transfer is actually done (see `dnd::drop_performed`).
3841     pub pending_drop_offer:
3842         Option<smithay_client_toolkit::data_device_manager::data_offer::DragOffer>,
3843     /// The input region last sent to the compositor, so a per-frame
3844     /// [`Application::input_regions`] only costs protocol traffic on change.
3845     pub applied_input_regions: Option<Vec<(i32, i32, i32, i32)>>,
3846 }
3847 
3848 impl<A: Application> EngineState<A> {
3849     pub fn init_gpu(&mut self, conn: &Connection, width_logical: f32, height_logical: f32) {
3850         let s = self.scale_factor as f32;
3851         let pw = (width_logical * s) as u32;
3852         let ph = (height_logical * s) as u32;
3853 
3854         let surface = self.surface.as_ref().expect("surface missing");
3855 
3856         let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
3857         let surface_ptr = surface.id().as_ptr() as *mut std::ffi::c_void;
3858 
3859         let load_system_fonts = self.inner.as_ref().map_or(false, |a| a.load_system_fonts());
3860         // Corner radius 0: runner apps tessellate their own rounded corners.
3861         let renderer =
3862             unsafe { VkRenderer::new(display_ptr, surface_ptr, pw, ph, 0.0) };
3863         self.font_system = Some(if load_system_fonts {
3864             crate::create_font_system_with_system_fonts()
3865         } else {
3866             crate::create_font_system()
3867         });
3868         self.renderer = Some(renderer);
3869         self.logical_width = width_logical;
3870         self.logical_height = height_logical;
3871     }
3872 
3873     /// Buffer scale and physical extent for a logical size under the current
3874     /// scale factor: rounded, then snapped up so the extent divides by the
3875     /// buffer scale (a wl_surface requirement). In forced-scale mode the
3876     /// surface stays at buffer_scale 1 (the compositor believes scale 1).
3877     ///
3878     /// This is the single source of the buffer-size formula: `resize` sizes
3879     /// the swapchain with it and `render` refuses to present any extent that
3880     /// disagrees with it — a mispaired buffer/scale commit is how the resume
3881     /// output bounce halved even-sized windows (buffer at the old scale's
3882     /// size, new scale latched; the compositor reads it as a self-resize).
3883     fn buffer_geometry(scale_factor: f64, w: f32, h: f32) -> (i32, u32, u32) {
3884         let s = if crate::scale::forced_scale().is_some() {
3885             1
3886         } else {
3887             (scale_factor.round() as i32).max(1)
3888         };
3889         let su = s as u32;
3890         let pw = ((w as f64 * scale_factor).round() as u32).max(1).div_ceil(su) * su;
3891         let ph = ((h as f64 * scale_factor).round() as u32).max(1).div_ceil(su) * su;
3892         (s, pw, ph)
3893     }
3894 
3895     pub fn resize(&mut self, w: f32, h: f32) {
3896         let (w, h) = self.inner.as_ref().unwrap().adjust_size(w, h);
3897         if w > 0.0 && h > 0.0 {
3898             self.logical_width = w;
3899             self.logical_height = h;
3900             let (_, pw, ph) = Self::buffer_geometry(self.scale_factor, w, h);
3901             if let Some(ref mut renderer) = self.renderer {
3902                 renderer.resize(pw, ph);
3903             }
3904             let scale = self.scale_factor;
3905             self.inner.as_mut().unwrap().handle_resize(w, h, scale);
3906             self.publish_window_geometry();
3907         }
3908     }
3909 
3910     /// Overflow-margin mode ([`Application::overflow_margin`]): re-publish the
3911     /// window frame — the surface rect inset by the margin — as the xdg window
3912     /// geometry, and an input region of the frame PLUS any open popover rects
3913     /// (an overhanging menu's rows must stay clickable; empty rim still falls
3914     /// through). Applied on every resize and, while the rim is live, every
3915     /// loop (the popover rects animate). Margin back at 0 resets both — a
3916     /// no-op only for apps that never had a rim. (All double-buffered surface
3917     /// state, latched by the next commit.)
3918     fn publish_window_geometry(&mut self) {
3919         let m = self.applied_margin;
3920         let Some(ref window) = self.window else { return };
3921         if m <= 0.0 {
3922             if self.overflow_was_active {
3923                 let gw = (self.logical_width as i32).max(1);
3924                 let gh = (self.logical_height as i32).max(1);
3925                 window.xdg_surface().set_window_geometry(0, 0, gw, gh);
3926                 if let Some(ref surface) = self.surface {
3927                     surface.set_input_region(None);
3928                 }
3929             }
3930             return;
3931         }
3932         // Right/bottom rim: the frame keeps the surface origin — no offset,
3933         // frame coords == surface coords.
3934         let gw = ((self.logical_width - m) as i32).max(1);
3935         let gh = ((self.logical_height - m) as i32).max(1);
3936         window.xdg_surface().set_window_geometry(0, 0, gw, gh);
3937         if let Some(ref surface) = self.surface {
3938             let compositor = self.compositor_state.wl_compositor();
3939             let wl_region = compositor.create_region(&self.qh, ());
3940             wl_region.add(0, 0, gw, gh);
3941             // Open popovers, clamped to the surface.
3942             if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
3943                 for (_id, ptr) in ctx.tree.iter_registered() {
3944                     unsafe {
3945                         let Some(w) = ptr.as_ref() else { continue };
3946                         if !w.visible() {
3947                             continue;
3948                         }
3949                         let Some((px, py, pw, ph)) = w.popover_rect() else { continue };
3950                         let x0 = px.max(0.0) as i32;
3951                         let y0 = py.max(0.0) as i32;
3952                         let x1 = ((px + pw).min(self.logical_width)) as i32;
3953                         let y1 = ((py + ph).min(self.logical_height)) as i32;
3954                         if x1 > x0 && y1 > y0 {
3955                             wl_region.add(x0, y0, x1 - x0, y1 - y0);
3956                         }
3957                     }
3958                 }
3959             }
3960             surface.set_input_region(Some(&wl_region));
3961             wl_region.destroy();
3962         }
3963     }
3964     
3965     /// Report the union of the open popover rects to the compositor
3966     /// (zcce set_popover_region, manager v7), so its window chrome — the
3967     /// overview resize ring — stays out from under an in-surface menu. Sent
3968     /// only on change, and a clear is sent when the last popover closes;
3969     /// rects are clamped to the surface in logical px, the coordinate space
3970     /// the protocol specifies. Popovers animate, so this runs every loop —
3971     /// the change gate is what keeps it quiet.
3972     fn send_popover_region(&mut self) {
3973         let Some(tl) = &self.cce_toplevel else { return };
3974         // Version gate on the MANAGER numbering the resource carries (the
3975         // toplevel inherits its bind version): 7 is where the request
3976         // appeared. An older compositor would kill the client on the
3977         // unknown opcode.
3978         if tl.version() < 7 {
3979             return;
3980         }
3981         let mut union: Option<(f32, f32, f32, f32)> = None;
3982         if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
3983             for (_id, ptr) in ctx.tree.iter_registered() {
3984                 unsafe {
3985                     let Some(w) = ptr.as_ref() else { continue };
3986                     if !w.visible() {
3987                         continue;
3988                     }
3989                     let Some((px, py, pw, ph)) = w.popover_rect() else { continue };
3990                     let (x0, y0) = (px.max(0.0), py.max(0.0));
3991                     let x1 = (px + pw).min(self.logical_width);
3992                     let y1 = (py + ph).min(self.logical_height);
3993                     if x1 <= x0 || y1 <= y0 {
3994                         continue;
3995                     }
3996                     union = Some(match union {
3997                         None => (x0, y0, x1, y1),
3998                         Some((ux0, uy0, ux1, uy1)) => {
3999                             (ux0.min(x0), uy0.min(y0), ux1.max(x1), uy1.max(y1))
4000                         }
4001                     });
4002                 }
4003             }
4004         }
4005         let next = union.map(|(x0, y0, x1, y1)| {
4006             (x0 as i32, y0 as i32, (x1 - x0).ceil() as i32, (y1 - y0).ceil() as i32)
4007         });
4008         if next == self.sent_popover_region {
4009             return;
4010         }
4011         match next {
4012             Some((x, y, w, h)) => tl.set_popover_region(x, y, w, h),
4013             None => tl.set_popover_region(0, 0, 0, 0),
4014         }
4015         self.sent_popover_region = next;
4016     }
4017 
4018     /// The cursor for the pointer at (lx, ly): the app's
4019     /// [`Application::cursor_icon`] override, else the standard-CSD edge
4020     /// cursors (status bars and non-standard-CSD apps fall back to Default).
4021     fn cursor_icon_at(&self, lx: f32, ly: f32) -> CursorIcon {
4022         let inner = self.inner.as_ref().unwrap();
4023         if let Some(icon) = inner.cursor_icon(lx, ly) {
4024             return icon;
4025         }
4026         if inner.settings().app_id.starts_with("cce-status")
4027             || !inner.standard_csd()
4028             || !inner.csd_resize_borders()
4029         {
4030             return CursorIcon::Default;
4031         }
4032         let border = 8.0f32;
4033         if ly < border {
4034             if lx < border {
4035                 CursorIcon::NwResize
4036             } else if lx > self.logical_width - border {
4037                 CursorIcon::NeResize
4038             } else {
4039                 CursorIcon::NResize
4040             }
4041         } else if ly > self.logical_height - border {
4042             if lx < border {
4043                 CursorIcon::SwResize
4044             } else if lx > self.logical_width - border {
4045                 CursorIcon::SeResize
4046             } else {
4047                 CursorIcon::SResize
4048             }
4049         } else if lx < border {
4050             CursorIcon::WResize
4051         } else if lx > self.logical_width - border {
4052             CursorIcon::EResize
4053         } else {
4054             CursorIcon::Default
4055         }
4056     }
4057 
4058     pub fn render(&mut self) {
4059         // Grid patch: resize to the patch's buffer size, tell the app what
4060         // world region this frame covers, and ack — the commit this render
4061         // produces is the one the compositor latches at the new anchor.
4062         if let Some((serial, px, py, pw, ph, pscale)) = self.pending_grid_patch.take() {
4063             self.resize((pw * pscale) as f32, (ph * pscale) as f32);
4064             self.inner.as_mut().unwrap().grid_patch(px, py, pw, ph, pscale);
4065             if let Some(tl) = &self.cce_toplevel {
4066                 tl.ack_grid_patch(serial);
4067             }
4068         }
4069         let logical_w = self.logical_width;
4070         let logical_h = self.logical_height;
4071         let scale_factor = self.scale_factor;
4072 
4073         if let Some(ref surface) = self.surface {
4074             if let Some(regions) = self.inner.as_ref().unwrap().input_regions() {
4075                 // Only re-send when it actually changes. This runs per frame,
4076                 // and a client whose region tracks its content (the desktop
4077                 // grid's items follow every pan) would otherwise create and
4078                 // destroy a wl_region on every frame of a camera flight.
4079                 if self.applied_input_regions.as_deref() != Some(regions.as_slice()) {
4080                     let compositor = self.compositor_state.wl_compositor();
4081                     let wl_region = compositor.create_region(&self.qh, ());
4082                     for &(rx, ry, rw, rh) in &regions {
4083                         wl_region.add(rx, ry, rw, rh);
4084                     }
4085                     surface.set_input_region(Some(&wl_region));
4086                     wl_region.destroy();
4087                     self.applied_input_regions = Some(regions);
4088                 }
4089             }
4090         }
4091         
4092         // 0. Shape every registered widget against the SAME FontSystem the glyph pass draws
4093         // with, before the app builds its frame. A widget's caret/selection/click→index math
4094         // reads per-glyph advances its `prepare_text` records; nothing else calls it on the
4095         // display-list path (the paint walk is `&dyn`, and apps were left to remember —
4096         // cce-list, cce-secrets, and the reference DemoApp all forgot, so their carets fell
4097         // back to `measure_text_width("M")`, an inked extent that drifts off the glyphs).
4098         // The flat path shapes in `layout::render_widget`; apps that hand-shape still work —
4099         // their call and this one hit the same shaped-buffer cache. Pointers are collected
4100         // first so the registry borrow ends before any widget is mutated (the missed-press
4101         // walk dereferences the same registry the same way).
4102         {
4103             let ptrs: Vec<*mut (dyn crate::widget::WidgetHost + 'static)> = self
4104                 .inner
4105                 .as_ref()
4106                 .unwrap()
4107                 .ui_context()
4108                 .map(|ctx| ctx.tree.iter_registered().map(|(_, p)| p).collect())
4109                 .unwrap_or_default();
4110             if !ptrs.is_empty() {
4111                 let fs = self.font_system.as_mut().unwrap();
4112                 for ptr in ptrs {
4113                     unsafe {
4114                         if let Some(w) = ptr.as_mut() {
4115                             w.prepare_text(fs);
4116                         }
4117                     }
4118                 }
4119             }
4120         }
4121 
4122         // 1. The frame's geometry IS the app's display list — the single paint path. Tessellated
4123         // below as one batched, GPU-scissor-clipped pass. An app that draws nothing returns
4124         // `None`, giving an empty frame (the legacy view*/tuple-wrapping path is gone).
4125         let dl = self.inner.as_mut().unwrap()
4126             .display_list(LogicalSize::new(logical_w, logical_h), scale_factor)
4127             .unwrap_or_else(|| crate::scene::paint::PaintCtx::new().finish());
4128 
4129         // 1a. Phase 6 display-list text: shape the list's Text prims through the shared buffer
4130         // cache and hold them for the glyph pass (the TextSpans built below borrow these).
4131         // Clip = the paint walk's item clip ∩ the prim's own bounds, in logical space.
4132         self.dl_text_items.clear();
4133         if self.inner.as_ref().unwrap().display_list_text() {
4134             let fs = self.font_system.as_mut().unwrap();
4135             for item in &dl.items {
4136                 if let crate::scene::paint::Prim::Text { text, x, y, font_size, color, alpha, font, bounds, attrs, layout } = &item.prim {
4137                     let clip = item.clip.map(|c| [c.x, c.y, c.x + c.width, c.y + c.height]);
4138                     let merged = match (clip, *bounds) {
4139                         (Some(a), Some(b)) => Some([a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])]),
4140                         (Some(a), None) => Some(a),
4141                         (None, b) => b,
4142                     };
4143                     // Boxed text (wrap/align) shapes uncached and shifts down by the vertical
4144                     // offset; ordinary labels take the shared cached buffer.
4145                     let (buffer, y_off) = match layout {
4146                         Some(l) => get_text_buffer_laid_out(fs, text, *font_size, font.as_deref(), *attrs, *l),
4147                         None => (get_text_buffer_attrs(fs, text, *font_size, font.as_deref(), *attrs), 0.0),
4148                     };
4149                     self.dl_text_items.push(TextItem {
4150                         buffer,
4151                         x: *x,
4152                         y: *y + y_off,
4153                         color: cosmic_text::Color::rgba(
4154                             color[0],
4155                             color[1],
4156                             color[2],
4157                             (alpha.clamp(0.0, 1.0) * 255.0).round() as u8,
4158                         ),
4159                         bounds: merged,
4160                         clip_circle: item.clip_circle,
4161                         clip_rrect: item.clip_rrect,
4162                     });
4163                 }
4164             }
4165         }
4166 
4167         let (mut verts, mut dl_batches, dl_images, plate_features) = tessellate_display_list(&dl, logical_w, logical_h, scale_factor as f32);
4168         // A pending height-field export (`CCE_HEIGHTMAP`, or an app's
4169         // `scene::heightfield::request`): the plates of THIS frame, sampled
4170         // as the geometry the shader is about to shade.
4171         if let Some(req) = crate::scene::heightfield::take_request() {
4172             let s = scale_factor as f32;
4173             let (pw, ph) = ((logical_w * s).round() as usize, (logical_h * s).round() as usize);
4174             let hf = crate::scene::heightfield::HeightField::from_frame(&dl_batches, &plate_features, pw, ph, s);
4175             let (lo, hi) = hf.range_px();
4176             match crate::scene::heightfield::export_png(&hf, &req.path, req.mm_per_sample) {
4177                 Ok(()) => log::info!(
4178                     "[heightfield] wrote {} ({}x{} px, {:.3}..{:.3} mm, metric {})",
4179                     req.path.display(), pw, ph, lo / hf.px_per_mm, hi / hf.px_per_mm, hf.source.as_str()
4180                 ),
4181                 Err(e) => log::warn!("[heightfield] export to {} failed: {e}", req.path.display()),
4182             }
4183         }
4184         // custom_vertices (e.g. graph geometry) is appended as a final unclipped batch drawn on top.
4185         let pre_custom = verts.len() as u32;
4186         self.inner.as_mut().unwrap().custom_vertices(&mut verts, LogicalSize::new(logical_w, logical_h), scale_factor);
4187         if (verts.len() as u32) > pre_custom {
4188             dl_batches.push(DlBatch { scissor: None, clip_rrect: None, start: pre_custom, end: verts.len() as u32, plate: None, blur_behind: false });
4189         }
4190 
4191         // 1b. Overlay quads (drawn after the text pass).
4192         let mut overlay_quads = Vec::new();
4193         self.inner.as_mut().unwrap().overlay_quads(&mut overlay_quads, LogicalSize::new(logical_w, logical_h), scale_factor);
4194         let mut overlay_verts = Vec::new();
4195         for &(qx, qy, qw, qh, qc) in &overlay_quads {
4196             overlay_verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
4197         }
4198 
4199         // 2. Prepare text
4200         let scale_f32 = scale_factor as f32;
4201         let pw = (logical_w * scale_f32) as u32;
4202         let ph = (logical_h * scale_f32) as u32;
4203 
4204         let bounds = TextBounds { left: 0, top: 0, right: pw as i32, bottom: ph as i32 };
4205         // All text is display-list text now (the legacy text_items/text_areas path is gone):
4206         // map each dl Text prim with the default mapping (scale + surface clamp) plus the
4207         // popover-occlusion clamp against the app's registered popovers.
4208         let mut dl_overlay_rects: Vec<(f32, f32, f32, f32)> = Vec::new();
4209         if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
4210             for &pop_id in &ctx.active_popovers {
4211                 if let Some(ptr) = ctx.tree.get_ptr(pop_id) {
4212                     unsafe {
4213                         if let Some((x, y, w, h)) = (*ptr).popover_rect() {
4214                             dl_overlay_rects.push((x, y, w, h));
4215                         }
4216                     }
4217                 }
4218             }
4219         }
4220         // The global context menu draws into the app's display list (the render-only xdg
4221         // popup is gone), so it gets the same occlusion: the menu rect clamps list text
4222         // beneath, and the menu's own labels are exempt because they carry bounds equal
4223         // to the rect.
4224         if crate::widget::context_menu::is_visible() {
4225             dl_overlay_rects.push((
4226                 crate::widget::context_menu::x(),
4227                 crate::widget::context_menu::y(),
4228                 crate::widget::context_menu::w(),
4229                 crate::widget::context_menu::h(),
4230             ));
4231         }
4232         let mut spans: Vec<TextSpan> = Vec::new();
4233         for ti in &self.dl_text_items {
4234             let mut item_bounds = if let Some([l, t, r, b]) = ti.bounds {
4235                 TextBounds {
4236                     left: ((l * scale_f32).round() as i32).clamp(0, bounds.right),
4237                     top: ((t * scale_f32).round() as i32).clamp(0, bounds.bottom),
4238                     right: ((r * scale_f32).round() as i32).clamp(0, bounds.right),
4239                     bottom: ((b * scale_f32).round() as i32).clamp(0, bounds.bottom),
4240                 }
4241             } else {
4242                 bounds
4243             };
4244             popover_occlusion_clamp(&dl_overlay_rects, ti, scale_f32, &mut item_bounds);
4245             spans.push(TextSpan {
4246                 buffer: &ti.buffer,
4247                 left: (ti.x * scale_f32).round(),
4248                 top: (ti.y * scale_f32).round(),
4249                 // Buffers are shaped at physical size (get_text_buffer_attrs).
4250                 scale: 1.0,
4251                 bounds: Some([
4252                     item_bounds.left,
4253                     item_bounds.top,
4254                     item_bounds.right,
4255                     item_bounds.bottom,
4256                 ]),
4257                 default_color: [
4258                     ti.color.r() as f32 / 255.0,
4259                     ti.color.g() as f32 / 255.0,
4260                     ti.color.b() as f32 / 255.0,
4261                     ti.color.a() as f32 / 255.0,
4262                 ],
4263                 rotation: None,
4264                 // Circle wins when both are set (the circular pane's innermost clip);
4265                 // otherwise a rounded-rect clip rides as center+radius with extents.
4266                 clip_circle: match (ti.clip_circle, ti.clip_rrect) {
4267                     (Some(c), _) => [c[0] * scale_f32, c[1] * scale_f32, c[2] * scale_f32],
4268                     (None, Some(rr)) => [rr[0] * scale_f32, rr[1] * scale_f32, rr[4] * scale_f32],
4269                     (None, None) => [0.0; 3],
4270                 },
4271                 clip_extents: match (ti.clip_circle, ti.clip_rrect) {
4272                     (None, Some(rr)) => [rr[2] * scale_f32, rr[3] * scale_f32],
4273                     _ => [0.0; 2],
4274                 },
4275             });
4276         }
4277 
4278         // 3. Frame: display-list batches under their physical scissors, then
4279         // text, then overlays. The renderer owns swapchain rebuild/recovery.
4280         // An app without display-list text owns the renderer's text state
4281         // itself (it stages via stage_renderer below); don't wipe it here.
4282         let renderer = self.renderer.as_mut().unwrap();
4283         if self.inner.as_ref().unwrap().display_list_text() {
4284             renderer.prepare_text(self.font_system.as_mut().unwrap(), &mut self.swash_cache, &spans);
4285         }
4286 
4287         let image_quads: Vec<crate::vk::ImageQuad> = dl_images
4288             .iter()
4289             .map(|di| crate::vk::ImageQuad {
4290                 image: di.image,
4291                 rect: (
4292                     di.rect.x * scale_f32,
4293                     di.rect.y * scale_f32,
4294                     di.rect.width * scale_f32,
4295                     di.rect.height * scale_f32,
4296                 ),
4297                 alpha: di.alpha,
4298                 z_before: di.at,
4299                 clip: di.clip.map(|c| {
4300                     (
4301                         (c.x * scale_f32).max(0.0) as u32,
4302                         (c.y * scale_f32).max(0.0) as u32,
4303                         (c.width * scale_f32) as u32,
4304                         (c.height * scale_f32) as u32,
4305                     )
4306                 }),
4307             })
4308             .collect();
4309 
4310         let batches: Vec<Batch2D> = dl_batches
4311             .iter()
4312             .map(|batch| Batch2D {
4313                 scissor: batch.scissor.map(|clip| {
4314                     (
4315                         (clip.x * scale_f32).max(0.0) as u32,
4316                         (clip.y * scale_f32).max(0.0) as u32,
4317                         (clip.width * scale_f32) as u32,
4318                         (clip.height * scale_f32) as u32,
4319                     )
4320                 }),
4321                 clip_rrect: batch
4322                     .clip_rrect
4323                     .map(|c| [c[0] * scale_f32, c[1] * scale_f32, c[2] * scale_f32, c[3] * scale_f32, c[4] * scale_f32]),
4324                 start: batch.start,
4325                 end: batch.end,
4326                 plate: batch.plate,
4327                 blur_behind: batch.blur_behind,
4328             })
4329             .collect();
4330 
4331         let cc = self.inner.as_ref().unwrap().clear_color();
4332         let clear_color = [cc[0].powf(2.2), cc[1].powf(2.2), cc[2].powf(2.2), cc[3]];
4333 
4334         // Commit the buffer scale together with a buffer it is legal for: the
4335         // present inside draw_frame_2d is the only commit on this surface, so
4336         // sending the request here orders it right before a matching-size
4337         // attach+commit.
4338         //
4339         // Present only the EXACT extent the current logical size and scale
4340         // call for. Divisibility is not enough: mid scale-transition (the
4341         // resume output bounce) the pending extent can belong to the other
4342         // scale, and an even-sized old-scale buffer divides cleanly by the
4343         // new scale — the commit is protocol-legal, so the compositor reads
4344         // it as a self-resize to half/double and reconfigures the window to
4345         // match (how the color editor came back from suspend at exactly half
4346         // size with the divisibility guard green). Odd sizes at least die
4347         // loudly (invalid_size). On mismatch, re-request the right extent
4348         // and skip — before the frame-callback request below, so the loop
4349         // isn't left waiting on a callback no commit will ever latch.
4350         if let Some(ref surface) = self.surface {
4351             let (s, epw, eph) =
4352                 Self::buffer_geometry(self.scale_factor, self.logical_width, self.logical_height);
4353             let e = renderer.pending_extent();
4354             if e.width != epw || e.height != eph {
4355                 renderer.resize(epw, eph);
4356                 self.extent_gate_skips += 1;
4357                 // ~5s of continuous skipping at the 16ms loop cadence: nothing
4358                 // is presenting and nothing else will say so — this is the
4359                 // only witness to a wedged pending extent.
4360                 if self.extent_gate_skips % 300 == 0 {
4361                     log::warn!(
4362                         "[window_runner] extent gate: pending {}x{} != expected {}x{} for {} consecutive renders; no frame is presenting",
4363                         e.width, e.height, epw, eph, self.extent_gate_skips,
4364                     );
4365                 }
4366                 self.redraw = true;
4367                 return;
4368             }
4369             self.extent_gate_skips = 0;
4370             if s != self.committed_buffer_scale {
4371                 surface.set_buffer_scale(s);
4372                 self.committed_buffer_scale = s;
4373             }
4374         }
4375 
4376         if let Some(ref surface) = self.surface {
4377             let _callback = surface.frame(&self.qh, ());
4378             self.frame_callback_pending = true;
4379             self.frame_callback_armed_at = Some(std::time::Instant::now());
4380             if std::env::var("CCE_PRESENT_DEBUG").is_ok() {
4381                 let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() % 100000;
4382                 eprintln!("[vk] t={} armed frame callback", t);
4383             }
4384         }
4385 
4386         // Direct renderer staging (3D scenes, RT panes, app-shaped text).
4387         if self.inner.as_mut().unwrap().stage_renderer(
4388             renderer,
4389             LogicalSize::new(logical_w, logical_h),
4390             scale_factor,
4391         ) {
4392             self.redraw = true;
4393         }
4394 
4395         if !renderer.draw_frame_2d(Frame2D {
4396             verts: &verts,
4397             batches: &batches,
4398             overlay_verts: &overlay_verts,
4399             images: &image_quads,
4400             plate_features: &plate_features,
4401             clear_color,
4402         }) {
4403             // No present happened (swapchain out-of-date, or the created
4404             // swapchain didn't match the requested extent). The frame
4405             // callback requested above will never latch without a commit —
4406             // clear it or the demand-driven loop stalls waiting forever.
4407             self.frame_callback_pending = false;
4408             self.redraw = true;
4409         }
4410     }
4411 }
4412 
4413 impl<A: Application> Drop for EngineState<A> {
4414     fn drop(&mut self) {
4415         self.renderer = None;
4416     }
4417 }
4418 
4419 impl<A: Application> CompositorHandler for EngineState<A> {
4420     fn scale_factor_changed(
4421         &mut self,
4422         _conn: &Connection,
4423         _qh: &QueueHandle<Self>,
4424         _surface: &wl_surface::WlSurface,
4425         scale_factor: i32,
4426     ) {
4427         // Don't send set_buffer_scale here: an in-flight present can commit an
4428         // old-scale-sized buffer right after it, which is a fatal invalid_size
4429         // protocol error (seen on resume, when outputs bounce 2→1→2). The scale
4430         // request is sent in `render`, paired with a matching-size present.
4431         if crate::scale::forced_scale().is_some() {
4432             // Forced mode: the compositor's opinion (scale 1 under cage) must
4433             // not clobber the override.
4434             return;
4435         }
4436         if self.inner.as_ref().map_or(false, |a| a.grid()) {
4437             // Grid surfaces stay at scale 1 — patch.scale is the sole
4438             // resolution authority (see the pin at surface creation).
4439             return;
4440         }
4441         // Resume bounce: when the surface sits on no LIVE output (the DRM
4442         // connector was destroyed and not yet re-created), the reported
4443         // factor is SCTK's no-outputs fallback, not information — hold the
4444         // last real scale. When the reborn output arrives, surface enter
4445         // recomputes and this handler runs again with a live output backing
4446         // it. Liveness matters (not just enter/leave counting): the leave
4447         // for a destroyed output may never be delivered.
4448         let on_live_output = self
4449             .entered_outputs
4450             .iter()
4451             .any(|o| self.output_state.info(o).is_some());
4452         if !on_live_output && (scale_factor as f64) < self.scale_factor {
4453             return;
4454         }
4455         self.scale_factor = scale_factor as f64;
4456         self.resize(self.logical_width, self.logical_height);
4457         self.redraw = true;
4458     }
4459     
4460     fn transform_changed(
4461         &mut self,
4462         _conn: &Connection,
4463         _qh: &QueueHandle<Self>,
4464         _surface: &wl_surface::WlSurface,
4465         _new_transform: wl_output::Transform,
4466     ) {}
4467     
4468     fn frame(
4469         &mut self,
4470         _conn: &Connection,
4471         _qh: &QueueHandle<Self>,
4472         _surface: &wl_surface::WlSurface,
4473         _time: u32,
4474     ) {}
4475     
4476     fn surface_enter(
4477         &mut self,
4478         _conn: &Connection,
4479         _qh: &QueueHandle<Self>,
4480         _surface: &wl_surface::WlSurface,
4481         output: &wl_output::WlOutput,
4482     ) {
4483         if !self.entered_outputs.contains(output) {
4484             self.entered_outputs.push(output.clone());
4485         }
4486         // Dead entries (destroyed outputs never send leave) are harmless —
4487         // the liveness check in scale_factor_changed skips them — but drop
4488         // them here so the list doesn't grow across suspend cycles.
4489         self.entered_outputs
4490             .retain(|o| self.output_state.info(o).is_some());
4491         self.redraw = true;
4492     }
4493 
4494     fn surface_leave(
4495         &mut self,
4496         _conn: &Connection,
4497         _qh: &QueueHandle<Self>,
4498         _surface: &wl_surface::WlSurface,
4499         output: &wl_output::WlOutput,
4500     ) {
4501         self.entered_outputs.retain(|o| o != output);
4502     }
4503 }
4504 
4505 impl<A: Application> OutputHandler for EngineState<A> {
4506     fn output_state(&mut self) -> &mut OutputState {
4507         &mut self.output_state
4508     }
4509     
4510     fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {
4511         let scale = crate::wayland::detect_scale_factor(&self.output_state);
4512         crate::scale::set_scale_factor(scale as f32);
4513         crate::units::set_metric(crate::wayland::detect_metric(&self.output_state, scale));
4514     }
4515     fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {
4516         let scale = crate::wayland::detect_scale_factor(&self.output_state);
4517         crate::scale::set_scale_factor(scale as f32);
4518         crate::units::set_metric(crate::wayland::detect_metric(&self.output_state, scale));
4519     }
4520     fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
4521 }
4522 
4523 impl<A: Application> ShmHandler for EngineState<A> {
4524     fn shm_state(&mut self) -> &mut Shm {
4525         &mut self.shm_state
4526     }
4527 }
4528 
4529 impl<A: Application> ProvidesRegistryState for EngineState<A> {
4530     fn registry(&mut self) -> &mut RegistryState {
4531         &mut self.registry_state
4532     }
4533     
4534     fn runtime_add_global(
4535         &mut self,
4536         _conn: &Connection,
4537         _qh: &QueueHandle<Self>,
4538         _name: u32,
4539         _interface: &str,
4540         _version: u32,
4541     ) {}
4542     
4543     fn runtime_remove_global(
4544         &mut self,
4545         _conn: &Connection,
4546         _qh: &QueueHandle<Self>,
4547         _name: u32,
4548         _interface: &str,
4549     ) {}
4550 }
4551 
4552 impl<A: Application> WindowHandler for EngineState<A> {
4553     fn configure(
4554         &mut self,
4555         _conn: &Connection,
4556         _qh: &QueueHandle<Self>,
4557         _window: &XdgWindow,
4558         configure: WindowConfigure,
4559         _serial: u32,
4560     ) {
4561         let is_fs = configure.is_fullscreen();
4562         let is_max = configure.is_maximized();
4563         crate::scale::set_fullscreen(is_fs);
4564         crate::scale::set_maximized(is_max);
4565 
4566         let (w, h) = configure.new_size;
4567         // Configure sizes are window-geometry sizes; with an overflow margin
4568         // the surface is a rim larger on the right and bottom.
4569         let m = self.inner.as_ref().unwrap().overflow_margin() as f32;
4570         if let (Some(w), Some(h)) = (w, h) {
4571             let width = w.get();
4572             let height = h.get();
4573             // Forced mode: the compositor's logical size is really physical
4574             // pixels (scale-1 output); divide to get the app's logical space.
4575             let f = crate::scale::forced_scale().unwrap_or(1.0);
4576             self.frame_logical = (width as f32 / f, height as f32 / f);
4577             self.applied_margin = m;
4578             self.resize(width as f32 / f + m, height as f32 / f + m);
4579         } else if self.inner.as_ref().unwrap().grid() && self.logical_width > 1.0 {
4580             // A grid app's size belongs to its PATCHES: the compositor's
4581             // "you choose" 0x0 must not bounce the surface back to the
4582             // settings size — that thrash recreated multi-hundred-MB
4583             // swapchains per bounce (6.3G peak in 10s). Keep the current
4584             // size; the next grid_patch is the only resizer.
4585         } else {
4586             let settings = self.inner.as_ref().unwrap().settings();
4587             self.frame_logical = (settings.width as f32, settings.height as f32);
4588             self.applied_margin = m;
4589             self.resize(settings.width as f32 + m, settings.height as f32 + m);
4590         }
4591         self.redraw = true;
4592         self.frame_callback_pending = false;
4593         self.first_configure_received = true;
4594         self.just_configured = true;
4595     }
4596 
4597     fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
4598         self.exit = true;
4599     }
4600 }
4601 
4602 impl<A: Application> LayerShellHandler for EngineState<A> {
4603     fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
4604         self.exit = true;
4605     }
4606 
4607     fn configure(
4608         &mut self,
4609         _conn: &Connection,
4610         _qh: &QueueHandle<Self>,
4611         _layer: &LayerSurface,
4612         configure: LayerSurfaceConfigure,
4613         _serial: u32,
4614     ) {
4615         // new_size is in logical pixels; 0 means "client decides", so fall back
4616         // to the app's requested size (mirrors the xdg WindowHandler above).
4617         let (w, h) = configure.new_size;
4618         if w > 0 && h > 0 {
4619             self.resize(w as f32, h as f32);
4620         } else {
4621             let settings = self.inner.as_ref().unwrap().settings();
4622             self.resize(settings.width as f32, settings.height as f32);
4623         }
4624         self.redraw = true;
4625         self.frame_callback_pending = false;
4626         self.first_configure_received = true;
4627         self.just_configured = true;
4628     }
4629 }
4630 
4631 impl<A: Application> SeatHandler for EngineState<A> {
4632     fn seat_state(&mut self) -> &mut SeatState {
4633         &mut self.seat_state
4634     }
4635     
4636     fn new_seat(&mut self, _conn: &Connection, qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
4637         self.ensure_data_device(qh, &seat);
4638         self.seats.push(seat);
4639     }
4640     
4641     fn new_capability(
4642         &mut self,
4643         _conn: &Connection,
4644         qh: &QueueHandle<Self>,
4645         seat: wl_seat::WlSeat,
4646         capability: Capability,
4647     ) {
4648         // Every seat arrives here, unlike `new_seat` — SCTK binds the seats
4649         // that already exist at startup without announcing them, so a device
4650         // created only there is never created at all on a normal launch.
4651         self.ensure_data_device(qh, &seat);
4652         if capability == Capability::Pointer && self.pointer.is_none() {
4653             let surface = self.compositor_state.create_surface::<Self>(qh);
4654             let themed_pointer = self.seat_state.get_pointer_with_theme(
4655                 qh,
4656                 &seat,
4657                 self.shm_state.wl_shm(),
4658                 surface,
4659                 ThemeSpec::System,
4660             ).unwrap();
4661             if let Some(ref pg) = self.pointer_gestures {
4662                 self.pinch_gesture = Some(pg.get_pinch_gesture(themed_pointer.pointer(), qh, ()));
4663             }
4664             self.pointer = Some(themed_pointer);
4665         }
4666         if capability == Capability::Keyboard && self.keyboard.is_none() {
4667             let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
4668             self.keyboard = Some(keyboard);
4669         }
4670     }
4671     
4672     fn remove_capability(
4673         &mut self,
4674         _conn: &Connection,
4675         _qh: &QueueHandle<Self>,
4676         _seat: wl_seat::WlSeat,
4677         capability: Capability,
4678     ) {
4679         if capability == Capability::Pointer {
4680             self.pinch_gesture = None;
4681             self.pointer = None;
4682         }
4683         if capability == Capability::Keyboard {
4684             self.keyboard = None;
4685         }
4686     }
4687     
4688     fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
4689         self.seats.retain(|s| s != &seat);
4690     }
4691 }
4692 
4693 impl<A: Application> PointerHandler for EngineState<A> {
4694     fn pointer_frame(
4695         &mut self,
4696         _conn: &Connection,
4697         _qh: &QueueHandle<Self>,
4698         _pointer: &wl_pointer::WlPointer,
4699         events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
4700     ) {
4701         use smithay_client_toolkit::seat::pointer::PointerEventKind;
4702         let mut coalesced_h = 0.0f64;
4703         let mut coalesced_v = 0.0f64;
4704         let mut discrete_h = 0;
4705         let mut discrete_v = 0;
4706         let mut has_scroll = false;
4707         let mut axis_source: Option<wl_pointer::AxisSource> = None;
4708         let mut axis_stop = false;
4709         let (mut last_lx, mut last_ly) = (0.0f32, 0.0f32);
4710 
4711         // Forced mode: pointer positions arrive in the compositor's scale-1
4712         // logical space (= physical); divide into the app's logical space.
4713         let forced = crate::scale::forced_scale().unwrap_or(1.0);
4714         for event in events {
4715             let (x, y) = event.position;
4716             // Overflow-margin mode needs no translation: the rim is
4717             // right/bottom-only, so frame coords == surface coords.
4718             let lx = x as f32 / forced;
4719             let ly = y as f32 / forced;
4720 
4721             self.cursor_pos = (lx, ly);
4722             match &event.kind {
4723                 PointerEventKind::Enter { .. } => {
4724                     // Enter carries the pointer's position but no Motion follows until it
4725                     // actually moves — without this the app's hover state is stale from
4726                     // enter to first move, and a press in that window can misroute (e.g. a
4727                     // divider press falling through to the movable-root plate window drag).
4728                     let mut rebuild = false;
4729                     self.inner.as_mut().unwrap().handle_pointer_move(LogicalPosition::new(lx, ly), &mut rebuild);
4730                     if rebuild {
4731                         self.redraw = true;
4732                     }
4733 
4734                     let cursor_icon = self.cursor_icon_at(lx, ly);
4735                     self.current_cursor_icon = Some(cursor_icon);
4736                     if let Some(ref themed_pointer) = self.pointer {
4737                         let _ = themed_pointer.set_cursor(_conn, cursor_icon);
4738                     }
4739                 }
4740                 PointerEventKind::Leave { .. } => {
4741                     self.current_cursor_icon = None;
4742                     // Focus can move mid-gesture (a fullscreen switch, a
4743                     // relayout sliding the window away): the real Release
4744                     // then lands on another surface, and an armed drag would
4745                     // live forever, steered by whatever motion arrives next.
4746                     // End held gestures with synthetic releases at the last
4747                     // known cursor position before anything else.
4748                     if self.buttons_down != 0 {
4749                         let (px, py) = self.cursor_pos;
4750                         for (bit, btn) in
4751                             [(1u32, MouseButton::Left), (2, MouseButton::Right), (4, MouseButton::Middle)]
4752                         {
4753                             if self.buttons_down & bit == 0 {
4754                                 continue;
4755                             }
4756                             let mut rebuild = false;
4757                             if let Some(msg) = self.inner.as_mut().unwrap().handle_mouse_input(
4758                                 btn,
4759                                 ElementState::Released,
4760                                 LogicalPosition::new(px, py),
4761                                 &mut rebuild,
4762                             ) {
4763                                 let mut update_rebuild = false;
4764                                 self.inner.as_mut().unwrap().update(msg, &mut update_rebuild, &mut self.exit);
4765                                 if update_rebuild {
4766                                     rebuild = true;
4767                                 }
4768                             }
4769                             if rebuild {
4770                                 self.redraw = true;
4771                             }
4772                         }
4773                         self.buttons_down = 0;
4774                     }
4775                     // Then clear hover with an off-screen move — safe now
4776                     // that no drag is held.
4777                     let mut rebuild = false;
4778                     self.inner.as_mut().unwrap().handle_pointer_move(LogicalPosition::new(-10000.0, -10000.0), &mut rebuild);
4779                     if rebuild {
4780                         self.redraw = true;
4781                     }
4782                 }
4783                 PointerEventKind::Motion { .. } => {
4784                     let mut rebuild = false;
4785                     self.inner.as_mut().unwrap().handle_pointer_move(LogicalPosition::new(lx, ly), &mut rebuild);
4786                     if rebuild {
4787                         self.redraw = true;
4788                     }
4789 
4790                     let cursor_icon = self.cursor_icon_at(lx, ly);
4791 
4792                     if self.current_cursor_icon != Some(cursor_icon) {
4793                         self.current_cursor_icon = Some(cursor_icon);
4794                         if let Some(ref themed_pointer) = self.pointer {
4795                             let _ = themed_pointer.set_cursor(_conn, cursor_icon);
4796                         }
4797                     }
4798                 }
4799                 PointerEventKind::Press { button, serial, .. } => {
4800                     let btn = match *button {
4801                         272 => MouseButton::Left,
4802                         273 => MouseButton::Right,
4803                         274 => MouseButton::Middle,
4804                         _ => continue,
4805                     };
4806                     self.last_press_serial = Some(*serial);
4807                     self.buttons_down |= match btn {
4808                         MouseButton::Left => 1,
4809                         MouseButton::Right => 2,
4810                         _ => 4,
4811                     };
4812 
4813                     // Client-Side Decorations (CSD) Drag & Resize Handling
4814                     let is_status_bar = self.inner.as_ref().unwrap().settings().app_id.starts_with("cce-status");
4815                     if btn == MouseButton::Left && !is_status_bar && self.inner.as_ref().unwrap().standard_csd() {
4816                         let border = 8.0f32;
4817                         let mut edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::None;
4818                         if !self.inner.as_ref().unwrap().csd_resize_borders() {
4819                             // Resize borders are off: the compositor's own band
4820                             // outside the window handles it. Fall through to the
4821                             // move checks so drag-to-move still works.
4822                         } else if ly < border {
4823                             if lx < border {
4824                                 edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::TopLeft;
4825                             } else if lx > self.logical_width - border {
4826                                 edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::TopRight;
4827                             } else {
4828                                 edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::Top;
4829                             }
4830                         } else if ly > self.logical_height - border {
4831                             if lx < border {
4832                                 edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::BottomLeft;
4833                             } else if lx > self.logical_width - border {
4834                                 edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::BottomRight;
4835                             } else {
4836                                 edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::Bottom;
4837                             }
4838                         } else if lx < border {
4839                             edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::Left;
4840                         } else if lx > self.logical_width - border {
4841                             edge = smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::Right;
4842                         }
4843 
4844                         if edge != smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::None {
4845                             if let Some(ref window) = self.window {
4846                                 let seat_owned = self.seats.first().cloned().or_else(|| self.seat_state.seats().next());
4847                                 if let Some(ref seat) = seat_owned {
4848                                     window.resize(seat, *serial, edge);
4849                                     continue;
4850                                 }
4851                             }
4852                         }
4853 
4854                         // Titlebar drag check: y is in [8.0, 32.0], and x is not in the top-right button area
4855                         let mut should_move = false;
4856                         let mut is_widget = false;
4857                         if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
4858                             if ctx.is_widget_at(lx, ly) {
4859                                 is_widget = true;
4860                             }
4861                         }
4862                         if !is_widget
4863                             && self.inner.as_ref().unwrap().csd_titlebar_move()
4864                             && ly >= border && ly < 32.0 && lx < self.logical_width - 70.0
4865                         {
4866                             should_move = true;
4867                         } else if self.inner.as_ref().unwrap().is_movable_root_plate_at(lx, ly) {
4868                             should_move = true;
4869                         }
4870 
4871                         if should_move {
4872                             if let Some(ref window) = self.window {
4873                                 let seat_owned = self.seats.first().cloned().or_else(|| self.seat_state.seats().next());
4874                                 if let Some(ref seat) = seat_owned {
4875                                     window.move_(seat, *serial);
4876                                     continue;
4877                                 }
4878                             }
4879                         }
4880                     }
4881 
4882                     // Outside-press close for open popovers, BEFORE the app's
4883                     // dispatch: apps commonly region-gate their routing, so an
4884                     // open menu's owner may never hear about a press elsewhere.
4885                     if btn == MouseButton::Left {
4886                         if let Some(ctx) = self.inner.as_mut().unwrap().ui_context_mut() {
4887                             ctx.close_popovers_missed_by_press(lx, ly);
4888                         }
4889                     }
4890 
4891                     let mut rebuild = false;
4892                     if let Some(msg) = self.inner.as_mut().unwrap().handle_mouse_input(btn, ElementState::Pressed, LogicalPosition::new(lx, ly), &mut rebuild) {
4893                         let mut update_rebuild = false;
4894                         self.inner.as_mut().unwrap().update(msg, &mut update_rebuild, &mut self.exit);
4895                         if update_rebuild {
4896                             rebuild = true;
4897                         }
4898                     }
4899                     if rebuild {
4900                         self.redraw = true;
4901                     }
4902                 }
4903                 PointerEventKind::Release { button, .. } => {
4904                     let btn = match *button {
4905                         272 => MouseButton::Left,
4906                         273 => MouseButton::Right,
4907                         274 => MouseButton::Middle,
4908                         _ => continue,
4909                     };
4910                     self.buttons_down &= !match btn {
4911                         MouseButton::Left => 1,
4912                         MouseButton::Right => 2,
4913                         _ => 4,
4914                     };
4915                     let mut rebuild = false;
4916                     if let Some(msg) = self.inner.as_mut().unwrap().handle_mouse_input(btn, ElementState::Released, LogicalPosition::new(lx, ly), &mut rebuild) {
4917                         let mut update_rebuild = false;
4918                         self.inner.as_mut().unwrap().update(msg, &mut update_rebuild, &mut self.exit);
4919                         if update_rebuild {
4920                             rebuild = true;
4921                         }
4922                     }
4923                     if rebuild {
4924                         self.redraw = true;
4925                     }
4926                 }
4927                 PointerEventKind::Axis { horizontal, vertical, source, .. } => {
4928                     coalesced_h += horizontal.absolute;
4929                     coalesced_v += vertical.absolute;
4930                     discrete_h += horizontal.discrete;
4931                     discrete_v += vertical.discrete;
4932                     // The source and the finger-lift stop ride in the same
4933                     // frame as the deltas (or alone, for the lift): they
4934                     // decide the smooth-scroll phase below.
4935                     if source.is_some() {
4936                         axis_source = *source;
4937                     }
4938                     axis_stop |= horizontal.stop || vertical.stop;
4939                     last_lx = lx;
4940                     last_ly = ly;
4941                     has_scroll = true;
4942                 }
4943             }
4944         }
4945 
4946         if has_scroll {
4947             // Per-app scroll factors from input.kdl (`<app>`/`cce-ui` domain
4948             // `input { }` blocks); the compositor's global device scaling has
4949             // already been applied at the source.
4950             let factors = crate::input::scroll_factors();
4951             // Smooth-scroll phase for this dispatch: a finger lift is a stop
4952             // frame (no delta); finger/continuous sources track 1:1 and may
4953             // fling on the lift; everything else is a wheel notch that glides.
4954             let no_delta = coalesced_h == 0.0 && coalesced_v == 0.0 && discrete_h == 0 && discrete_v == 0;
4955             let phase = if axis_stop && no_delta {
4956                 crate::widget::ScrollPhase::FingerEnd
4957             } else if discrete_h == 0 && discrete_v == 0
4958                 && matches!(
4959                     axis_source,
4960                     None | Some(wl_pointer::AxisSource::Finger) | Some(wl_pointer::AxisSource::Continuous)
4961                 )
4962             {
4963                 crate::widget::ScrollPhase::Finger
4964             } else {
4965                 crate::widget::ScrollPhase::Wheel
4966             };
4967             crate::widget::scroll_motion::set_scroll_phase(phase);
4968             let delta = if discrete_h == 0 && discrete_v == 0 {
4969                 // Pixel scroll event from touchpad / smooth mouse
4970                 MouseScrollDelta::PixelDelta(Position {
4971                     x: -coalesced_h * factors.trackpad,
4972                     y: -coalesced_v * factors.trackpad,
4973                 })
4974             } else {
4975                 // Discrete scroll event (e.g. wheel clicks)
4976                 let h_lines = if discrete_h != 0 { discrete_h as f32 } else { coalesced_h as f32 / 10.0 };
4977                 let v_lines = if discrete_v != 0 { discrete_v as f32 } else { coalesced_v as f32 / 10.0 };
4978                 MouseScrollDelta::LineDelta(-h_lines * factors.mouse as f32, -v_lines * factors.mouse as f32)
4979             };
4980             if crate::scroll_debug() {
4981                 static T0: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
4982                 let t = T0.get_or_init(std::time::Instant::now).elapsed().as_millis();
4983                 eprintln!(
4984                     "[scroll {t}ms] runner: coalesced=({coalesced_h:.2},{coalesced_v:.2}) discrete=({discrete_h},{discrete_v}) source={axis_source:?} stop={axis_stop} phase={phase:?} factors=(tp {:.2}, m {:.2}) -> {delta:?} at ({last_lx:.0},{last_ly:.0})",
4985                     factors.trackpad, factors.mouse
4986                 );
4987             }
4988             let mut rebuild = false;
4989             if let Some(ctx) = self.inner.as_mut().unwrap().ui_context_mut() {
4990                 ctx.ctrl_pressed = self.ctrl_pressed;
4991                 ctx.shift_pressed = self.shift_pressed;
4992                 ctx.alt_pressed = self.alt_pressed;
4993                 ctx.logo_pressed = self.logo_pressed;
4994             }
4995             self.inner.as_mut().unwrap().handle_mouse_wheel(&delta, LogicalPosition::new(last_lx, last_ly), &mut rebuild);
4996             if rebuild {
4997                 self.redraw = true;
4998             }
4999         }
5000 
5001         // App-driven window move/resize (non-standard CSD; see WindowAction):
5002         // executed with the serial of the most recent pointer press.
5003         if let Some(action) = self.inner.as_mut().unwrap().take_window_action() {
5004             if let (Some(ref window), Some(serial)) = (&self.window, self.last_press_serial) {
5005                 let seat_owned = self.seats.first().cloned().or_else(|| self.seat_state.seats().next());
5006                 if let Some(ref seat) = seat_owned {
5007                     match action {
5008                         WindowAction::Move => window.move_(seat, serial),
5009                         WindowAction::Resize(edge) => window.resize(seat, serial, edge),
5010                     }
5011                 }
5012             }
5013         }
5014     }
5015 }
5016 
5017 impl<A: Application> KeyboardHandler for EngineState<A> {
5018     fn enter(
5019         &mut self,
5020         _conn: &Connection,
5021         _qh: &QueueHandle<Self>,
5022         _keyboard: &wl_keyboard::WlKeyboard,
5023         _surface: &wl_surface::WlSurface,
5024         _serial: u32,
5025         _raw_modifiers: &[u32],
5026         _keysyms: &[xkeysym::Keysym],
5027     ) {
5028         let mut rebuild = false;
5029         self.inner.as_mut().unwrap().handle_focus_change(true, &mut rebuild);
5030         if rebuild {
5031             self.redraw = true;
5032         }
5033     }
5034 
5035     fn leave(
5036         &mut self,
5037         _conn: &Connection,
5038         _qh: &QueueHandle<Self>,
5039         _keyboard: &wl_keyboard::WlKeyboard,
5040         _surface: &wl_surface::WlSurface,
5041         _serial: u32,
5042     ) {
5043         self.pressed_key = None;
5044         self.ctrl_pressed = false;
5045         self.shift_pressed = false;
5046         self.alt_pressed = false;
5047         let mut rebuild = false;
5048         self.inner.as_mut().unwrap().handle_focus_change(false, &mut rebuild);
5049         if rebuild {
5050             self.redraw = true;
5051         }
5052     }
5053     
5054     fn press_key(
5055         &mut self,
5056         _conn: &Connection,
5057         _qh: &QueueHandle<Self>,
5058         _keyboard: &wl_keyboard::WlKeyboard,
5059         _serial: u32,
5060         event: smithay_client_toolkit::seat::keyboard::KeyEvent,
5061     ) {
5062         self.handle_key(event, ElementState::Pressed);
5063     }
5064     
5065     fn release_key(
5066         &mut self,
5067         _conn: &Connection,
5068         _qh: &QueueHandle<Self>,
5069         _keyboard: &wl_keyboard::WlKeyboard,
5070         _serial: u32,
5071         event: smithay_client_toolkit::seat::keyboard::KeyEvent,
5072     ) {
5073         self.handle_key(event, ElementState::Released);
5074     }
5075     
5076     fn update_modifiers(
5077         &mut self,
5078         _conn: &Connection,
5079         _qh: &QueueHandle<Self>,
5080         _keyboard: &wl_keyboard::WlKeyboard,
5081         _serial: u32,
5082         modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
5083         _layout: u32,
5084     ) {
5085         self.ctrl_pressed = modifiers.ctrl;
5086         self.shift_pressed = modifiers.shift;
5087         self.alt_pressed = modifiers.alt;
5088         self.logo_pressed = modifiers.logo;
5089 
5090         if let Some(ctx) = self.inner.as_mut().unwrap().ui_context_mut() {
5091             ctx.ctrl_pressed = self.ctrl_pressed;
5092             ctx.shift_pressed = self.shift_pressed;
5093             ctx.alt_pressed = self.alt_pressed;
5094             ctx.logo_pressed = self.logo_pressed;
5095         }
5096     }
5097 
5098     fn update_repeat_info(
5099         &mut self,
5100         _conn: &Connection,
5101         _qh: &QueueHandle<Self>,
5102         _keyboard: &wl_keyboard::WlKeyboard,
5103         info: smithay_client_toolkit::seat::keyboard::RepeatInfo,
5104     ) {
5105         match info {
5106             smithay_client_toolkit::seat::keyboard::RepeatInfo::Repeat { rate, delay } => {
5107                 // Store/expose delay/rate if required by the application
5108                 let _ = (rate, delay);
5109             }
5110             smithay_client_toolkit::seat::keyboard::RepeatInfo::Disable => {}
5111         }
5112     }
5113 }
5114 
5115 impl<A: Application> EngineState<A> {
5116     /// The toolkit-wide undo/redo routing: a press matching the `undo` /
5117     /// `redo` chord goes to the focused widget first (`ContextAction::Undo`
5118     /// / `Redo` — a text box that is editing steps its own typing), then to
5119     /// the app's `Application::undo` / `redo`. Returns whether either took
5120     /// it; otherwise the key is dispatched as usual, so an app with its own
5121     /// scheme is undisturbed. Runs for repeats too — holding the chord walks
5122     /// the history like holding Backspace walks the text.
5123     /// The toolkit's Tab traversal, for apps that opt in
5124     /// (`Application::plate_navigation`): a bare Tab / Shift+Tab press moves
5125     /// keyboard focus to the next / previous plate or well. Returns whether it
5126     /// moved; otherwise the key is dispatched as usual.
5127     fn route_plate_navigation(&mut self, event: &KeyEvent, rebuild: &mut bool) -> bool {
5128         if event.state != ElementState::Pressed {
5129             return false;
5130         }
5131         // The group jump first (its chords carry ctrl); then a bare Tab.
5132         let group_next = crate::widget::match_key_shortcut(event, &self.group_next_chord);
5133         let group_prev = !group_next && crate::widget::match_key_shortcut(event, &self.group_prev_chord);
5134         let bare_tab = event.logical_key == Key::Named(NamedKey::Tab)
5135             && !self.ctrl_pressed
5136             && !self.alt_pressed
5137             && !self.logo_pressed;
5138         if !group_next && !group_prev && !bare_tab {
5139             return false;
5140         }
5141         let reverse = if bare_tab { self.shift_pressed } else { group_prev };
5142         let app = self.inner.as_mut().unwrap();
5143         if !app.plate_navigation() {
5144             return false;
5145         }
5146         let moved = app.ui_context_mut().is_some_and(|ctx| if bare_tab { ctx.focus_step(reverse) } else { ctx.focus_step_group(reverse) });
5147         if moved {
5148             app.focus_stepped();
5149             *rebuild = true;
5150         }
5151         moved
5152     }
5153 
5154     fn route_history_chord(&mut self, event: &KeyEvent, rebuild: &mut bool) -> bool {
5155         if event.state != ElementState::Pressed {
5156             return false;
5157         }
5158         let undo = crate::widget::match_key_shortcut(event, &self.undo_chord);
5159         let redo = !undo && crate::widget::match_key_shortcut(event, &self.redo_chord);
5160         if !undo && !redo {
5161             return false;
5162         }
5163         let app = self.inner.as_mut().unwrap();
5164         let action = if undo { crate::widget::ContextAction::Undo } else { crate::widget::ContextAction::Redo };
5165         if let Some(ctx) = app.ui_context_mut() {
5166             if ctx.focused_context_action(action) {
5167                 *rebuild = true;
5168                 return true;
5169             }
5170         }
5171         let taken = if undo { app.undo(rebuild) } else { app.redo(rebuild) };
5172         if taken {
5173             *rebuild = true;
5174         }
5175         taken
5176     }
5177 
5178     fn handle_key(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, state: ElementState) {
5179         let logical_key = match event.keysym {
5180             xkeysym::Keysym::Escape => Key::Named(NamedKey::Escape),
5181             xkeysym::Keysym::Return => Key::Named(NamedKey::Enter),
5182             xkeysym::Keysym::BackSpace => Key::Named(NamedKey::Backspace),
5183             xkeysym::Keysym::Down => Key::Named(NamedKey::ArrowDown),
5184             xkeysym::Keysym::Up => Key::Named(NamedKey::ArrowUp),
5185             xkeysym::Keysym::Left => Key::Named(NamedKey::ArrowLeft),
5186             xkeysym::Keysym::Right => Key::Named(NamedKey::ArrowRight),
5187             // xkb reports Shift+Tab as ISO_Left_Tab; apps see plain Tab plus
5188             // the shift modifier, matching winit.
5189             xkeysym::Keysym::Tab | xkeysym::Keysym::ISO_Left_Tab => Key::Named(NamedKey::Tab),
5190             xkeysym::Keysym::Delete => Key::Named(NamedKey::Delete),
5191             xkeysym::Keysym::space => Key::Named(NamedKey::Space),
5192             xkeysym::Keysym::Page_Up => Key::Named(NamedKey::PageUp),
5193             xkeysym::Keysym::Page_Down => Key::Named(NamedKey::PageDown),
5194             xkeysym::Keysym::Home => Key::Named(NamedKey::Home),
5195             xkeysym::Keysym::End => Key::Named(NamedKey::End),
5196             xkeysym::Keysym::Super_L | xkeysym::Keysym::Super_R => Key::Named(NamedKey::Super),
5197             xkeysym::Keysym::Alt_L | xkeysym::Keysym::Alt_R => Key::Named(NamedKey::Alt),
5198             xkeysym::Keysym::Control_L | xkeysym::Keysym::Control_R => Key::Named(NamedKey::Control),
5199             xkeysym::Keysym::Shift_L | xkeysym::Keysym::Shift_R => Key::Named(NamedKey::Shift),
5200             xkeysym::Keysym::F5 => Key::Named(NamedKey::F5),
5201             _ => {
5202                 // With Ctrl held, xkb's utf8 goes through the legacy control-character
5203                 // transformation (ctrl+j = "\n", ctrl+a = 0x01, ...); the keysym is
5204                 // untransformed, so prefer it there or ctrl+<letter> shortcuts can
5205                 // never match their letter.
5206                 if self.ctrl_pressed {
5207                     if let Some(ch) = event.keysym.key_char() {
5208                         Key::Character(ch.to_string())
5209                     } else if let Some(ref text) = event.utf8 {
5210                         Key::Character(text.clone())
5211                     } else {
5212                         return;
5213                     }
5214                 } else if let Some(ref text) = event.utf8 {
5215                     Key::Character(text.clone())
5216                 } else if let Some(ch) = event.keysym.key_char() {
5217                     Key::Character(ch.to_string())
5218                 } else {
5219                     return;
5220                 }
5221             }
5222         };
5223 
5224         let custom_event = KeyEvent {
5225             state,
5226             logical_key,
5227             text: event.utf8.clone(),
5228             repeat: false,
5229             ctrl: self.ctrl_pressed,
5230             shift: self.shift_pressed,
5231             alt: self.alt_pressed,
5232         };
5233 
5234         if state == ElementState::Pressed {
5235             if is_repeatable_key(&custom_event.logical_key) {
5236                 self.pressed_key = Some(PressedKey {
5237                     logical_key: custom_event.logical_key.clone(),
5238                     text: custom_event.text.clone(),
5239                     first_pressed: Instant::now(),
5240                     last_repeated: Instant::now(),
5241                 });
5242             } else {
5243                 self.pressed_key = None;
5244             }
5245         } else if state == ElementState::Released {
5246             if let Some(ref pk) = self.pressed_key {
5247                 if pk.logical_key == custom_event.logical_key {
5248                     self.pressed_key = None;
5249                 }
5250             }
5251         }
5252 
5253         if let Some(ctx) = self.inner.as_mut().unwrap().ui_context_mut() {
5254             ctx.ctrl_pressed = self.ctrl_pressed;
5255             ctx.shift_pressed = self.shift_pressed;
5256             ctx.alt_pressed = self.alt_pressed;
5257             ctx.logo_pressed = self.logo_pressed;
5258         }
5259 
5260         // Escape dismisses the shared context menu before app dispatch — the
5261         // toolkit-wide default, mirroring the click-outside dismissal. Consumed:
5262         // while a menu is open, Escape means "close it", nothing else.
5263         if state == ElementState::Pressed
5264             && custom_event.logical_key == Key::Named(NamedKey::Escape)
5265             && crate::widget::context_menu::is_visible()
5266         {
5267             crate::widget::context_menu::hide();
5268             self.redraw = true;
5269             return;
5270         }
5271 
5272         let mut rebuild = false;
5273         if self.route_history_chord(&custom_event, &mut rebuild)
5274             || self.route_plate_navigation(&custom_event, &mut rebuild)
5275         {
5276             self.redraw = true;
5277             return;
5278         }
5279         if let Some(msg) = self.inner.as_mut().unwrap().handle_key_input(&custom_event, &mut rebuild) {
5280             let mut update_rebuild = false;
5281             self.inner.as_mut().unwrap().update(msg, &mut update_rebuild, &mut self.exit);
5282             if update_rebuild {
5283                 rebuild = true;
5284             }
5285         }
5286         if rebuild {
5287             self.redraw = true;
5288         }
5289     }
5290 }
5291 
5292 impl<A: Application> wayland_client::Dispatch<wl_registry::WlRegistry, GlobalList, Self> for EngineState<A> {
5293     fn event(
5294         _state: &mut Self,
5295         _proxy: &wl_registry::WlRegistry,
5296         _event: wl_registry::Event,
5297         _data: &GlobalList,
5298         _conn: &Connection,
5299         _qh: &QueueHandle<Self>,
5300     ) {}
5301 }
5302 
5303 impl<A: Application> wayland_client::Dispatch<crate::protocol::zcce_inspector_v1::ZcceInspectorV1, ()> for EngineState<A> {
5304     fn event(
5305         _state: &mut Self,
5306         _proxy: &crate::protocol::zcce_inspector_v1::ZcceInspectorV1,
5307         _event: crate::protocol::zcce_inspector_v1::Event,
5308         _data: &(),
5309         _conn: &Connection,
5310         _qh: &QueueHandle<Self>,
5311     ) {}
5312 }
5313 
5314 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1, ()> for EngineState<A> {
5315     fn event(
5316         _state: &mut Self,
5317         _proxy: &crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1,
5318         _event: crate::protocol::cce_window_management_v1::zcce_window_manager_v1::Event,
5319         _data: &(),
5320         _conn: &Connection,
5321         _qh: &QueueHandle<Self>,
5322     ) {}
5323 
5324     wayland_client::event_created_child!(
5325         EngineState<A>,
5326         crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1,
5327         [
5328             6 => (crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1, ()),
5329             7 => (crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1, ()),
5330             8 => (crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1, ()),
5331         ]
5332     );
5333 }
5334 
5335 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1, ()> for EngineState<A> {
5336     fn event(
5337         _state: &mut Self,
5338         _proxy: &crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1,
5339         _event: crate::protocol::cce_window_management_v1::zcce_window_v1::Event,
5340         _data: &(),
5341         _conn: &Connection,
5342         _qh: &QueueHandle<Self>,
5343     ) {}
5344 }
5345 
5346 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1, ()> for EngineState<A> {
5347     fn event(
5348         _state: &mut Self,
5349         _proxy: &crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1,
5350         _event: crate::protocol::cce_window_management_v1::zcce_output_v1::Event,
5351         _data: &(),
5352         _conn: &Connection,
5353         _qh: &QueueHandle<Self>,
5354     ) {}
5355 }
5356 
5357 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1, ()> for EngineState<A> {
5358     fn event(
5359         _state: &mut Self,
5360         _proxy: &crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1,
5361         _event: crate::protocol::cce_window_management_v1::zcce_seat_v1::Event,
5362         _data: &(),
5363         _conn: &Connection,
5364         _qh: &QueueHandle<Self>,
5365     ) {}
5366 }
5367 
5368 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1, ()> for EngineState<A> {
5369     fn event(
5370         state: &mut Self,
5371         _proxy: &crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1,
5372         event: crate::protocol::cce_window_management_v1::zcce_toplevel_v1::Event,
5373         _data: &(),
5374         _conn: &Connection,
5375         _qh: &QueueHandle<Self>,
5376     ) {
5377         use crate::protocol::cce_window_management_v1::zcce_toplevel_v1::Event;
5378         if let Event::GridPatch { serial, x, y, width, height, scale } = event {
5379             // A newer patch supersedes an unconsumed older one.
5380             state.pending_grid_patch = Some((serial, x, y, width, height, scale));
5381             state.redraw = true;
5382         }
5383     }
5384 }
5385 
5386 delegate_compositor!(@<A: Application> EngineState<A>);
5387 delegate_xdg_shell!(@<A: Application> EngineState<A>);
5388 delegate_xdg_window!(@<A: Application> EngineState<A>);
5389 delegate_layer!(@<A: Application> EngineState<A>);
5390 delegate_shm!(@<A: Application> EngineState<A>);
5391 delegate_seat!(@<A: Application> EngineState<A>);
5392 delegate_pointer!(@<A: Application> EngineState<A>);
5393 delegate_keyboard!(@<A: Application> EngineState<A>);
5394 delegate_registry!(@<A: Application> EngineState<A>);
5395 delegate_output!(@<A: Application> EngineState<A>);
5396 
5397 impl<A: Application> wayland_client::Dispatch<wl_region::WlRegion, ()> for EngineState<A> {
5398     fn event(
5399         _state: &mut Self,
5400         _proxy: &wl_region::WlRegion,
5401         _event: wl_region::Event,
5402         _data: &(),
5403         _conn: &Connection,
5404         _qh: &QueueHandle<Self>,
5405     ) {}
5406 }
5407 
5408 impl<A: Application> wayland_client::Dispatch<wl_callback::WlCallback, ()> for EngineState<A> {
5409     fn event(
5410         state: &mut Self,
5411         _proxy: &wl_callback::WlCallback,
5412         event: wl_callback::Event,
5413         _data: &(),
5414         _conn: &Connection,
5415         _qh: &QueueHandle<Self>,
5416     ) {
5417         if let wl_callback::Event::Done { .. } = event {
5418             state.frame_callback_pending = false;
5419             if std::env::var("CCE_PRESENT_DEBUG").is_ok() {
5420                 let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() % 100000;
5421                 let waited = state.frame_callback_armed_at.map(|a| a.elapsed().as_millis()).unwrap_or(0);
5422                 eprintln!("[vk] t={} frame-done (waited {}ms)", t, waited);
5423             }
5424         }
5425     }
5426 }
5427 
5428 impl<A: Application> wayland_client::Dispatch<ZwpPointerGesturesV1, ()> for EngineState<A> {
5429     fn event(
5430         _state: &mut Self,
5431         _proxy: &ZwpPointerGesturesV1,
5432         _event: zwp_pointer_gestures::Event,
5433         _data: &(),
5434         _conn: &Connection,
5435         _qh: &QueueHandle<Self>,
5436     ) {}
5437 }
5438 
5439 impl<A: Application> wayland_client::Dispatch<ZwpPointerGesturePinchV1, ()> for EngineState<A> {
5440     fn event(
5441         state: &mut Self,
5442         _proxy: &ZwpPointerGesturePinchV1,
5443         event: zwp_pointer_gesture_pinch_v1::Event,
5444         _data: &(),
5445         _conn: &Connection,
5446         _qh: &QueueHandle<Self>,
5447     ) {
5448         match event {
5449             zwp_pointer_gesture_pinch_v1::Event::Begin { .. } => {
5450                 state.last_pinch_scale = 1.0;
5451             }
5452             zwp_pointer_gesture_pinch_v1::Event::Update { scale, .. } => {
5453                 let scale_f32 = scale as f32;
5454                 let factor = scale_f32 / state.last_pinch_scale;
5455                 state.last_pinch_scale = scale_f32;
5456 
5457                 let (px, py) = state.cursor_pos;
5458                 let mut rebuild = false;
5459 
5460                 // First offer the gesture as-is: apps with true pinch
5461                 // surfaces (the designer's 3D viewport) consume it here at
5462                 // 1:1 scale instead of through the wheel synthesis below.
5463                 if state.inner.as_mut().unwrap().handle_pinch(factor, LogicalPosition::new(px, py), &mut rebuild) {
5464                     if rebuild {
5465                         state.redraw = true;
5466                     }
5467                     return;
5468                 }
5469 
5470                 // Calculate the y_delta for PixelDelta mapping.
5471                 // Since cce-graph interprets factor = 1.0 + y_delta * 0.015, we reverse it:
5472                 let y_delta = (factor - 1.0) / 0.015;
5473                 let delta = MouseScrollDelta::PixelDelta(Position {
5474                     x: 0.0,
5475                     y: y_delta as f64,
5476                 });
5477 
5478                 if let Some(ctx) = state.inner.as_mut().unwrap().ui_context_mut() {
5479                     ctx.ctrl_pressed = true; // Force ctrl_pressed = true for the pinch event
5480                 }
5481                 // A synthesized delta, not a scroll gesture: no glide, no fling.
5482                 crate::widget::scroll_motion::set_scroll_phase(crate::widget::ScrollPhase::Wheel);
5483 
5484                 state.inner.as_mut().unwrap().handle_mouse_wheel(&delta, LogicalPosition::new(px, py), &mut rebuild);
5485 
5486                 if let Some(ctx) = state.inner.as_mut().unwrap().ui_context_mut() {
5487                     ctx.ctrl_pressed = state.ctrl_pressed; // Restore original state
5488                 }
5489 
5490                 if rebuild {
5491                     state.redraw = true;
5492                 }
5493             }
5494             zwp_pointer_gesture_pinch_v1::Event::End { .. } => {
5495                 state.last_pinch_scale = 1.0;
5496             }
5497             _ => {}
5498         }
5499     }
5500 }
5501 
5502 /// Why a session's event loop stopped.
5503 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5504 enum SessionEnd {
5505     /// The app asked to exit.
5506     AppExit,
5507     /// The compositor connection died while the compositor itself may well be
5508     /// alive — a broken transport. The `Application` is intact and can be
5509     /// re-attached to a fresh connection.
5510     ConnectionLost,
5511     /// Nothing answered at the display socket: the compositor this app
5512     /// belonged to is gone. A deliberate exit unlinks the socket and a crash
5513     /// leaves it refusing; either way there is no session left to rejoin.
5514     NoCompositor,
5515 }
5516 
5517 /// What [`run`] does once a session has ended.
5518 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5519 enum AfterSession {
5520     /// Leave the process-lifetime loop: run `on_exit` and quit.
5521     Exit,
5522     /// Sleep this long, then open a fresh session on the same `Application`.
5523     Reconnect(std::time::Duration),
5524 }
5525 
5526 /// How many consecutive failed reconnects before giving up. Reset once a
5527 /// session has survived [`RECONNECT_RESET`], so a long-lived window that loses
5528 /// its connection twice in a day still gets a full budget the second time.
5529 const RECONNECT_ATTEMPTS: u32 = 8;
5530 const RECONNECT_RESET: std::time::Duration = std::time::Duration::from_secs(10);
5531 
5532 /// Decide whether a finished session is followed by another.
5533 ///
5534 /// `lived` is how long the session that just ended lasted, `has_app` whether
5535 /// an `Application` exists to carry over, and `attempt` the running count of
5536 /// consecutive reconnects (reset here once a session outlives
5537 /// [`RECONNECT_RESET`]).
5538 ///
5539 /// Only a lost connection is retried, and only while the compositor is still
5540 /// there to reconnect to. A reconnect is a repair of THIS session's transport
5541 /// — the fd-exhaustion break `raise_fd_limit` documents — not a way to outlive
5542 /// the compositor. When the connect itself fails the compositor has exited,
5543 /// and it has already saved this window for restore: the next compositor
5544 /// respawns the app from `state.json` on its own. A client that kept
5545 /// retrying instead (the backoff below spans ~25s) reattached to that
5546 /// successor beside the respawned copy, and every restore after a forced
5547 /// exit or a crash came up with two of each cce-ui window. So the process
5548 /// exits, as a Wayland client whose display went away always has.
5549 fn after_session(
5550     end: SessionEnd,
5551     has_app: bool,
5552     lived: std::time::Duration,
5553     attempt: &mut u32,
5554 ) -> AfterSession {
5555     match end {
5556         SessionEnd::AppExit | SessionEnd::NoCompositor => AfterSession::Exit,
5557         SessionEnd::ConnectionLost => {
5558             // Nothing to preserve if we never got as far as building the
5559             // app — that is a failure to start, not a lost window.
5560             if !has_app {
5561                 return AfterSession::Exit;
5562             }
5563             if lived > RECONNECT_RESET {
5564                 *attempt = 0;
5565             }
5566             *attempt += 1;
5567             if *attempt > RECONNECT_ATTEMPTS {
5568                 return AfterSession::Exit;
5569             }
5570             AfterSession::Reconnect(std::time::Duration::from_millis(
5571                 100 * (1 << (*attempt).min(6)),
5572             ))
5573         }
5574     }
5575 }
5576 
5577 /// Raise this process's file-descriptor soft limit toward its hard limit.
5578 ///
5579 /// A cce-ui client's fd usage is not bounded by anything the app controls.
5580 /// Every dmabuf-feedback event the compositor sends carries a format-table
5581 /// fd, and those arrive per surface whenever scanout candidacy changes —
5582 /// entering the overview re-sends one for every window at once. Long-lived
5583 /// windows sit at 700+ open fds in normal use, against a soft limit of 1024.
5584 ///
5585 /// Crossing that limit does not fail politely. `recvmsg` drops the SCM_RIGHTS
5586 /// payload when it cannot allocate descriptors, while still delivering the
5587 /// message body — so libwayland hits a message whose fd never arrived,
5588 /// reports "file descriptor expected", and the connection dies. That is
5589 /// precisely the transport break [`run`] reconnects from below, at the cost
5590 /// of a rebuilt window.
5591 ///
5592 /// The compositor raises itself to 65536 for the same reason and then
5593 /// deliberately restores the inherited limit for the programs it spawns
5594 /// (cce-compositor `process.rs::cleanup_child`) — right for an arbitrary
5595 /// child, far too low for a dmabuf-heavy Wayland client. So each client
5596 /// raises its own, to the same ceiling.
5597 fn raise_fd_limit() {
5598     unsafe {
5599         let mut lim: libc::rlimit = std::mem::zeroed();
5600         if libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) != 0 {
5601             return;
5602         }
5603         let want = std::cmp::min(65536, lim.rlim_max);
5604         if lim.rlim_cur >= want {
5605             return;
5606         }
5607         let raised = libc::rlimit { rlim_cur: want, rlim_max: lim.rlim_max };
5608         if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
5609             log::info!("[window_runner] fd limit raised {} -> {}", lim.rlim_cur, want);
5610         } else {
5611             log::warn!("[window_runner] could not raise fd limit from {}", lim.rlim_cur);
5612         }
5613     }
5614 }
5615 
5616 /// Run an [`Application`] to completion, surviving loss of the compositor
5617 /// connection.
5618 ///
5619 /// A Wayland connection cannot be repaired once its transport state breaks — a
5620 /// single dropped file descriptor on a dmabuf-feedback event is enough, and
5621 /// libwayland then fails every dispatch with `EINVAL`. Exiting the process on
5622 /// that error (the old behavior) threw away everything the window held: a
5623 /// terminal's shell and scrollback, an editor's unsaved buffer.
5624 ///
5625 /// So a connection is one *session*. Objects that belong to the connection —
5626 /// the Wayland globals, the surface, the swapchain, the renderer — are rebuilt
5627 /// per session. The things that carry user state outlive it: the `Application`
5628 /// itself, the calloop loop, and the message channel. Keeping the **same
5629 /// channel** matters as much as keeping the app: worker threads hold clones of
5630 /// its `Sender` (cce-terminal's pty reader is the canonical case), and a fresh
5631 /// channel would orphan them into a live-but-deaf process.
5632 ///
5633 /// What is repaired is the transport, never the compositor: a reconnect only
5634 /// goes through while the compositor that owned the lost session is still
5635 /// listening. If the connect itself fails the compositor has exited, and the
5636 /// process exits with it — see [`after_session`] for why staying alive there
5637 /// duplicated every window on the next session restore.
5638 ///
5639 /// Caveat: GPU resources belong to the renderer, so a rebuild re-runs
5640 /// [`Application::renderer_init`]. Images uploaded outside it (e.g. in
5641 /// [`Application::new`]) are not replayed into the new renderer — upload from
5642 /// `renderer_init` if they must survive a reconnect.
5643 pub fn run<A: Application>() {
5644     raise_fd_limit();
5645 
5646     // Outlives every session: worker threads hold this Sender, and the app's
5647     // own event sources are registered on this loop once.
5648     let (sender, channel) = calloop::channel::channel::<A::Message>();
5649     // Drop payloads come back from the per-drop reader threads (see
5650     // `backend::dnd`); registered once, like the app channel, because the
5651     // loop outlives a reconnect while the EngineState does not.
5652     let (drop_tx, drop_rx) =
5653         calloop::channel::channel::<crate::backend::dnd::DroppedData>();
5654     let mut event_loop = match EventLoop::try_new() {
5655         Ok(l) => l,
5656         Err(e) => {
5657             log::error!("[window_runner] cannot create event loop: {e}");
5658             return;
5659         }
5660     };
5661     event_loop
5662         .handle()
5663         .insert_source(channel, |event, _metadata, app_state: &mut EngineState<A>| {
5664             if let calloop::channel::Event::Msg(msg) = event {
5665                 let mut rebuild = false;
5666                 app_state.inner.as_mut().unwrap().update(msg, &mut rebuild, &mut app_state.exit);
5667                 if rebuild {
5668                     app_state.redraw = true;
5669                 }
5670             }
5671         })
5672         .unwrap();
5673     event_loop
5674         .handle()
5675         .insert_source(drop_rx, |event, _metadata, app_state: &mut EngineState<A>| {
5676             if let calloop::channel::Event::Msg(drop) = event {
5677                 // The transfer is complete, so the source can be released now
5678                 // — doing it any earlier costs the payload.
5679                 if let Some(offer) = app_state.pending_drop_offer.take() {
5680                     offer.finish();
5681                     offer.destroy();
5682                 }
5683                 let mut rebuild = false;
5684                 if let Some(app) = app_state.inner.as_mut() {
5685                     app.handle_drop(&drop.mime, &drop.bytes, drop.pos, &mut rebuild);
5686                 }
5687                 if rebuild {
5688                     app_state.redraw = true;
5689                 }
5690             }
5691         })
5692         .unwrap();
5693 
5694     let mut app: Option<A> = None;
5695     let mut sources_registered = false;
5696     let mut attempt: u32 = 0;
5697 
5698     loop {
5699         let started = std::time::Instant::now();
5700         let (returned_app, end) =
5701             run_session(&mut event_loop, sender.clone(), drop_tx.clone(), app.take(), !sources_registered);
5702         app = returned_app;
5703         sources_registered = true;
5704 
5705         match after_session(end, app.is_some(), started.elapsed(), &mut attempt) {
5706             AfterSession::Exit => {
5707                 match end {
5708                     SessionEnd::AppExit => {}
5709                     SessionEnd::NoCompositor if app.is_some() => log::warn!(
5710                         "[window_runner] compositor is gone; exiting (its successor restores the session itself)"
5711                     ),
5712                     SessionEnd::NoCompositor => {
5713                         log::error!("[window_runner] no compositor connection; giving up")
5714                     }
5715                     SessionEnd::ConnectionLost if app.is_some() => log::error!(
5716                         "[window_runner] connection lost; giving up after {} attempts",
5717                         attempt - 1
5718                     ),
5719                     SessionEnd::ConnectionLost => {
5720                         log::error!("[window_runner] no compositor connection; giving up")
5721                     }
5722                 }
5723                 break;
5724             }
5725             AfterSession::Reconnect(backoff) => {
5726                 log::warn!(
5727                     "[window_runner] compositor connection lost; reconnecting in {backoff:?} (attempt {attempt})"
5728                 );
5729                 std::thread::sleep(backoff);
5730             }
5731         }
5732     }
5733 
5734     if let Some(mut app) = app {
5735         app.on_exit();
5736     }
5737     crate::process::cleanup_spawned_processes();
5738 }
5739 
5740 /// One connection's lifetime: connect, build the surface and renderer, pump
5741 /// events until the app exits or the connection dies. Returns the
5742 /// `Application` so the caller can hand it to the next session.
5743 fn run_session<'l, A: Application>(
5744     event_loop: &mut EventLoop<'l, EngineState<A>>,
5745     sender: calloop::channel::Sender<A::Message>,
5746     drop_tx: calloop::channel::Sender<crate::backend::dnd::DroppedData>,
5747     existing_app: Option<A>,
5748     register_app_sources: bool,
5749 ) -> (Option<A>, SessionEnd) {
5750     let conn = match Connection::connect_to_env() {
5751         Ok(c) => c,
5752         Err(e) => {
5753             log::error!("[window_runner] cannot connect to compositor: {e}");
5754             return (existing_app, SessionEnd::NoCompositor);
5755         }
5756     };
5757     let (globals, mut event_queue) = match registry_queue_init(&conn) {
5758         Ok(v) => v,
5759         Err(e) => {
5760             log::error!("[window_runner] registry init failed: {e}");
5761             return (existing_app, SessionEnd::ConnectionLost);
5762         }
5763     };
5764     let qh = event_queue.handle();
5765 
5766     let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
5767     let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
5768     let layer_shell_state = LayerShell::bind(&globals, &qh).ok();
5769     let shm_state = Shm::bind(&globals, &qh).unwrap();
5770     let seat_state = SeatState::new(&globals, &qh);
5771     let output_state = OutputState::new(&globals, &qh);
5772 
5773     let pointer_gestures: Option<ZwpPointerGesturesV1> = globals.bind(&qh, 1..=3, ()).ok();
5774 
5775     let mut engine_state = EngineState {
5776         data_device_manager: DataDeviceManagerState::bind(&globals, &qh).ok(),
5777         data_devices: Vec::new(),
5778         drag_mime: None,
5779         drag_pos: LogicalPosition::new(0.0, 0.0),
5780         drop_tx: Some(drop_tx),
5781         pending_drop_offer: None,
5782         applied_input_regions: None,
5783         registry_state: RegistryState::new(&globals),
5784         compositor_state,
5785         xdg_shell_state,
5786         layer_shell_state,
5787         shm_state,
5788         seat_state,
5789         output_state,
5790         seats: Vec::new(),
5791         pointer: None,
5792         keyboard: None,
5793         window: None,
5794         layer_surface: None,
5795         surface: None,
5796         inner: None,
5797         renderer: None,
5798         font_system: None,
5799         swash_cache: cosmic_text::SwashCache::new(),
5800         scale_factor: 1.0,
5801         committed_buffer_scale: 1,
5802         entered_outputs: Vec::new(),
5803         logical_width: 0.0,
5804         logical_height: 0.0,
5805         frame_logical: (0.0, 0.0),
5806         applied_margin: 0.0,
5807         overflow_was_active: false,
5808         sent_popover_region: None,
5809         exit: false,
5810         redraw: false,
5811         frame_callback_pending: false,
5812         frame_callback_armed_at: None,
5813         warm_until: None,
5814         extent_gate_skips: 0,
5815         first_configure_received: false,
5816         ctrl_pressed: false,
5817         undo_chord: crate::input::app_chord("undo", "ctrl+z"),
5818         redo_chord: crate::input::app_chord("redo", "ctrl+shift+z"),
5819         group_next_chord: crate::input::app_chord("focus_next_group", "ctrl+tab"),
5820         group_prev_chord: crate::input::app_chord("focus_prev_group", "ctrl+shift+tab"),
5821         shift_pressed: false,
5822         alt_pressed: false,
5823         logo_pressed: false,
5824         pressed_key: None,
5825         sender,
5826         current_cursor_icon: None,
5827         qh: qh.clone(),
5828         just_configured: false,
5829         pointer_gestures,
5830         pinch_gesture: None,
5831         cce_toplevel: None,
5832         pending_grid_patch: None,
5833         last_pinch_scale: 1.0,
5834         cursor_pos: (0.0, 0.0),
5835         last_press_serial: None,
5836         buttons_down: 0,
5837         dl_text_items: Vec::new(),
5838     };
5839 
5840     if let Err(e) = event_queue.roundtrip(&mut engine_state) {
5841         log::error!("[window_runner] initial roundtrip failed: {e}");
5842         return (existing_app, SessionEnd::ConnectionLost);
5843     }
5844 
5845     let scale = detect_scale_factor(&engine_state.output_state);
5846     engine_state.scale_factor = scale;
5847     crate::scale::set_scale_factor(scale as f32);
5848     crate::units::set_metric(crate::wayland::detect_metric(&engine_state.output_state, scale));
5849 
5850     // A reconnect re-attaches the SAME app: its state is the thing worth
5851     // saving, and `A::new` would both discard it and hand a fresh Sender to
5852     // worker threads that are still holding the original.
5853     let inner = match existing_app {
5854         Some(app) => app,
5855         None => A::new(&qh, engine_state.sender.clone()),
5856     };
5857     let settings = inner.settings();
5858     crate::scale::set_app_id(settings.app_id.clone());
5859     engine_state.logical_width = settings.width as f32;
5860     engine_state.logical_height = settings.height as f32;
5861     engine_state.inner = Some(inner);
5862 
5863     let surface = engine_state.compositor_state.create_surface(&qh);
5864     // A grid app's surface is pinned to scale 1: the patch's `scale` is
5865     // BUFFER px per virtual unit and already carries the output scale (the
5866     // patch manager folds it in), so adopting the output scale here would
5867     // square it — the client renders a doubled buffer and the compositor
5868     // downsamples it straight back into blur.
5869     if engine_state.inner.as_ref().unwrap().grid() {
5870         engine_state.scale_factor = 1.0;
5871     }
5872     // Forced-scale mode renders scaled-up into a buffer_scale-1 surface.
5873     let buffer_scale = if crate::scale::forced_scale().is_some()
5874         || engine_state.inner.as_ref().unwrap().grid()
5875     {
5876         1
5877     } else {
5878         scale as i32
5879     };
5880     surface.set_buffer_scale(buffer_scale);
5881     engine_state.committed_buffer_scale = buffer_scale;
5882 
5883     if settings.app_id.starts_with("cce-status") {
5884         let compositor = engine_state.compositor_state.wl_compositor();
5885         let region = compositor.create_region(&qh, ());
5886         region.add(0, 0, settings.width as i32, settings.height as i32);
5887         surface.set_input_region(Some(&region));
5888         region.destroy();
5889     }
5890 
5891     let layer_settings = engine_state.inner.as_ref().unwrap().layer();
5892     if let Some(ls) = layer_settings {
5893         let layer_shell = engine_state
5894             .layer_shell_state
5895             .as_ref()
5896             .expect("compositor does not support wlr-layer-shell");
5897         let layer_surface = layer_shell.create_layer_surface(
5898             &qh,
5899             surface.clone(),
5900             ls.layer,
5901             Some(ls.namespace.clone()),
5902             None,
5903         );
5904         layer_surface.set_anchor(ls.anchor);
5905         layer_surface.set_exclusive_zone(ls.exclusive_zone);
5906         layer_surface.set_keyboard_interactivity(ls.keyboard_interactivity);
5907         let (t, r, b, l) = ls.margin;
5908         layer_surface.set_margin(t, r, b, l);
5909         layer_surface.set_size(settings.width, settings.height);
5910         layer_surface.commit();
5911         engine_state.layer_surface = Some(layer_surface);
5912     } else {
5913         let window = engine_state.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
5914         window.set_title(&settings.title);
5915         window.set_app_id(&settings.app_id);
5916         if settings.fullscreen {
5917             window.set_fullscreen(None);
5918         }
5919         if let Some((min_w, min_h)) = settings.min_size {
5920             window.set_min_size(Some((min_w, min_h)));
5921         }
5922         let wants_utility = engine_state.inner.as_ref().unwrap().utility();
5923         let wants_grid = engine_state.inner.as_ref().unwrap().grid();
5924         {
5925             // Bound for EVERY app now, not just utility/grid ones: the
5926             // toplevel also carries the popover-region hint (manager v7),
5927             // which any app with a dropdown wants. Role declarations go
5928             // BEFORE the initial commit so the mode is set by the time the
5929             // compositor maps the window. Version floors: set_utility
5930             // appeared at manager 5, the grid role at 6; the range tops at 7
5931             // so a newer compositor grants the hint and an older one simply
5932             // yields a lower-versioned toplevel — the hint send is gated on
5933             // version() >= 7 (send_popover_region), and on a pre-5
5934             // compositor the bind fails and the app runs plain.
5935             let version = if wants_grid { 6..=7 } else { 5..=7 };
5936             match globals.bind::<crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1, _, _>(&qh, version, ()) {
5937                 Ok(cce_wm) => {
5938                     let toplevel = cce_wm.get_cce_toplevel(&surface, &qh, ());
5939                     if wants_utility {
5940                         toplevel.set_utility();
5941                     }
5942                     if wants_grid {
5943                         toplevel.set_grid();
5944                     }
5945                     engine_state.cce_toplevel = Some(toplevel);
5946                 }
5947                 Err(e) => {
5948                     log::warn!("[window_runner] cce window-management declaration unavailable: {e}");
5949                 }
5950             }
5951         }
5952         window.commit();
5953         engine_state.window = Some(window);
5954     }
5955     engine_state.surface = Some(surface);
5956 
5957     // Overflow-margin mode: the surface (and so the GPU swapchain) is a rim
5958     // larger than the window frame on every side; geometry/input-region are
5959     // published per-resize.
5960     let rim = 2.0 * engine_state.inner.as_ref().unwrap().overflow_margin() as f32;
5961     engine_state.init_gpu(&conn, settings.width as f32 + rim, settings.height as f32 + rim);
5962     engine_state
5963         .inner
5964         .as_mut()
5965         .unwrap()
5966         .renderer_init(engine_state.renderer.as_mut().unwrap());
5967 
5968     let loop_handle = event_loop.handle();
5969     let wayland_token = match WaylandSource::new(conn.clone(), event_queue).insert(loop_handle.clone())
5970     {
5971         Ok(token) => token,
5972         Err(e) => {
5973             log::error!("[window_runner] cannot register the wayland source: {e}");
5974             return (engine_state.inner.take(), SessionEnd::ConnectionLost);
5975         }
5976     };
5977 
5978     // The app's own sources live on the persistent loop, so they are registered
5979     // once for the process — re-registering per session would double-deliver
5980     // every event on them.
5981     if register_app_sources {
5982         engine_state.inner.as_mut().unwrap().register_sources(&loop_handle);
5983     }
5984 
5985     const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
5986     const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
5987 
5988     /// Same switch as the renderer's present tracer, resolved once — this sits
5989     /// in the per-iteration path, so a `std::env::var` call here would be I/O
5990     /// on the loop that is under measurement.
5991     fn loop_debug() -> bool {
5992         static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5993         *FLAG.get_or_init(|| std::env::var_os("CCE_PRESENT_DEBUG").is_some())
5994     }
5995 
5996     /// Loop cadence while something is in motion: one tick per frame.
5997     const ACTIVE_DISPATCH: std::time::Duration = std::time::Duration::from_millis(16);
5998 
5999     /// Upper bound on an idle sleep. The loop is woken early by any Wayland
6000     /// event or calloop-channel message, so this only caps how long an
6001     /// app-side poll that bypasses both (see `Application::idle_poll_interval`)
6002     /// can wait. `CCE_UI_IDLE_MS` overrides it — `16` restores the old
6003     /// always-ticking loop for a bisect.
6004     fn idle_dispatch() -> std::time::Duration {
6005         static IDLE: std::sync::OnceLock<std::time::Duration> = std::sync::OnceLock::new();
6006         *IDLE.get_or_init(|| {
6007             std::env::var("CCE_UI_IDLE_MS")
6008                 .ok()
6009                 .and_then(|v| v.parse::<u64>().ok())
6010                 .map(std::time::Duration::from_millis)
6011                 .unwrap_or(IDLE_DISPATCH)
6012         })
6013     }
6014 
6015     /// Seconds after session start at which to inject a simulated connection
6016     /// loss, from `CCE_UI_FAULT_RECONNECT`. Resolved once: this is read from
6017     /// the per-iteration path.
6018     fn fault_reconnect_after() -> Option<std::time::Duration> {
6019         static AFTER: std::sync::OnceLock<Option<std::time::Duration>> =
6020             std::sync::OnceLock::new();
6021         *AFTER.get_or_init(|| {
6022             std::env::var("CCE_UI_FAULT_RECONNECT")
6023                 .ok()
6024                 .and_then(|v| v.parse::<f32>().ok())
6025                 .map(std::time::Duration::from_secs_f32)
6026         })
6027     }
6028 
6029     let mut last_title = settings.title.clone();
6030     let mut last_tick = std::time::Instant::now();
6031     let mut end = SessionEnd::AppExit;
6032     let session_start = std::time::Instant::now();
6033     // The loop's cadence. ACTIVE while anything is in motion (a redraw
6034     // pending or just done, an animation, a held key, the post-activity
6035     // warm-down); otherwise the app's own poll interval or IDLE_DISPATCH.
6036     // Before 2026-09-11 this was a flat 16 ms whatever the state: every
6037     // cce-ui client woke 60 times a second forever — ~1200 wakeups/s across
6038     // a session's twenty clients — and each wake ran tick, desired_size,
6039     // title and margin checks for nothing.
6040     let mut next_timeout = ACTIVE_DISPATCH;
6041     let mut slept_idle = false;
6042     loop {
6043         // Frame callbacks arrive with a p50 of 0ms but a ~0.5s tail, while the
6044         // compositor's own trace shows it firing them within one or two vsyncs
6045         // of the arm. Tracing each iteration bisects that: if this loop keeps
6046         // turning at ~16ms all through a long wait, the event was not there to
6047         // read, and the delay is upstream rather than in dispatching it.
6048         let iter_start = if loop_debug() {
6049             Some(std::time::Instant::now())
6050         } else {
6051             None
6052         };
6053         if let Err(e) = event_loop.dispatch(next_timeout, &mut engine_state) {
6054             log::error!("[window_runner] event loop error, ending session: {e:?}");
6055             end = SessionEnd::ConnectionLost;
6056             break;
6057         }
6058         if let Some(start) = iter_start {
6059             let t = std::time::SystemTime::now()
6060                 .duration_since(std::time::UNIX_EPOCH)
6061                 .unwrap()
6062                 .as_millis()
6063                 % 100000;
6064             eprintln!(
6065                 "[vk] t={} loop dispatch={}us pending_cb={}",
6066                 t,
6067                 start.elapsed().as_micros(),
6068                 engine_state.frame_callback_pending
6069             );
6070         }
6071         // A protocol error kills the connection permanently, but it surfaces
6072         // through queue flushes whose errors calloop's WaylandSource swallows
6073         // (it only treats Io errors as fatal) — without this check the loop
6074         // spins forever on a dead display while wayland-backend re-prints the
6075         // error on every flush attempt.
6076         if let Some(perr) = conn.protocol_error() {
6077             log::error!("[window_runner] wayland protocol error, ending session: {perr}");
6078             end = SessionEnd::ConnectionLost;
6079             break;
6080         }
6081         // Fault injection for the reconnect path (`CCE_UI_FAULT_RECONNECT=<secs>`):
6082         // real connection loss is a rare race that cannot be provoked on demand,
6083         // so this drops the session exactly as a transport error would. One-shot
6084         // per process, so the app reconnects and then stays up.
6085         if let Some(after) = fault_reconnect_after() {
6086             static FIRED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
6087             if session_start.elapsed() >= after
6088                 && !FIRED.swap(true, std::sync::atomic::Ordering::Relaxed)
6089             {
6090                 log::warn!("[window_runner] CCE_UI_FAULT_RECONNECT: dropping the session");
6091                 end = SessionEnd::ConnectionLost;
6092                 break;
6093             }
6094         }
6095         if engine_state.exit {
6096             // The close dissolve. It is the COMPOSITOR that fades us — it
6097             // ramps our scene subtree's opacity, which takes the backdrop
6098             // blur, drop shadow and bevel down with the window; all this side
6099             // has to do is not vanish before it finishes. So keep the surface
6100             // mapped and the loop turning for exactly as long as the
6101             // compositor asked for, then leave. Dispatching (rather than
6102             // sleeping) keeps the connection pumped and lets any last
6103             // animation finish on screen while the window dissolves.
6104             let fade = crate::ipc::request_close_fade();
6105             if !fade.is_zero() {
6106                 let until = std::time::Instant::now() + fade;
6107                 loop {
6108                     let left = until.saturating_duration_since(std::time::Instant::now());
6109                     if left.is_zero() {
6110                         break;
6111                     }
6112                     if event_loop.dispatch(left.min(ACTIVE_DISPATCH), &mut engine_state).is_err() {
6113                         break;
6114                     }
6115                 }
6116             }
6117             break;
6118         }
6119 
6120         let now = std::time::Instant::now();
6121         let mut dt = now.duration_since(last_tick).as_secs_f32();
6122         last_tick = now;
6123         if dt > 0.1 {
6124             dt = 0.1;
6125         }
6126         // Waking from an idle sleep: the interval is not animation time. An
6127         // animation an event just started must take its first step at frame
6128         // size, not leap 100 ms in one tick.
6129         if slept_idle {
6130             dt = dt.min(1.0 / 60.0);
6131         }
6132 
6133         let mut rebuild = false;
6134         let roster_ticks_before =
6135             engine_state.inner.as_mut().unwrap().ui_context_mut().map(|ctx| ctx.tick_count());
6136         engine_state.inner.as_mut().unwrap().tick(dt, &mut rebuild);
6137         if rebuild {
6138             engine_state.redraw = true;
6139         }
6140         // Tick the app's retained UiContext (widget tick_receivers — e.g. an
6141         // animating Dropdown popover) for apps that expose it — but only when
6142         // the app's own tick did not already do so this frame. Receivers
6143         // integrate `dt` (scroll glides, slider inertia), so the old
6144         // "double-ticking is harmless" assumption ran every glide at twice
6145         // its configured rate in apps that tick the context themselves.
6146         if let Some(ctx) = engine_state.inner.as_mut().unwrap().ui_context_mut() {
6147             if Some(ctx.tick_count()) == roster_ticks_before {
6148                 if ctx.tick(dt) {
6149                     engine_state.redraw = true;
6150                 }
6151             }
6152         }
6153 
6154         let just_configured = engine_state.just_configured;
6155         engine_state.just_configured = false;
6156 
6157         if !just_configured {
6158             if let Some((w, h)) = engine_state.inner.as_ref().unwrap().desired_size() {
6159                 // desired_size is a window-frame size; the surface adds the
6160                 // right/bottom overflow rim (0 for margin-less apps).
6161                 let m = engine_state.inner.as_ref().unwrap().overflow_margin() as f32;
6162                 let (sw, sh) = (w as f32 + m, h as f32 + m);
6163                 if (engine_state.logical_width - sw).abs() > 0.001 || (engine_state.logical_height - sh).abs() > 0.001 {
6164                     engine_state.frame_logical = (w as f32, h as f32);
6165                     engine_state.applied_margin = m;
6166                     engine_state.resize(sw, sh);
6167                     engine_state.redraw = true;
6168                 }
6169             }
6170         }
6171 
6172         // Overflow-margin drift (configure-sized apps): the rim can change at
6173         // runtime — a popover overhanging the window frame — so re-derive the
6174         // surface from the stored frame whenever the app's answer moves. While
6175         // the rim is live, re-publish geometry every loop: the input region
6176         // tracks the animating popover rects.
6177         {
6178             let m_now = engine_state.inner.as_ref().unwrap().overflow_margin() as f32;
6179             if (m_now - engine_state.applied_margin).abs() > 0.001 && engine_state.frame_logical.0 > 0.0 {
6180                 engine_state.applied_margin = m_now;
6181                 let (fw, fh) = engine_state.frame_logical;
6182                 engine_state.resize(fw + m_now, fh + m_now);
6183                 engine_state.redraw = true;
6184             }
6185             if engine_state.applied_margin > 0.0 || engine_state.overflow_was_active {
6186                 engine_state.publish_window_geometry();
6187                 engine_state.overflow_was_active = engine_state.applied_margin > 0.0;
6188             }
6189             engine_state.send_popover_region();
6190         }
6191 
6192         if let Some(ref mut pk) = engine_state.pressed_key {
6193             let now = std::time::Instant::now();
6194             if now.duration_since(pk.first_pressed) >= KEY_REPEAT_DELAY {
6195                 if now.duration_since(pk.last_repeated) >= KEY_REPEAT_INTERVAL {
6196                     pk.last_repeated = now;
6197                     let custom_event = KeyEvent {
6198                         state: ElementState::Pressed,
6199                         logical_key: pk.logical_key.clone(),
6200                         text: pk.text.clone(),
6201                         repeat: true,
6202                         ctrl: engine_state.ctrl_pressed,
6203                         shift: engine_state.shift_pressed,
6204                         alt: engine_state.alt_pressed,
6205                     };
6206 
6207                     if let Some(ctx) = engine_state.inner.as_mut().unwrap().ui_context_mut() {
6208                         ctx.ctrl_pressed = engine_state.ctrl_pressed;
6209                         ctx.shift_pressed = engine_state.shift_pressed;
6210                         ctx.alt_pressed = engine_state.alt_pressed;
6211                         ctx.logo_pressed = engine_state.logo_pressed;
6212                     }
6213 
6214                     let mut key_rebuild = false;
6215                     if engine_state.route_history_chord(&custom_event, &mut key_rebuild)
6216                         || engine_state.route_plate_navigation(&custom_event, &mut key_rebuild)
6217                     {
6218                         engine_state.redraw = true;
6219                     } else if let Some(msg) = engine_state.inner.as_mut().unwrap().handle_key_input(&custom_event, &mut key_rebuild) {
6220                         let mut update_rebuild = false;
6221                         engine_state.inner.as_mut().unwrap().update(msg, &mut update_rebuild, &mut engine_state.exit);
6222                         if update_rebuild {
6223                             key_rebuild = true;
6224                         }
6225                     }
6226                     if key_rebuild {
6227                         engine_state.redraw = true;
6228                     }
6229                 }
6230             }
6231         }
6232         let current_title = engine_state.inner.as_ref().unwrap().settings().title;
6233         if current_title != last_title {
6234             if let Some(ref window) = engine_state.window {
6235                 window.set_title(&current_title);
6236                 window.commit();
6237             }
6238             last_title = current_title;
6239         }
6240 
6241         // Frame-callback starvation fallback: the compositor only sends
6242         // frame-done for surfaces it actually renders, so a callback armed
6243         // while the window sat off-viewport (or the scene went static) may
6244         // never fire — and the vsync gate below then freezes the app forever
6245         // with a perfectly live event loop (input processes, state changes,
6246         // nothing repaints). If a redraw has been waiting on a callback well
6247         // past any real vsync interval, stop waiting and draw.
6248         //
6249         // Gated on the renderer's present mode: forcing a present past a
6250         // dead callback is only safe under MAILBOX (the present replaces the
6251         // queued buffer). Under FIFO the driver's throttle waits on the
6252         // previous present's frame event, so the forced present itself
6253         // blocks forever inside the driver — the exact freeze this fallback
6254         // exists to prevent. There the gate stays closed: pixels may stale
6255         // until the next frame-done/configure, but the loop stays alive.
6256         if engine_state.redraw
6257             && engine_state.frame_callback_pending
6258             && engine_state
6259                 .renderer
6260                 .as_ref()
6261                 .is_some_and(|r| r.forced_present_safe())
6262             && engine_state
6263                 .frame_callback_armed_at
6264                 .is_none_or(|t| t.elapsed().as_millis() > 250)
6265         {
6266             engine_state.frame_callback_pending = false;
6267             if std::env::var("CCE_PRESENT_DEBUG").is_ok() {
6268                 let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() % 100000;
6269                 eprintln!("[vk] t={} starvation fallback fired (callback never came)", t);
6270             }
6271         }
6272 
6273         if engine_state.redraw {
6274             // Genuine dirt (input, app state, animation) extends the warm window;
6275             // warm-down renders below do NOT, so idle decays in one window.
6276             engine_state.warm_until =
6277                 Some(std::time::Instant::now() + std::time::Duration::from_millis(200));
6278         }
6279         let mut rendered = false;
6280         if engine_state.redraw && !engine_state.frame_callback_pending {
6281             engine_state.redraw = false;
6282             if engine_state.first_configure_received {
6283                 engine_state.render();
6284                 rendered = true;
6285             }
6286         } else if !engine_state.redraw
6287             && !engine_state.frame_callback_pending
6288             && engine_state
6289                 .warm_until
6290                 .is_some_and(|t| std::time::Instant::now() < t)
6291         {
6292             // Warm-down re-render of the cached frame, paced by frame callbacks.
6293             if engine_state.first_configure_received {
6294                 engine_state.render();
6295                 rendered = true;
6296             }
6297         }
6298 
6299         // Anything still moving keeps the frame cadence; a frame callback
6300         // outstanding on its own does not (it arrives as an event) unless a
6301         // redraw is queued behind it, which is what the starvation fallback
6302         // above times. `redraw` still set here means the frame was withheld
6303         // (callback pending, or no configure yet) and must be retried soon.
6304         let warm = engine_state
6305             .warm_until
6306             .is_some_and(|t| std::time::Instant::now() < t);
6307         let busy = engine_state.redraw || rendered || warm || engine_state.pressed_key.is_some();
6308         next_timeout = if busy {
6309             ACTIVE_DISPATCH
6310         } else {
6311             let app_poll = engine_state.inner.as_ref().unwrap().idle_poll_interval();
6312             app_poll.map_or(idle_dispatch(), |d| d.min(idle_dispatch()))
6313         };
6314         slept_idle = !busy;
6315     }
6316 
6317     // Tear the session down: drop its Wayland source from the persistent loop
6318     // (leaving it would leak a dead source per reconnect), then hand the app
6319     // back before `engine_state` drops the renderer and the surface with it.
6320     // `on_exit` and process cleanup belong to the app's real exit, in `run`.
6321     loop_handle.remove(wayland_token);
6322     let app = engine_state.inner.take();
6323     drop(engine_state);
6324     (app, end)
6325 }
6326 
6327 // `all(test, debug_assertions)`: the function under test only exists in
6328 // debug builds, so a `cargo test --release` must compile the module out too.
6329 #[cfg(all(test, debug_assertions))]
6330 mod near_roll_fallback_tests {
6331     use super::near_roll_fallback_reason;
6332     use crate::scene::layout::Rect;
6333 
6334     fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
6335         Rect { x, y, width: w, height: h }
6336     }
6337 
6338     const HOST: Rect = Rect { x: 0.0, y: 0.0, width: 800.0, height: 600.0 };
6339     const ROLL: f32 = 8.0;
6340 
6341     #[test]
6342     fn interior_carve_is_quiet() {
6343         // Well inside the deflated host: the overlay fallback is exact there.
6344         let carve = r(100.0, 100.0, 200.0, 100.0);
6345         assert_eq!(near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[], false), None);
6346     }
6347 
6348     #[test]
6349     fn shaded_region_reaching_the_roll_is_loud() {
6350         // Carve rect stops 3px short of the roll band, but its shaded region
6351         // (depth*0.5 + 2 = 5px) crosses in — the inflation must count.
6352         let carve = r(ROLL + 3.0, 100.0, 200.0, 100.0);
6353         assert_eq!(
6354             near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[], false),
6355             Some("the host's feature run is closed (another plate appended features since)")
6356         );
6357     }
6358 
6359     #[test]
6360     fn occlusion_is_named_before_run_contiguity() {
6361         let carve = r(2.0, 100.0, 200.0, 100.0);
6362         let occluder = r(150.0, 150.0, 100.0, 100.0);
6363         assert_eq!(
6364             near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[occluder], false),
6365             Some("a later plate overlaps the carve's shaded region")
6366         );
6367     }
6368 
6369     #[test]
6370     fn non_overlapping_later_plate_is_not_occlusion() {
6371         let carve = r(2.0, 100.0, 200.0, 100.0);
6372         let elsewhere = r(500.0, 400.0, 100.0, 100.0);
6373         assert_eq!(
6374             near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[elsewhere], false),
6375             Some("the host's feature run is closed (another plate appended features since)")
6376         );
6377     }
6378 
6379     #[test]
6380     fn budget_wins_over_every_other_reason() {
6381         let carve = r(2.0, 100.0, 200.0, 100.0);
6382         let occluder = r(150.0, 150.0, 100.0, 100.0);
6383         assert_eq!(
6384             near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[occluder], true),
6385             Some("the feature budget is full")
6386         );
6387     }
6388 }
6389 
6390 #[cfg(test)]
6391 mod reconnect_tests {
6392     use super::{after_session, AfterSession, SessionEnd, RECONNECT_ATTEMPTS, RECONNECT_RESET};
6393     use std::time::Duration;
6394 
6395     const LONG: Duration = Duration::from_secs(60);
6396     const SHORT: Duration = Duration::from_millis(50);
6397 
6398     #[test]
6399     fn app_exit_ends_the_process() {
6400         let mut attempt = 0;
6401         assert_eq!(after_session(SessionEnd::AppExit, true, LONG, &mut attempt), AfterSession::Exit);
6402         assert_eq!(attempt, 0);
6403     }
6404 
6405     #[test]
6406     fn lost_transport_reconnects_with_backoff() {
6407         let mut attempt = 0;
6408         assert_eq!(
6409             after_session(SessionEnd::ConnectionLost, true, LONG, &mut attempt),
6410             AfterSession::Reconnect(Duration::from_millis(200))
6411         );
6412         assert_eq!(attempt, 1);
6413         assert_eq!(
6414             after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt),
6415             AfterSession::Reconnect(Duration::from_millis(400))
6416         );
6417         assert_eq!(attempt, 2);
6418     }
6419 
6420     /// The compositor exited (its socket is unlinked, or refusing after a
6421     /// crash). It saved this window for restore, so the successor respawns
6422     /// the app itself; a client that waited for it reattached beside the
6423     /// respawned copy, and the restore came up with two of every window.
6424     #[test]
6425     fn compositor_gone_exits_instead_of_waiting_for_a_successor() {
6426         let mut attempt = 0;
6427         assert_eq!(
6428             after_session(SessionEnd::NoCompositor, true, LONG, &mut attempt),
6429             AfterSession::Exit
6430         );
6431         // Even mid-budget: a reconnect that finds nobody listening is the
6432         // compositor leaving, not another transport break.
6433         let mut attempt = 3;
6434         assert_eq!(
6435             after_session(SessionEnd::NoCompositor, true, SHORT, &mut attempt),
6436             AfterSession::Exit
6437         );
6438     }
6439 
6440     #[test]
6441     fn nothing_to_carry_over_gives_up() {
6442         let mut attempt = 0;
6443         assert_eq!(
6444             after_session(SessionEnd::ConnectionLost, false, SHORT, &mut attempt),
6445             AfterSession::Exit
6446         );
6447         assert_eq!(
6448             after_session(SessionEnd::NoCompositor, false, SHORT, &mut attempt),
6449             AfterSession::Exit
6450         );
6451     }
6452 
6453     #[test]
6454     fn budget_is_bounded_and_resets_after_a_long_session() {
6455         let mut attempt = 0;
6456         for _ in 0..RECONNECT_ATTEMPTS {
6457             assert!(matches!(
6458                 after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt),
6459                 AfterSession::Reconnect(_)
6460             ));
6461         }
6462         assert_eq!(
6463             after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt),
6464             AfterSession::Exit
6465         );
6466         // A session that outlived the reset window earns a fresh budget.
6467         assert_eq!(
6468             after_session(SessionEnd::ConnectionLost, true, RECONNECT_RESET + SHORT, &mut attempt),
6469             AfterSession::Reconnect(Duration::from_millis(200))
6470         );
6471         assert_eq!(attempt, 1);
6472     }
6473 
6474     #[test]
6475     fn backoff_caps_at_six_point_four_seconds() {
6476         let mut attempt = 6;
6477         assert_eq!(
6478             after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt),
6479             AfterSession::Reconnect(Duration::from_millis(6400))
6480         );
6481         assert_eq!(
6482             after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt),
6483             AfterSession::Reconnect(Duration::from_millis(6400))
6484         );
6485     }
6486 }