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

src/widget/mod.rs (39.7K)

   1 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
   2 pub enum ElementState {
   3     Pressed,
   4     Released,
   5 }
   6 
   7 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
   8 pub enum MouseButton {
   9     Left,
  10     Right,
  11     Middle,
  12     Back,
  13     Forward,
  14     Other(u16),
  15 }
  16 
  17 #[derive(Debug, Clone, Copy, PartialEq)]
  18 pub struct Position {
  19     pub x: f64,
  20     pub y: f64,
  21 }
  22 
  23 #[derive(Debug, Clone, Copy, PartialEq)]
  24 pub enum MouseScrollDelta {
  25     LineDelta(f32, f32),
  26     PixelDelta(Position),
  27 }
  28 
  29 impl MouseScrollDelta {
  30     /// Vertical scroll in wheel-notch equivalents for VALUE widgets (sliders,
  31     /// float3 rows). The pixel divisor is calibrated against a measured
  32     /// trackpad stream, not a notch convention: a real two-finger swipe
  33     /// delivers 10–20 axis units per event at 6–8ms intervals (~2000
  34     /// units/sec sustained). At 60 units per notch-equivalent (0.02 of the
  35     /// range each), that sustains ~0.6 range/sec — a full sweep is a couple
  36     /// of committed swipes, while slow fine-tuning events (2–5 units) move
  37     /// well under one readout tick. 15 (the DE's hardware-notch unit) slams
  38     /// bound-to-bound in ~150ms; 120 (the wheel standard) needs ~6000px of
  39     /// finger travel per sweep.
  40     pub fn notches_y(&self) -> f32 {
  41         match self {
  42             MouseScrollDelta::LineDelta(_x, y) => *y,
  43             MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 60.0,
  44         }
  45     }
  46 
  47     /// The notches a VALUE control takes, "up is more": a wheel notch up
  48     /// is positive, and a finger's travel is positive when the fingers
  49     /// went UP — which under natural scrolling is the negative of the
  50     /// pixel delta, since that delta is what a list scrolls by and a
  51     /// natural list moves its content the way the fingers went. Until
  52     /// 2026-09-30 every value control read `notches_y` and each had picked
  53     /// a sign: the slider was right for a natural trackpad and backwards
  54     /// for a wheel, the spinbox and the menu and palette sliders the other
  55     /// way round.
  56     pub fn value_notches_y(&self) -> f32 {
  57         self.value_notches_of(crate::input::natural_scroll())
  58     }
  59 
  60     /// [`Self::value_notches_y`] for a given natural-scroll setting.
  61     pub fn value_notches_of(&self, natural: bool) -> f32 {
  62         match self {
  63             MouseScrollDelta::LineDelta(_x, y) => *y,
  64             MouseScrollDelta::PixelDelta(pos) => {
  65                 let n = (pos.y as f32) / 60.0;
  66                 if natural { -n } else { n }
  67             }
  68         }
  69     }
  70 }
  71 
  72 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
  73 pub enum Key {
  74     Named(NamedKey),
  75     Character(String),
  76 }
  77 
  78 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  79 pub enum NamedKey {
  80     Backspace,
  81     Tab,
  82     Enter,
  83     Escape,
  84     Space,
  85     ArrowDown,
  86     ArrowLeft,
  87     ArrowRight,
  88     ArrowUp,
  89     End,
  90     Home,
  91     PageDown,
  92     PageUp,
  93     Delete,
  94     Control,
  95     Shift,
  96     Alt,
  97     Super,
  98     // The function keys. F5 came first (the login greeter's restart); the
  99     // rest arrived together on 2026-09-25 for the greeter's F1 power off and
 100     // F2 reboot.
 101     F1,
 102     F2,
 103     F3,
 104     F4,
 105     F5,
 106     F6,
 107     F7,
 108     F8,
 109     F9,
 110     F10,
 111     F11,
 112     F12,
 113 }
 114 
 115 #[derive(Debug, Clone, PartialEq, Eq)]
 116 pub struct KeyEvent {
 117     pub state: ElementState,
 118     pub logical_key: Key,
 119     pub text: Option<String>,
 120     pub repeat: bool,
 121     pub ctrl: bool,
 122     pub shift: bool,
 123     pub alt: bool,
 124 }
 125 
 126 /// Text justification for widget labels/content (shared by Button, cce-files' row
 127 /// list, and settings; formerly defined by the dissolved json_layout host).
 128 #[derive(serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
 129 #[serde(rename_all = "lowercase")]
 130 pub enum Justification {
 131     Left,
 132     Center,
 133     Right,
 134 }
 135 
 136 /// A context-menu action dispatched on the menu's target widget (6bd phase 1: one enum
 137 /// replaces the 13 per-action `WidgetHost` methods). `ClearText` is the search-box "Cear" item.
 138 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 139 pub enum ContextAction {
 140     Cut,
 141     Copy,
 142     Paste,
 143     SelectAll,
 144     /// Step the widget's own edit history (a text box's typing). Routed by
 145     /// the runner to the focused widget on the `undo` / `redo` chords before
 146     /// the app's `Application::undo` / `redo` get their turn; also reachable
 147     /// as "Undo" / "Redo" context-menu rows.
 148     Undo,
 149     Redo,
 150     ClearText,
 151     CopyKey,
 152     CopyValue,
 153     CopyPath,
 154     DeleteKey,
 155     ExpandNode,
 156     CollapseNode,
 157     ExpandAll,
 158     CollapseAll,
 159     /// Ramp: hide/show the bottom control strip, the graph claiming the space.
 160     ToggleRampControls,
 161 }
 162 
 163 use crate::colors;
 164 use std::sync::atomic::AtomicUsize;
 165 use std::collections::HashMap;
 166 
 167 pub const DROPDOWN_ITEM_H: f32 = 22.0;
 168 
 169 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 170 pub struct WidgetId(pub usize);
 171 
 172 pub static NEXT_WIDGET_ID: AtomicUsize = AtomicUsize::new(1);
 173 
 174 #[derive(Debug, Clone)]
 175 pub struct LayoutTree {
 176     pub parents: HashMap<WidgetId, WidgetId>,
 177     pub children: HashMap<WidgetId, Vec<WidgetId>>,
 178 }
 179 
 180 pub use crate::scene::paint::{ControlPlate, PlateStance};
 181 pub use crate::widget::model::FocusRole;
 182 pub use crate::context::UiContext;
 183 
 184 #[derive(Debug, Clone, PartialEq)]
 185 pub enum Event {
 186     PointerMove { x: f32, y: f32, local_x: f32, local_y: f32 },
 187     MouseButton { button: MouseButton, state: ElementState, x: f32, y: f32, local_x: f32, local_y: f32 },
 188     MouseWheel { delta: MouseScrollDelta, x: f32, y: f32, local_x: f32, local_y: f32 },
 189     KeyInput(KeyEvent),
 190     Tick(f32),
 191 
 192     MouseEnter,
 193     MouseLeave,
 194     DragStart { start_x: f32, start_y: f32 },
 195     DragUpdate { dx: f32, dy: f32, x: f32, y: f32, local_x: f32, local_y: f32 },
 196     DragEnd,
 197     FocusIn,
 198     FocusOut,
 199 }
 200 
 201 #[derive(Debug, Clone, Copy, PartialEq)]
 202 pub struct Point {
 203     pub x: f32,
 204     pub y: f32,
 205 }
 206 
 207 #[derive(Debug, Clone, Copy, PartialEq)]
 208 pub struct LayoutConstraints {
 209     pub min_width: f32,
 210     pub max_width: f32,
 211     pub min_height: f32,
 212     pub max_height: f32,
 213 }
 214 
 215 impl LayoutConstraints {
 216     pub fn new(min_w: f32, max_w: f32, min_h: f32, max_h: f32) -> Self {
 217         Self { min_width: min_w, max_width: max_w, min_height: min_h, max_height: max_h }
 218     }
 219     
 220     pub fn loose(max_w: f32, max_h: f32) -> Self {
 221         Self { min_width: 0.0, max_width: max_w, min_height: 0.0, max_height: max_h }
 222     }
 223 }
 224 
 225 #[derive(Debug, Clone, Copy, PartialEq)]
 226 pub struct Size {
 227     pub width: f32,
 228     pub height: f32,
 229 }
 230 
 231 /// The single host surface every widget presents to the machinery (context routing, the
 232 /// paint walk, the render loop, app dyn broadcasts). **Formerly `Element`**, the ~125-method
 233 /// god-trait — renamed at the 6bd flip once census-driven shrink batches brought it down to
 234 /// the measured blueprint. `Adapted<W>` is the one production implementor; concrete behavior
 235 /// lives on the narrow `Layout`/`Paint`/`Input` traits it wraps. The direct-dispatch and
 236 /// value blocks shrink further as apps move to routed events / concrete slots.
 237 pub trait WidgetHost {
 238     /// The widget's shared base state — GUARANTEED (the flip): the `Option` escape hatch
 239     /// and its `WidgetId(0)` sentinel class are gone. `Adapted` (the one production
 240     /// implementor) always owns a base; test shims carry one via `impl_widget_base!`.
 241     fn base(&self) -> &Widget;
 242     fn base_mut(&mut self) -> &mut Widget;
 243 
 244     /// The height of the detached-label strip above this widget's content: zero for
 245     /// unlabeled widgets and for those whose base label IS their content
 246     /// ([`Layout::inline_label`]). A widget's rect is always its content plus this
 247     /// strip — `set_rect` takes that block, `layout` lands the content at the origin
 248     /// and hangs the strip above it. A strategy reserves that
 249     /// row above every child's content (`container_layout::label_lead`) and puts
 250     /// `layout::CONTROL_GAP` between the blocks.
 251     fn label_strip(&self) -> f32 { self.base().label_offset() }
 252 
 253     /// Where the detached label is drawn: the strip above the content, as wide as the
 254     /// label's text. `None` for an unlabeled widget and for an inline label. The label
 255     /// may be wider than the widget's rect (a StatusDot's, a Checkbox's) — the rect is
 256     /// the content's width, and the text runs past it — so anything wrapping a widget
 257     /// as a block (a `Group`'s hull) unions this with the rect.
 258     fn detached_label_rect(&self) -> Option<crate::scene::layout::Rect> { None }
 259 
 260 
 261     // Required (the flip): the old defaults manufactured DummyAny stand-ins nothing
 262     // could legitimately use. `impl_widget_base!` provides both. `as_ptr`/`as_ptr_mut`
 263     // are GONE from the trait (the plumbing retype): a pointer to a widget you already
 264     // hold is a plain cast (`w as *mut (dyn WidgetHost + 'static)`); concrete
 265     // registration sites ride the inherent `Adapted<W>` methods (the registration
 266     // bridge — derived from a live borrow, never stored beyond the registry).
 267     fn as_any(&self) -> &dyn std::any::Any;
 268     fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
 269 
 270     /// The widget's own model, as its narrow traits: what [`WidgetHostExt`] reads its
 271     /// one-line answers off (`Adapted` hands out its inner widget; a test shim with no
 272     /// model gets [`NoModel`]'s defaults).
 273     fn layout_model(&self) -> &dyn Layout {
 274         &NoModel
 275     }
 276     fn paint_model(&self) -> &dyn Paint {
 277         &NoModel
 278     }
 279     fn input_model(&self) -> &dyn Input {
 280         &NoModel
 281     }
 282     fn input_model_mut(&mut self) -> &mut dyn Input {
 283         // A zero-sized value: leaking it allocates nothing.
 284         Box::leak(Box::new(NoModel))
 285     }
 286 
 287     fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
 288         // The default serves test shims only (Adapted overrides this): base hover
 289         // bookkeeping on moves, tick forwarding, everything else inert — the old
 290         // per-method dispatch died with the direct-dispatch entry points (6bd collapse).
 291         match event {
 292             Event::PointerMove { x, y, .. } => {
 293                 let (px, py) = (*x, *y);
 294                 ctx.set_cursor_pos(px, py);
 295                 let was = self.base().hovered;
 296                 let is_hit = self.hit_test(px, py, ctx);
 297                 self.base_mut().hovered = is_hit;
 298                 was != is_hit
 299             }
 300             Event::Tick(dt) => {
 301                 self.tick(*dt, ctx)
 302             }
 303             _ => false,
 304         }
 305     }
 306 
 307     fn measure(&self, constraints: LayoutConstraints, _ctx: &UiContext) -> Size {
 308         let (_, _, w, h) = self.rect();
 309         let pref_h = self.preferred_height().unwrap_or(h);
 310         
 311         let width = w.clamp(constraints.min_width, constraints.max_width);
 312         let height = pref_h.clamp(constraints.min_height, constraints.max_height);
 313         
 314         Size { width, height }
 315     }
 316 
 317     /// Land the CONTENT box at `origin`, the label strip hanging above it — the one
 318     /// placement contract (`Adapted` repeats it over its measured content size).
 319     fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
 320         let size = self.measure(constraints, ctx);
 321         let strip = self.label_strip();
 322         self.set_rect(origin.x, origin.y - strip, size.width, size.height + strip);
 323     }
 324 
 325     fn rect(&self) -> (f32, f32, f32, f32) {
 326         let b = self.base();
 327         (b.x, b.y, b.w, b.h)
 328     }
 329 
 330 
 331     // The value/polling block (`get_value_string`/`set_value_string`/`take_change`/
 332     // `take_click`/`value`/`set_text`/`set_selected`) is GONE from the trait (6bd value
 333     // shrink): apps drain widget state through the concrete inherent `Adapted<W>` methods
 334     // (which forward to the narrow `Input` hooks). The last dyn readers went concrete-slot
 335     // (TI's roster drain, cloud's JsonControl, designer's pane-focus sync).
 336 
 337 
 338     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
 339         let b = self.base_mut();
 340         b.x = x;
 341         b.y = y;
 342         b.w = w;
 343         b.h = h;
 344     }
 345 
 346     fn set_row_rect(&mut self, x: f32, w: f32) {
 347         let b = self.base_mut();
 348         b.row_x = x;
 349         b.row_w = w;
 350     }
 351 
 352     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
 353         if ctx.is_coordinate_covered(self.base().id(), px, py) {
 354             return false;
 355         }
 356         let (x, y, w, h) = self.rect();
 357         if w <= 0.0 || h <= 0.0 {
 358             return false;
 359         }
 360         let b = self.base();
 361         let (hx, hw) = if b.row_w > 0.0 { (b.row_x, b.row_w) } else { (x, w) };
 362         px >= hx && px <= hx + hw && py >= y && py <= y + h
 363     }
 364 
 365     // The direct-dispatch entry points (`cursor_moved`, `on_cursor_moved`, `mouse_input`,
 366     // `mouse_wheel`, `keyboard_input`, `drag_begin`/`drag_update`/`drag_end`) are GONE from
 367     // the trait (6bd collapse): every event delivery goes through `handle_event` — the entry
 368     // points live on as inherent `Adapted<W>` methods for concrete in-crate forwards.
 369 
 370     // `hovered`/`set_hovered` are GONE from the trait (6bd batch 2): the state is the base
 371     // `Widget::hovered` flag, read/written directly by the defaults above; Button/Checkbox
 372     // keep inherent accessors for immediate-mode hosts.
 373 
 374 
 375 
 376     // `draggable`/`is_dragging` are GONE from the trait (the ControlPanel endgame
 377     // removed their last stored-child-pointer consumer): the drag queries are concrete
 378     // inherent `Adapted<W>` reads; index-driven rosters (TI, designer) route them
 379     // through per-slot matches like the other value drains.
 380 
 381     
 382 
 383     /// Emit this widget's OWN primitives (non-recursive) into the single paint pass (Phase 3).
 384     /// Every production host overrides this (`Adapted`); the
 385     /// default serves a host with no widget of its own (the test shims): its plate — a solid
 386     /// border, or a rounded fill in its colour where its corner style rounds — then what its
 387     /// model paints, text aside. Recursion into children and clipping are the paint walk's
 388     /// (`scene::painter`), not this.
 389     fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) {
 390         use crate::scene::layout::Rect;
 391         use crate::scene::paint::{PaintCtx, Prim};
 392         let (x, y, w, h) = self.rect();
 393         let rect = Rect { x, y, width: w, height: h };
 394         let color = self.color();
 395         if let Some((border_color, thickness)) = self.solid_border() {
 396             let cr = self.corner_radii();
 397             ctx.border(rect, (cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left), color, border_color, thickness);
 398         } else if let Some((radius, corners)) = self.paint_model().corner_style(self.content_rect()) {
 399             if corners != (false, false, false, false) && color[3].abs() > 0.001 {
 400                 ctx.rounded_rect(rect, radius, corners, color);
 401             }
 402         }
 403         if !self.visible() {
 404             return;
 405         }
 406         // Text: none. A host with text of its own overrides this; emitting the model's text
 407         // here would double what the walk's descent draws for a container (the Phase 6d trap).
 408         let mut tmp = PaintCtx::new();
 409         self.paint_model().paint_ui(ui, self.content_rect(), &mut tmp);
 410         for item in tmp.finish().items {
 411             // `replay` emits every prim but Text, which it hands back; dropping it is the point.
 412             let _text: Option<Prim> = ctx.replay(item.prim);
 413         }
 414     }
 415 
 416 
 417 
 418 
 419 
 420     // The per-widget text getters (text_labels / text_labels_with_bounds /
 421     // text_labels_with_font_and_bounds / get_text_items) are GONE: every widget emits
 422     // its own text as display-list prims via paint_self (Adapted::paint_self). The
 423     // deleted default's base-label synthesis lives on in Adapted's base-label fallback,
 424     // and its scroll-ancestor clamp in scene::painter::scroll_ancestor_text_bounds.
 425 
 426     fn type_name(&self) -> &'static str {
 427         let full_name = std::any::type_name::<Self>();
 428         full_name.split("::").last().unwrap_or("Widget")
 429     }
 430     fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { None }
 431     fn render_popover(&self, _pc: &mut dyn crate::layout::RenderTarget) {}
 432 
 433     fn focus(&mut self) {
 434         self.base_mut().focused = true;
 435     }
 436     fn unfocus(&mut self) {
 437         self.base_mut().focused = false;
 438     }
 439     fn focused(&self, ctx: &UiContext) -> bool {
 440         ctx.is_focused_id(self.base().id())
 441     }
 442     fn prepare_text(&mut self, _fs: &mut cosmic_text::FontSystem) {}
 443 
 444     fn set_visible(&mut self, _visible: bool) {}
 445     fn visible(&self) -> bool { true }
 446     fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool { false }
 447 
 448     /// Put the widget's embedded children (`widget::Embedded`) into `ctx`: what
 449     /// `UiContext::insert` calls once the widget is in. The adapter forwards to
 450     /// `Layout::register_embedded_children`, which also runs on every layout and tick.
 451     fn attach_embedded(&mut self, _ctx: &mut UiContext) {}
 452 
 453     /// Take the widget's embedded children back out of `ctx`, so it leaves whole: what
 454     /// `UiContext::remove` calls before the widget goes. The adapter forwards to
 455     /// `Layout::release_embedded_children`.
 456     fn release_embedded(&mut self, _ctx: &mut UiContext) {}
 457 
 458     // `set_parent`/`add_child` are GONE from the trait (6bd batch 4): linking is a tree
 459     // operation, by id (`UiContext::link_ids`, `ctx.tree`). `parent`/`children` are GONE
 460     // too: tree structure is read off `ctx.tree` (`parent_id` / `child_ids`), and no trait
 461     // method returns a raw pointer.
 462 
 463 
 464 
 465 
 466 
 467 
 468 
 469 
 470 
 471     /// The parts a screen reader sees as nodes of their own (`Input::a11y_items`).
 472     fn a11y_items(&self) -> Vec<crate::a11y::A11yItem> {
 473         Vec::new()
 474     }
 475 
 476 
 477 }
 478 
 479 /// The host surface that does not need a slot of its own in `WidgetHost`: what a widget's
 480 /// narrow traits answer ([`Input`], [`Paint`], [`Layout`], reached through the host's
 481 /// [`WidgetHost::input_model`] / [`paint_model`](WidgetHost::paint_model) /
 482 /// [`layout_model`](WidgetHost::layout_model)) and what is derived from the host's own state.
 483 /// Implemented for every host, `dyn WidgetHost` included, so `w.focus_role()` reads as it
 484 /// always did — with this trait in scope (`use cce_ui::widget::WidgetHostExt`). Until
 485 /// 2026-10-08 each of these was a `WidgetHost` method that `Adapted` overrode with a one-line
 486 /// forward.
 487 pub trait WidgetHostExt: WidgetHost {
 488     /// This widget's part in keyboard navigation — `Input::focus_role` through
 489     /// the adapter; `FocusRole::None` for anything that is not a plate or a well.
 490     fn focus_role(&self) -> FocusRole {
 491         self.input_model().focus_role()
 492     }
 493 
 494     /// Whether, focused, it takes Tab itself instead of the Tab walk (`Input::keeps_tab`).
 495     fn keeps_tab(&self) -> bool {
 496         self.input_model().keeps_tab()
 497     }
 498 
 499     fn blocks_root_plate_drag(&self) -> bool {
 500         self.input_model().blocks_root_plate_drag()
 501     }
 502 
 503     fn wants_tick(&self) -> bool {
 504         self.input_model().wants_tick()
 505     }
 506 
 507     fn is_scrollable(&self) -> bool {
 508         self.input_model().scrollable()
 509     }
 510 
 511     /// An explicit accessibility role, overriding the guess `crate::a11y::role_for` makes
 512     /// from the widget's type and focus role. Default `None`.
 513     fn a11y_role(&self) -> Option<accesskit::Role> {
 514         self.input_model().a11y_role()
 515     }
 516 
 517     /// The widget's value for assistive technology: a field's text, a slider's number, a
 518     /// check box's "true" / "false". Default `None`.
 519     fn a11y_value(&self) -> Option<String> {
 520         self.input_model().value_string()
 521     }
 522 
 523     /// The `(min, max, step)` an assistive tool may set the value in (`Input::a11y_range`).
 524     fn a11y_range(&self) -> Option<(f64, f64, f64)> {
 525         self.input_model().a11y_range()
 526     }
 527 
 528     /// Set the value an assistive tool asked for (`Input::a11y_set_value`).
 529     fn a11y_set_value(&mut self, value: f64) -> bool {
 530         self.input_model_mut().a11y_set_value(value)
 531     }
 532 
 533     /// A text field's text, caret and selection for a reader (`Input::a11y_text`).
 534     fn a11y_text(&self) -> Option<crate::a11y::A11yText> {
 535         self.input_model().a11y_text()
 536     }
 537 
 538     /// Replace a text field's text for an assistive tool (`Input::a11y_set_text`).
 539     fn a11y_set_text(&mut self, text: &str) -> bool {
 540         self.input_model_mut().a11y_set_text(text)
 541     }
 542 
 543     /// An assistive tool clicked one of them (`Input::a11y_select_item`).
 544     fn a11y_select_item(&mut self, idx: usize) -> bool {
 545         self.input_model_mut().a11y_select_item(idx)
 546     }
 547 
 548     fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
 549         self.input_model_mut().set_modifiers(ctrl, shift, alt)
 550     }
 551 
 552     /// Dispatch a context-menu action on this widget. Returns whether it was applied.
 553     /// Default inert; the adapter forwards to `Input::context_action` (whose default gives
 554     /// every widget whole-value Cut/Copy/Paste through the value-string pair).
 555     fn context_action(&mut self, action: ContextAction) -> bool {
 556         self.input_model_mut().context_action(action)
 557     }
 558 
 559     fn color(&self) -> [f32; 4] {
 560         self.paint_model().color()
 561     }
 562 
 563     fn solid_border(&self) -> Option<([f32; 4], f32)> {
 564         self.paint_model().solid_border()
 565     }
 566 
 567     fn widget_font(&self) -> Option<String> {
 568         self.paint_model().widget_font()
 569     }
 570 
 571     /// Whether the paint walk should clip this widget's children to its rect (scroll/root plate
 572     /// containers). Default: no clipping.
 573     fn clips_children(&self) -> bool {
 574         self.paint_model().clips_children()
 575     }
 576 
 577     /// Whether this widget paints its ENTIRE subtree itself through its (recursive)
 578     /// `all_rounded_quads` / `all_quads` — a legacy "subtree painter" such as `TreeList`, whose
 579     /// row backgrounds and separators live in an `all_rounded_quads` override that also recurses
 580     /// into its children. When true, the paint walk emits those directly and does NOT recurse
 581     /// (the widget already did). Transitional: such widgets will eventually get a proper
 582     /// non-recursive `paint_self`. Default: false.
 583     fn renders_own_subtree(&self) -> bool {
 584         self.paint_model().paints_own_subtree()
 585     }
 586 
 587     fn z_index(&self) -> i32 {
 588         self.layout_model().z_order()
 589     }
 590 
 591     /// The widget's natural CONTENT height — the control below its detached label, if
 592     /// any. What a layout strategy allots; [`WidgetHost::layout`] places that content
 593     /// box at the origin it is given and hangs the label ([`WidgetHost::label_strip`])
 594     /// above it. `None` when the widget has no natural height.
 595     fn preferred_height(&self) -> Option<f32> {
 596         self.layout_model().intrinsic_size().map(|s| s.height)
 597     }
 598 
 599     fn label(&self) -> Option<String> {
 600         self.base().label.clone()
 601     }
 602 
 603     fn corner_radii(&self) -> CornerRadii {
 604         let (r, (tl, tr, br, bl)) =
 605             self.paint_model().corner_style(self.content_rect()).unwrap_or((0.0, (false, false, false, false)));
 606         CornerRadii::new(
 607             if tl { r } else { 0.0 },
 608             if tr { r } else { 0.0 },
 609             if br { r } else { 0.0 },
 610             if bl { r } else { 0.0 },
 611         )
 612     }
 613 
 614     fn mark_dirty(&mut self, ctx: &mut UiContext){
 615         let b = self.base_mut();
 616         if b.dirty {
 617             return;
 618         }
 619         b.dirty = true;
 620         if let Some(parent) = b.id.get().and_then(|id| ctx.tree.parent_id(id)) {
 621             ctx.lend(parent, |p, ctx| p.mark_dirty(ctx));
 622         }
 623     }
 624 
 625     // ── What the widget paints, read from its model ──────────────────────────────
 626     // The legacy tuple views (`extra_quads`, `all_quads`, `all_rounded_quads`,
 627     // `extra_arcs`, `extra_circles`, `highlight_quad`, `corner_style`) are gone since
 628     // 2026-10-08: every host paints a widget through `paint_self` / the paint walk, and a
 629     // composite that draws a child's chrome in its own order reads `painted_prims`.
 630 
 631     /// The rect the widget's model paints into: the host's rect below its detached label.
 632     fn content_rect(&self) -> crate::scene::layout::Rect {
 633         let b = self.base();
 634         let top = if self.layout_model().inline_label() { 0.0 } else { b.label_offset() };
 635         // Deliberately NOT clamped at zero: hosts under-size labeled sliders (label taller
 636         // than the assigned rect), and the negative-height quads still rasterize.
 637         crate::scene::layout::Rect { x: b.x, y: b.y + top, width: b.w, height: b.h - top }
 638     }
 639 
 640     /// Everything the widget's model paints into [`content_rect`](Self::content_rect), as prims.
 641     fn painted_prims(&self) -> Vec<crate::scene::paint::Prim> {
 642         let mut pc = crate::scene::paint::PaintCtx::new();
 643         self.paint_model().paint(self.content_rect(), &mut pc);
 644         pc.finish().items.into_iter().map(|item| item.prim).collect()
 645     }
 646 }
 647 
 648 impl<T: WidgetHost + ?Sized> WidgetHostExt for T {}
 649 
 650 /// A shown widget's prims as its model paints them, nothing while it is hidden — for a
 651 /// composite that draws a child's chrome in its own order rather than walking it (the params
 652 /// pane's rows, the ramp's key editor, the menubar's strip).
 653 pub(crate) fn shown_prims<W: WidgetHost + ?Sized>(w: &W) -> Vec<crate::scene::paint::Prim> {
 654     if w.visible() { w.painted_prims() } else { Vec::new() }
 655 }
 656 
 657 /// The plain quads among [`shown_prims`], as `(x, y, w, h, colour)`.
 658 pub(crate) fn shown_quads<W: WidgetHost + ?Sized>(w: &W) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
 659     shown_prims(w)
 660         .into_iter()
 661         .filter_map(|prim| match prim {
 662             crate::scene::paint::Prim::Quad { rect, color } => Some((rect.x, rect.y, rect.width, rect.height, color)),
 663             _ => None,
 664         })
 665         .collect()
 666 }
 667 
 668 /// The rounded quads among [`shown_prims`], as `(x, y, w, h, radius, colour, corners)`.
 669 pub(crate) fn shown_rounded_quads<W: WidgetHost + ?Sized>(
 670     w: &W,
 671 ) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
 672     shown_prims(w)
 673         .into_iter()
 674         .filter_map(|prim| match prim {
 675             crate::scene::paint::Prim::RoundedRect { rect, radius, corners, color } => {
 676                 Some((rect.x, rect.y, rect.width, rect.height, radius, color, corners))
 677             }
 678             _ => None,
 679         })
 680         .collect()
 681 }
 682 
 683 /// The narrow traits' defaults, for a host that has no widget model of its own (the test
 684 /// shims that implement `WidgetHost` directly): transparent, no focus role, no value.
 685 pub struct NoModel;
 686 impl Layout for NoModel {}
 687 impl Paint for NoModel {
 688     fn color(&self) -> [f32; 4] {
 689         [0.0; 4]
 690     }
 691 }
 692 impl Input for NoModel {}
 693 
 694 
 695 // The `Control` subtrait (set_label + control_label) is DELETED (6bd value shrink):
 696 // zero dyn consumers and zero `control_label()` callers remained; `set_label` lives on as
 697 // the inherent `Adapted<W>` method every call site already resolved to (it shadowed the
 698 // trait), and detached-label paint moved to the adapter in the Phase 5 leaf sweeps.
 699 
 700 pub mod core;
 701 pub mod input;
 702 pub mod plate_dock;
 703 pub mod container;
 704 pub mod display;
 705 pub mod editor;
 706 pub mod shaping;
 707 #[cfg(feature = "markdown")]
 708 pub mod markdown;
 709 
 710 /// An image a host has ready for a Markdown embed (`![[pic.png]]`): its id
 711 /// from `vk::upload_rgba` and its size in px. `MarkdownView` and
 712 /// `DocEditor` ask the host for one by the embed's link text — sizing at
 713 /// layout, the id again at paint, so a re-upload after a reconnect needs
 714 /// no relayout.
 715 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 716 pub struct EmbedImage {
 717     pub id: u32,
 718     pub width: u32,
 719     pub height: u32,
 720 }
 721 
 722 impl EmbedImage {
 723     /// The size it draws at in a column `max_w` wide: the requested width
 724     /// (else its own), the requested height (else the aspect's), scaled
 725     /// down as a whole to fit the column.
 726     pub fn fit(&self, want_w: Option<u32>, want_h: Option<u32>, max_w: f32) -> (f32, f32) {
 727         let (iw, ih) = (self.width.max(1) as f32, self.height.max(1) as f32);
 728         let w = want_w.map_or(iw, |w| w as f32);
 729         let h = want_h.map_or(w * ih / iw, |h| h as f32);
 730         if w > max_w {
 731             (max_w, h * max_w / w)
 732         } else {
 733             (w, h)
 734         }
 735     }
 736 }
 737 #[cfg(feature = "doc_editor")]
 738 pub mod doc_editor;
 739 pub mod line_edit;
 740 pub mod model;
 741 pub mod handle;
 742 pub mod embedded;
 743 pub mod scroll_region;
 744 pub mod scroll_motion;
 745 pub mod side_swipe;
 746  
 747 // Re-exports
 748 pub use self::editor::TextEditorState;
 749 pub use self::line_edit::{EditOutcome, LineEdit};
 750 pub use self::scroll_region::{ScrollRegion, ScrollbarActivity};
 751 pub use self::side_swipe::{SideSwipe, SwipeDir};
 752 pub use self::scroll_motion::{Bounds, ScrollAxis, ScrollMotion, ScrollPhase, ScrollSettings, LINE_PX};
 753 pub use self::model::{Adapted, EventCtx, Input, Layout, Paint};
 754 pub use self::handle::Handle;
 755 pub use self::embedded::Embedded;
 756 pub use self::core::{Widget, hover_animation, clipboard, context_menu, clear_widget_references};
 757 pub use self::input::{
 758     Button, TextBox, Spinbox, Dropdown, Checkbox, Toggle, RadioGroup, Slider, RangeSlider,
 759     ColorSelector, Finger, Trackpad, get_font_db, ActiveThumb, FontSelector,
 760     BevelPreview, bevel_ease, parse_bevel_knobs, RampPreview,
 761     ButtonStrip, KeybindRecorder, Ramp, RampKey, ColorRamp, ColorRampKey,
 762     format_ramp_spec, parse_ramp_spec
 763 };
 764 pub use self::container::{
 765     Dialog, Group, GroupFrame,
 766     ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
 767     ColumnsLayout, MosaicLayout, ReverseMosaicLayout,
 768     ContentBg, ParametersBg,
 769     ScrollBox, MenuBar, SheetColumn, Spreadsheet, Breadcrumb,
 770     Paginator, TreeList, TreeElement
 771 };
 772 pub use self::display::{
 773     TextLabel, Label, StyledLabel, LabelPrim, TextItem, UsageBar,
 774     InfoBox, StatusDot, InteractiveListItem,
 775     GraphNode, Graph, TaggedQuad, node_wires, Float3, ProgressBar, StatusBar, Splitter, Separator,
 776     DotStatus, ImageView,
 777     truncate_head, truncate_tail,
 778 };
 779 
 780 pub trait PageSelector {
 781     fn selected_page(&self) -> usize;
 782     fn set_selected_page(&mut self, page: usize);
 783     fn sidebar_w(&self) -> f32;
 784 }
 785 
 786 pub trait MenuController {
 787     fn menu_click(&mut self) -> Option<(usize, usize)>;
 788     fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize);
 789     fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool);
 790     fn set_menu_items(&mut self, menu_idx: usize, items: &[String]);
 791     fn is_menu_bar(&self) -> bool;
 792     fn is_menu_open(&self) -> bool;
 793     fn menu_items(&self) -> Vec<String>;
 794     fn menu_item_checked(&self) -> Vec<Option<bool>>;
 795     fn is_vertical(&self) -> bool;
 796     fn menu_names(&self) -> Vec<String>;
 797     fn menu_items_list(&self) -> Vec<Vec<String>>;
 798     fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>>;
 799     fn take_context_change(&mut self) -> Option<usize>;
 800     fn set_context_selected(&mut self, selected: usize);
 801     fn set_center_items(&mut self, center: bool);
 802     fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)>;
 803 }
 804 
 805 pub trait GraphController {
 806     fn set_nodes(&mut self, nodes: &[GraphNode]);
 807     fn get_nodes(&self) -> Vec<GraphNode>;
 808     fn selected_node(&self) -> Option<usize>;
 809     fn set_selected_node(&mut self, idx: Option<usize>);
 810     fn double_clicked_node(&self) -> Option<usize>;
 811     fn clear_double_clicked_node(&mut self);
 812     fn set_grid_snap_enabled(&mut self, enabled: bool);
 813     fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)>;
 814     fn set_grid_snap(&mut self, gx: f32, gy: f32);
 815     /// The grid's ONE size per axis: the pitch, from the centre of one grid
 816     /// line to the centre of the next. Nodes are centred on the lattice
 817     /// intersections. Leaves the node size alone.
 818     fn set_grid_pitch(&mut self, px: f32, py: f32);
 819     /// The node body's size, independent of the pitch — hosts scale it with
 820     /// their zoom as they scale the pitch.
 821     fn set_node_size(&mut self, w: f32, h: f32);
 822     /// The older cell-and-gap description of the same lattice — a cell plus
 823     /// its gap is a pitch, and the node body is the cell. Kept for hosts
 824     /// that still speak it (cce-files, cce-graph); new code sets the pitch.
 825     fn set_grid_sizes(&mut self, gx: f32, gy: f32);
 826     /// The gap half of the cell-and-gap description; see [`set_grid_sizes`].
 827     fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32);
 828     /// The lattice intersection node (0, 0) is centred on, window-absolute.
 829     fn set_grid_origin(&mut self, ox: f32, oy: f32);
 830     fn grid_origin(&self) -> (f32, f32);
 831     fn set_show_network_grid(&mut self, show: bool);
 832     fn take_pending_connection(&mut self) -> Option<(String, String)>;
 833     /// [`Self::take_pending_connection`] with the INPUT PORT the connection
 834     /// was dropped on: (input node id, output node name, port). A host whose
 835     /// nodes read several wires (the k-th `node` parameter into port k)
 836     /// writes the one the port is. Taking either takes the connection.
 837     fn take_pending_connection_to_port(&mut self) -> Option<(String, String, usize)> {
 838         self.take_pending_connection().map(|(id, name)| (id, name, 0))
 839     }
 840     /// A node dropped onto a wire, to be spliced in between its ends:
 841     /// (dragged node id, the wire's upstream node NAME — what Input params
 842     /// store, the wire's downstream node id). The host rewires both Input
 843     /// params: dragged.Input = upstream name, downstream.Input = dragged's
 844     /// name. Default None for hosts whose graphs have no wires to splice.
 845     fn take_pending_splice(&mut self) -> Option<(String, String, String)> {
 846         None
 847     }
 848     /// A node dropped onto another node, which it swapped places with:
 849     /// (dragged node id, the other node's id). The widget has traded their
 850     /// cells; the host trades the rest. Only while the host opted in
 851     /// (`Graph::set_swap_on_drop`); default None.
 852     fn take_pending_swap(&mut self) -> Option<(String, String)> {
 853         None
 854     }
 855     /// The wire into an Input that runs through the body a node would have
 856     /// at lattice cell (col, row), as (upstream id, downstream id): where a
 857     /// node ADDED there splices in, by the hit test a drop uses. Default
 858     /// None for hosts whose graphs have no wires to splice.
 859     fn input_wire_through_cell(&self, _col: f32, _row: f32) -> Option<(String, String)> {
 860         None
 861     }
 862     fn cancel_connecting(&mut self);
 863     fn is_node_rect(&self, qx: f32, qy: f32, qw: f32, qh: f32) -> bool;
 864     /// The topmost node whose body contains (px, py), window-absolute coords.
 865     fn node_at(&self, px: f32, py: f32) -> Option<usize>;
 866     /// The corner radius of anything node-shaped on the grid at the current
 867     /// zoom — the cursor, the drop-target highlight (0 = square).
 868     fn cell_corner_radius(&self) -> f32;
 869     /// The flat-geometry emission with grid cells tagged by their surviving
 870     /// rounded corners — for hosts that draw the graph's quads themselves
 871     /// (the designer) and want cells as superellipse tiles.
 872     fn geometry_quads_tagged(&self, rect: crate::scene::layout::Rect) -> Vec<TaggedQuad>;
 873     /// The grid lines and origin axes, flat, over what is beneath — for
 874     /// hosts that draw the graph's quads themselves, called at the point in
 875     /// their walk where the grid goes (under the wires and nodes).
 876     fn paint_grid(&self, rect: crate::scene::layout::Rect, pc: &mut crate::scene::paint::PaintCtx);
 877     /// The wires and the connection being dragged out, stroked in the wire
 878     /// style in effect — for hosts that draw the graph's quads themselves,
 879     /// called after [`paint_grid`](Self::paint_grid) and under the nodes.
 880     /// The quads carry no wires.
 881     fn paint_wires(&self, rect: crate::scene::layout::Rect, pc: &mut crate::scene::paint::PaintCtx);
 882     /// The pixel rect the in-flight node drag will deposit its body on
 883     /// (`commit_drag`'s resolution), for hosts' drop-target highlight.
 884     /// None outside a node drag.
 885     fn drop_target_cell_rect(&self) -> Option<(f32, f32, f32, f32)>;
 886 }
 887 
 888 pub trait SpreadsheetController {
 889     fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>);
 890     /// The table as columns of values ([`SheetColumn`]), a column a header:
 891     /// the cells are written as they are painted, so a refill costs a copy
 892     /// of the values. Rows of text, by default.
 893     fn set_spreadsheet_columns(&mut self, headers: Vec<String>, columns: Vec<SheetColumn>) {
 894         let n = columns.iter().map(SheetColumn::len).max().unwrap_or(0);
 895         let rows = (0..n).map(|r| columns.iter().map(|c| c.cell(r)).collect()).collect();
 896         self.set_spreadsheet_data(headers, rows);
 897     }
 898     /// The selected rows, as indices into the rows last set, ascending.
 899     fn selected_rows(&self) -> Vec<usize> {
 900         Vec::new()
 901     }
 902     /// Replace the selection; a row the table does not have is left out.
 903     fn set_selected_rows(&mut self, _rows: &[usize]) {}
 904     /// Whether the selection changed since this was last asked.
 905     fn take_selection_change(&mut self) -> bool {
 906         false
 907     }
 908 }
 909 
 910 pub trait PathController {
 911     fn set_path(&mut self, segments: &[String]);
 912     fn path_click(&mut self) -> Option<usize>;
 913 }
 914 
 915 pub trait ParamController {
 916     fn node_params(&self) -> Vec<(String, String, String)>;
 917     fn set_display_params(&mut self, params: &[(String, String, String)]);
 918 }
 919 
 920 pub trait GeomController {
 921     fn set_geom_visible(&mut self, visible: bool);
 922     fn geom_visible(&self) -> bool;
 923     fn take_geom_toggle(&mut self) -> bool;
 924 }
 925 
 926 #[derive(Debug, Clone, Copy, PartialEq)]
 927 pub struct CornerRadii {
 928     pub top_left: f32,
 929     pub top_right: f32,
 930     pub bottom_right: f32,
 931     pub bottom_left: f32,
 932 }
 933 
 934 impl CornerRadii {
 935     pub fn new(tl: f32, tr: f32, br: f32, bl: f32) -> Self {
 936         Self { top_left: tl, top_right: tr, bottom_right: br, bottom_left: bl }
 937     }
 938 
 939     pub fn uniform(radius: f32) -> Self {
 940         Self::new(radius, radius, radius, radius)
 941     }
 942 }
 943 
 944 pub fn match_key_shortcut(event: &KeyEvent, shortcut_str: &str) -> bool {
 945     let shortcut_lower = shortcut_str.to_lowercase();
 946     let parts: Vec<&str> = shortcut_lower.split('+').collect();
 947     
 948     let mut req_ctrl = false;
 949     let mut req_shift = false;
 950     let mut req_alt = false;
 951     let mut req_key = "";
 952 
 953     for part in parts {
 954         match part {
 955             "ctrl" | "control" => req_ctrl = true,
 956             "shift" => req_shift = true,
 957             "alt" | "meta" => req_alt = true,
 958             // Super chords belong to the compositor; a client never sees them.
 959             "super" | "win" | "logo" => {}
 960             k => req_key = k,
 961         }
 962     }
 963 
 964     if event.ctrl != req_ctrl { return false; }
 965     if event.shift != req_shift { return false; }
 966     if event.alt != req_alt { return false; }
 967     
 968     if let Key::Character(ref ch) = event.logical_key {
 969         let ch_lower = ch.to_lowercase();
 970         if req_key.len() == 1 {
 971             return ch_lower == req_key;
 972         } else {
 973             let mapped_key = match req_key {
 974                 "slash" => "/",
 975                 "enter" => "enter",
 976                 "escape" => "escape",
 977                 "space" => " ",
 978                 k => k,
 979             };
 980             return ch_lower == mapped_key;
 981         }
 982     } else if let Key::Named(nk) = event.logical_key {
 983         let nk_str = format!("{:?}", nk).to_lowercase();
 984         return nk_str == req_key;
 985     }
 986     false
 987 }
 988 
 989 
 990 #[cfg(test)]
 991 mod value_notch_tests {
 992     use super::{MouseScrollDelta, Position};
 993 
 994     /// A value control reads "up is more": a wheel notch up is positive
 995     /// either way; a finger's pixel delta is taken as it comes with natural
 996     /// scrolling off, and negated with it on, since that delta is what a
 997     /// list scrolls by and a natural list follows the fingers.
 998     #[test]
 999     fn a_value_control_reads_up_as_more_on_a_wheel_and_a_natural_finger() {
1000         let wheel_up = MouseScrollDelta::LineDelta(0.0, 1.0);
1001         let finger = MouseScrollDelta::PixelDelta(Position { x: 0.0, y: -60.0 });
1002         assert_eq!(wheel_up.value_notches_of(false), 1.0);
1003         assert_eq!(wheel_up.value_notches_of(true), 1.0);
1004         assert_eq!(finger.value_notches_of(false), -1.0, "natural off: the delta as it comes");
1005         assert_eq!(finger.value_notches_of(true), 1.0, "natural on: the fingers went up, so more");
1006         // Under `cfg(test)` the toolkit's own suite reads natural as off,
1007         // unless a thread forces it.
1008         assert_eq!(finger.value_notches_y(), -1.0);
1009         crate::input::force_natural_scroll(Some(true));
1010         assert_eq!(finger.value_notches_y(), 1.0);
1011         crate::input::force_natural_scroll(None);
1012         assert_eq!(finger.value_notches_y(), -1.0);
1013     }
1014 }