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 }