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

src/widget/mod.rs (30.4K)

  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 
 48 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 49 pub enum Key {
 50     Named(NamedKey),
 51     Character(String),
 52 }
 53 
 54 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 55 pub enum NamedKey {
 56     Backspace,
 57     Tab,
 58     Enter,
 59     Escape,
 60     Space,
 61     ArrowDown,
 62     ArrowLeft,
 63     ArrowRight,
 64     ArrowUp,
 65     End,
 66     Home,
 67     PageDown,
 68     PageUp,
 69     Delete,
 70     Control,
 71     Shift,
 72     Alt,
 73     Super,
 74     F5,
 75 }
 76 
 77 #[derive(Debug, Clone, PartialEq, Eq)]
 78 pub struct KeyEvent {
 79     pub state: ElementState,
 80     pub logical_key: Key,
 81     pub text: Option<String>,
 82     pub repeat: bool,
 83     pub ctrl: bool,
 84     pub shift: bool,
 85     pub alt: bool,
 86 }
 87 
 88 /// Text justification for widget labels/content (shared by Button, cce-files' row
 89 /// list, and settings; formerly defined by the dissolved json_layout host).
 90 #[derive(serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
 91 #[serde(rename_all = "lowercase")]
 92 pub enum Justification {
 93     Left,
 94     Center,
 95     Right,
 96 }
 97 
 98 /// A context-menu action dispatched on the menu's target widget (6bd phase 1: one enum
 99 /// replaces the 13 per-action `WidgetHost` methods). `ClearText` is the search-box "Cear" item.
100 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101 pub enum ContextAction {
102     Cut,
103     Copy,
104     Paste,
105     SelectAll,
106     /// Step the widget's own edit history (a text box's typing). Routed by
107     /// the runner to the focused widget on the `undo` / `redo` chords before
108     /// the app's `Application::undo` / `redo` get their turn; also reachable
109     /// as "Undo" / "Redo" context-menu rows.
110     Undo,
111     Redo,
112     ClearText,
113     CopyKey,
114     CopyValue,
115     CopyPath,
116     DeleteKey,
117     ExpandNode,
118     CollapseNode,
119     ExpandAll,
120     CollapseAll,
121     /// Ramp: hide/show the bottom control strip, the graph claiming the space.
122     ToggleRampControls,
123 }
124 
125 use crate::colors;
126 use std::sync::atomic::AtomicUsize;
127 use std::collections::HashMap;
128 
129 pub const DROPDOWN_ITEM_H: f32 = 22.0;
130 
131 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
132 pub struct WidgetId(pub usize);
133 
134 pub static NEXT_WIDGET_ID: AtomicUsize = AtomicUsize::new(1);
135 
136 #[derive(Debug, Clone)]
137 pub struct LayoutTree {
138     pub parents: HashMap<WidgetId, WidgetId>,
139     pub children: HashMap<WidgetId, Vec<WidgetId>>,
140 }
141 
142 pub use crate::scene::paint::{ControlPlate, PlateStance};
143 pub use crate::widget::model::FocusRole;
144 pub use crate::context::UiContext;
145 
146 #[derive(Debug, Clone, PartialEq)]
147 pub enum Event {
148     PointerMove { x: f32, y: f32, local_x: f32, local_y: f32 },
149     MouseButton { button: MouseButton, state: ElementState, x: f32, y: f32, local_x: f32, local_y: f32 },
150     MouseWheel { delta: MouseScrollDelta, x: f32, y: f32, local_x: f32, local_y: f32 },
151     KeyInput(KeyEvent),
152     Tick(f32),
153 
154     MouseEnter,
155     MouseLeave,
156     DragStart { start_x: f32, start_y: f32 },
157     DragUpdate { dx: f32, dy: f32, x: f32, y: f32, local_x: f32, local_y: f32 },
158     DragEnd,
159     FocusIn,
160     FocusOut,
161 }
162 
163 #[derive(Debug, Clone, Copy, PartialEq)]
164 pub struct Point {
165     pub x: f32,
166     pub y: f32,
167 }
168 
169 #[derive(Debug, Clone, Copy, PartialEq)]
170 pub struct LayoutConstraints {
171     pub min_width: f32,
172     pub max_width: f32,
173     pub min_height: f32,
174     pub max_height: f32,
175 }
176 
177 impl LayoutConstraints {
178     pub fn new(min_w: f32, max_w: f32, min_h: f32, max_h: f32) -> Self {
179         Self { min_width: min_w, max_width: max_w, min_height: min_h, max_height: max_h }
180     }
181     
182     pub fn loose(max_w: f32, max_h: f32) -> Self {
183         Self { min_width: 0.0, max_width: max_w, min_height: 0.0, max_height: max_h }
184     }
185 }
186 
187 #[derive(Debug, Clone, Copy, PartialEq)]
188 pub struct Size {
189     pub width: f32,
190     pub height: f32,
191 }
192 
193 /// The single host surface every widget presents to the machinery (context routing, the
194 /// paint walk, the render loop, app dyn broadcasts). **Formerly `Element`**, the ~125-method
195 /// god-trait — renamed at the 6bd flip once census-driven shrink batches brought it down to
196 /// the measured blueprint. `Adapted<W>` is the one production implementor; concrete behavior
197 /// lives on the narrow `Layout`/`Paint`/`Input` traits it wraps. The direct-dispatch and
198 /// value blocks shrink further as apps move to routed events / concrete slots.
199 pub trait WidgetHost {
200     /// The widget's shared base state — GUARANTEED (the flip): the `Option` escape hatch
201     /// and its `WidgetId(0)` sentinel class are gone. `Adapted` (the one production
202     /// implementor) always owns a base; test shims carry one via `impl_widget_base!`.
203     fn base(&self) -> &Widget;
204     fn base_mut(&mut self) -> &mut Widget;
205     /// The widget's natural CONTENT height — the control below its detached label, if
206     /// any. What a layout strategy allots; [`WidgetHost::layout`] places that content
207     /// box at the origin it is given and hangs the label ([`WidgetHost::label_strip`])
208     /// above it. `None` when the widget has no natural height.
209     fn preferred_height(&self) -> Option<f32> { None }
210 
211     /// The height of the detached-label strip above this widget's content: zero for
212     /// unlabeled widgets and for those whose base label IS their content
213     /// ([`Layout::inline_label`]). A widget's rect is always its content plus this
214     /// strip — `set_rect` takes that block, `layout` lands the content at the origin
215     /// and hangs the strip above it. A strategy reserves that
216     /// row above every child's content (`container_layout::label_lead`) and puts
217     /// `layout::CONTROL_GAP` between the blocks.
218     fn label_strip(&self) -> f32 { self.base().label_offset() }
219 
220     /// Where the detached label is drawn: the strip above the content, as wide as the
221     /// label's text. `None` for an unlabeled widget and for an inline label. The label
222     /// may be wider than the widget's rect (a StatusDot's, a Checkbox's) — the rect is
223     /// the content's width, and the text runs past it — so anything wrapping a widget
224     /// as a block (a `Group`'s hull) unions this with the rect.
225     fn detached_label_rect(&self) -> Option<crate::scene::layout::Rect> { None }
226 
227     fn mark_dirty(&mut self, ctx: &mut UiContext) {
228         let b = self.base_mut();
229         if b.dirty {
230             return;
231         }
232         b.dirty = true;
233         if let Some(id) = b.id.get() {
234             if let Some(parent_ptr) = ctx.tree.parent_ptr(id) {
235                 unsafe {
236                     (*parent_ptr).mark_dirty(ctx);
237                 }
238             }
239         }
240     }
241 
242     // Required (the flip): the old defaults manufactured DummyAny stand-ins nothing
243     // could legitimately use. `impl_widget_base!` provides both. `as_ptr`/`as_ptr_mut`
244     // are GONE from the trait (the plumbing retype): a pointer to a widget you already
245     // hold is a plain cast (`w as *mut (dyn WidgetHost + 'static)`); concrete
246     // registration sites ride the inherent `Adapted<W>` methods (the registration
247     // bridge — derived from a live borrow, never stored beyond the registry).
248     fn as_any(&self) -> &dyn std::any::Any;
249     fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
250 
251     fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
252         // The default serves test shims only (Adapted overrides this): base hover
253         // bookkeeping on moves, tick forwarding, everything else inert — the old
254         // per-method dispatch died with the direct-dispatch entry points (6bd collapse).
255         match event {
256             Event::PointerMove { x, y, .. } => {
257                 let (px, py) = (*x, *y);
258                 ctx.set_cursor_pos(px, py);
259                 let was = self.base().hovered;
260                 let is_hit = self.hit_test(px, py, ctx);
261                 self.base_mut().hovered = is_hit;
262                 was != is_hit
263             }
264             Event::Tick(dt) => {
265                 self.tick(*dt, ctx)
266             }
267             _ => false,
268         }
269     }
270 
271     fn measure(&self, constraints: LayoutConstraints, _ctx: &UiContext) -> Size {
272         let (_, _, w, h) = self.rect();
273         let pref_h = self.preferred_height().unwrap_or(h);
274         
275         let width = w.clamp(constraints.min_width, constraints.max_width);
276         let height = pref_h.clamp(constraints.min_height, constraints.max_height);
277         
278         Size { width, height }
279     }
280 
281     /// Land the CONTENT box at `origin`, the label strip hanging above it — the one
282     /// placement contract (`Adapted` repeats it over its measured content size).
283     fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
284         let size = self.measure(constraints, ctx);
285         let strip = self.label_strip();
286         self.set_rect(origin.x, origin.y - strip, size.width, size.height + strip);
287     }
288 
289     fn rect(&self) -> (f32, f32, f32, f32) {
290         let b = self.base();
291         (b.x, b.y, b.w, b.h)
292     }
293 
294     fn label(&self) -> Option<String> {
295         self.base().label.clone()
296     }
297 
298     // The value/polling block (`get_value_string`/`set_value_string`/`take_change`/
299     // `take_click`/`value`/`set_text`/`set_selected`) is GONE from the trait (6bd value
300     // shrink): apps drain widget state through the concrete inherent `Adapted<W>` methods
301     // (which forward to the narrow `Input` hooks). The last dyn readers went concrete-slot
302     // (TI's roster drain, cloud's JsonControl, designer's pane-focus sync).
303 
304     /// Dispatch a context-menu action on this widget. Returns whether it was applied.
305     /// Default inert; the adapter forwards to `Input::context_action` (whose default gives
306     /// every widget whole-value Cut/Copy/Paste through the value-string pair).
307     fn context_action(&mut self, _action: ContextAction) -> bool {
308         false
309     }
310 
311     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
312         let b = self.base_mut();
313         b.x = x;
314         b.y = y;
315         b.w = w;
316         b.h = h;
317     }
318 
319     fn set_row_rect(&mut self, x: f32, w: f32) {
320         let b = self.base_mut();
321         b.row_x = x;
322         b.row_w = w;
323     }
324 
325     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
326         if ctx.is_coordinate_covered(self.base().id(), px, py) {
327             return false;
328         }
329         let (x, y, w, h) = self.rect();
330         if w <= 0.0 || h <= 0.0 {
331             return false;
332         }
333         let b = self.base();
334         let (hx, hw) = if b.row_w > 0.0 { (b.row_x, b.row_w) } else { (x, w) };
335         px >= hx && px <= hx + hw && py >= y && py <= y + h
336     }
337 
338     // The direct-dispatch entry points (`cursor_moved`, `on_cursor_moved`, `mouse_input`,
339     // `mouse_wheel`, `keyboard_input`, `drag_begin`/`drag_update`/`drag_end`) are GONE from
340     // the trait (6bd collapse): every event delivery goes through `handle_event` — the entry
341     // points live on as inherent `Adapted<W>` methods for concrete in-crate forwards.
342 
343     // `hovered`/`set_hovered` are GONE from the trait (6bd batch 2): the state is the base
344     // `Widget::hovered` flag, read/written directly by the defaults above; Button/Checkbox
345     // keep inherent accessors for immediate-mode hosts.
346 
347     fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
348         // Focus/hover highlight color, folded from the zero-override `highlight_color` (6bd).
349         let is_focused = ctx.is_focused_id(self.base().id());
350         let hc = if is_focused {
351             colors::highlight_primary_color()
352         } else if self.base().hovered {
353             colors::HIGHLIGHT_SECONDARY
354         } else {
355             return None;
356         };
357         let b = self.base();
358         let hx = if b.row_w > 0.0 { b.row_x } else { b.x };
359         let hw = if b.row_w > 0.0 { b.row_w } else { b.w };
360         Some((hx, b.y, hw, b.h, hc))
361     }
362 
363     fn color(&self) -> [f32; 4];
364     fn solid_border(&self) -> Option<([f32; 4], f32)> { None }
365     fn plate_bevel(&self) -> Option<f32> { None }
366 
367     // `draggable`/`is_dragging` are GONE from the trait (the ControlPanel endgame
368     // removed their last stored-child-pointer consumer): the drag queries are concrete
369     // inherent `Adapted<W>` reads; index-driven rosters (TI, designer) route them
370     // through per-slot matches like the other value drains.
371 
372     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { Vec::new() }
373     fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> { Vec::new() }
374     fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> { Vec::new() }
375     
376     fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
377         let mut quads = self.extra_quads();
378         if let Some(hq) = self.highlight_quad(ctx) {
379             if hq.4 != colors::HIGHLIGHT_SECONDARY {
380                 quads.push(hq);
381             }
382         }
383         quads
384     }
385 
386     /// Emit this widget's OWN primitives (non-recursive) into the single paint pass (Phase 3).
387     /// The default composes the pieces the legacy recursive `all_*` emit for one node: rounded
388     /// background, plain/decoration quads, circles, and own text. Widgets with richer painting
389     /// (borders, relief primitives, arcs, vectors, SVGs) can override. Recursion into children and clipping
390     /// are handled by the paint walk (`scene::painter`), not here.
391     fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) {
392         use crate::scene::layout::Rect;
393         let (x, y, w, h) = self.rect();
394         let rect = Rect { x, y, width: w, height: h };
395         let color = self.color();
396 
397         if ui.tree.children_ptrs(self.base().id()).is_empty() {
398             // Leaf: emit its own rounded quads directly. For an ordinary widget this is just the
399             // rounded background; for widgets that override `all_rounded_quads` with custom
400             // geometry (e.g. Graph's nodes and edges) it captures that too. No recursion happens
401             // because there are no children.
402             for (qx, qy, qw, qh, r, c, corners) in self.all_rounded_quads(ui) {
403                 ctx.rounded_rect(Rect { x: qx, y: qy, width: qw, height: qh }, r, corners, c);
404             }
405         } else {
406             // Container: reconstruct its own plate (bevel / border / rounded background) — mirrors
407             // `push_widget_vertices`. Its children are drawn by the paint walk, so we must NOT call
408             // `all_rounded_quads` here (that would recurse and double-draw them).
409             let cr = self.corner_radii();
410             let radii = (cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left);
411             if let Some(depth) = self.plate_bevel() {
412                 ctx.bevel(rect, radii, &crate::scene::material::Material::from_fill(color), depth);
413             } else if let Some((border_color, thickness)) = self.solid_border() {
414                 ctx.border(rect, radii, color, border_color, thickness);
415             } else if color[3].abs() > 0.001 {
416                 let (radius, (r1, r2, r3, r4)) = self.corner_style();
417                 if r1 || r2 || r3 || r4 {
418                     ctx.rounded_rect(rect, radius, (r1, r2, r3, r4), color);
419                 }
420             }
421         }
422 
423         for (qx, qy, qw, qh, c) in self.all_quads(ui) {
424             ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, c);
425         }
426         for (cx, cy, r, t, start, end, c) in self.extra_arcs() {
427             ctx.arc(cx, cy, r, t, start, end, c);
428         }
429         for (cx, cy, r, c) in self.extra_circles() {
430             ctx.circle(cx, cy, r, c);
431         }
432         // Text: NONE by default. Every live legacy widget with own text carries a
433         // paint_self override (most via `scene::painter::paint_legacy_leaf` + its own
434         // labels); containers' text_labels aggregates are covered by the walk's descent
435         // (emitting them here would double-draw every descendant's text — the Phase 6d
436         // trap). Migrated widgets go through `Adapted::paint_self`, never this default.
437     }
438 
439     /// Whether the paint walk should clip this widget's children to its rect (scroll/root plate
440     /// containers). Default: no clipping.
441     fn clips_children(&self) -> bool { false }
442 
443     /// Whether this widget paints its ENTIRE subtree itself through its (recursive)
444     /// `all_rounded_quads` / `all_quads` — a legacy "subtree painter" such as `TreeList`, whose
445     /// row backgrounds and separators live in an `all_rounded_quads` override that also recurses
446     /// into its children. When true, the paint walk emits those directly and does NOT recurse
447     /// (the widget already did). Transitional: such widgets will eventually get a proper
448     /// non-recursive `paint_self`. Default: false.
449     fn renders_own_subtree(&self) -> bool { false }
450 
451     fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
452         if !self.visible() {
453             return Vec::new();
454         }
455         let mut quads = Vec::new();
456         let (radius, (r1, r2, r3, r4)) = self.corner_style();
457         if r1 || r2 || r3 || r4 {
458             let (x, y, w, h) = self.rect();
459             let c = self.color();
460             if c[3].abs() > 0.001 {
461                 quads.push((x, y, w, h, radius, c, (r1, r2, r3, r4)));
462             }
463         }
464         for &child_ptr in &ctx.tree.children_ptrs(self.base().id()) {
465             let widget = unsafe { &*child_ptr };
466             quads.extend(widget.all_rounded_quads(ctx));
467         }
468         quads
469     }
470 
471 
472     // The per-widget text getters (text_labels / text_labels_with_bounds /
473     // text_labels_with_font_and_bounds / get_text_items) are GONE: every widget emits
474     // its own text as display-list prims via paint_self (Adapted::paint_self for
475     // migrated widgets; paint_legacy_leaf-based overrides for the legacy leaves). The
476     // deleted default's base-label synthesis lives on in Adapted's base-label fallback,
477     // and its scroll-ancestor clamp in scene::painter::scroll_ancestor_text_bounds.
478 
479     fn widget_font(&self) -> Option<String> { None }
480     fn type_name(&self) -> &'static str {
481         let full_name = std::any::type_name::<Self>();
482         full_name.split("::").last().unwrap_or("Widget")
483     }
484     fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { None }
485     fn render_popover(&self, _pc: &mut dyn crate::layout::RenderTarget) {}
486 
487     fn focus(&mut self) {
488         self.base_mut().focused = true;
489     }
490     fn unfocus(&mut self) {
491         self.base_mut().focused = false;
492     }
493     fn focused(&self, ctx: &UiContext) -> bool {
494         ctx.is_focused_id(self.base().id())
495     }
496     fn prepare_text(&mut self, _fs: &mut cosmic_text::FontSystem) {}
497 
498     fn set_visible(&mut self, _visible: bool) {}
499     fn visible(&self) -> bool { true }
500     fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool { false }
501     fn wants_tick(&self) -> bool { false }
502     fn is_child_visible(&self, _child_id: WidgetId) -> bool { true }
503     fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
504 
505     // `set_parent`/`add_child` are GONE from the trait (6bd batch 4): linking is a tree
506     // operation — concrete callers ride the inherent `Adapted` methods, dyn callers go
507     // through `focus::link_parent_child` or `ctx.tree` directly. `parent`/`children` are
508     // GONE too (the plumbing retype): tree structure is read off `ctx.tree`
509     // (`parent_id`/`parent_ptr`/`child_ids`/`children_ptrs`) — the trait no longer
510     // proxies it, and no trait method returns a raw pointer. Paginator's field-derived
511     // child (the one `Layout::container_children` implementor) reaches the walks through
512     // the tree link its per-tick `register_embedded_children` maintains.
513 
514     fn z_index(&self) -> i32 { 0 }
515     fn is_scrollable(&self) -> bool { false }
516     fn blocks_root_plate_drag(&self) -> bool { true }
517 
518     /// Uniform corner radius + per-corner on-flags, in one read (6bd batch 2 — replaced the
519     /// separate `corner_radius`/`rounded_corners` getters). The radius is meaningful even with
520     /// every corner off: Menu/StatusBar report their parent's radius to children this way, so
521     /// the flags-off channel can't be folded into `corner_radii`.
522     fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) {
523         (12.0, (false, false, false, false))
524     }
525 
526     /// This widget's part in keyboard navigation — `Input::focus_role` through
527     /// the adapter; `FocusRole::None` for anything that is not a plate or a well.
528     fn focus_role(&self) -> FocusRole {
529         FocusRole::None
530     }
531 
532     fn corner_radii(&self) -> CornerRadii {
533         let (r, (tl, tr, br, bl)) = self.corner_style();
534         CornerRadii::new(
535             if tl { r } else { 0.0 },
536             if tr { r } else { 0.0 },
537             if br { r } else { 0.0 },
538             if bl { r } else { 0.0 },
539         )
540     }
541 }
542 
543 // The `Control` subtrait (set_label + control_label) is DELETED (6bd value shrink):
544 // zero dyn consumers and zero `control_label()` callers remained; `set_label` lives on as
545 // the inherent `Adapted<W>` method every call site already resolved to (it shadowed the
546 // trait), and detached-label paint moved to the adapter in the Phase 5 leaf sweeps.
547 
548 pub mod core;
549 pub mod input;
550 pub mod plate_dock;
551 pub mod container;
552 pub mod display;
553 pub mod editor;
554 pub mod layout_helper;
555 pub mod model;
556 pub mod scroll_region;
557 pub mod scroll_motion;
558  
559 // Re-exports
560 pub use self::editor::TextEditorState;
561 pub use self::layout_helper::{ColumnLayout, RowLayout};
562 pub use self::scroll_region::{ScrollRegion, ScrollbarActivity};
563 pub use self::scroll_motion::{Bounds, ScrollAxis, ScrollMotion, ScrollPhase, ScrollSettings, LINE_PX};
564 pub use self::model::{Adapted, EventCtx, Input, Layout, Paint};
565 pub use self::core::{Widget, focus, hover_animation, clipboard, context_menu, clear_widget_references};
566 pub use self::core::focus::link_parent_child;
567 pub use self::input::{
568     Button, TextBox, Spinbox, Dropdown, Checkbox, Toggle, Slider, RangeSlider,
569     ColorSelector, Finger, Trackpad, get_font_db, ActiveThumb, FontSelector,
570     BevelPreview, bevel_ease, parse_bevel_knobs, RampPreview,
571     ButtonStrip, KeybindRecorder, Ramp, RampKey, ColorRamp, ColorRampKey,
572     format_ramp_spec, parse_ramp_spec
573 };
574 pub use self::container::{
575     Group, GroupFrame,
576     ContainerLayout, OverlayLayout, ManualLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
577     ColumnsLayout, MosaicLayout, ReverseMosaicLayout,
578     ContentBg, ParametersBg,
579     ScrollBox, MenuBar, Spreadsheet, Breadcrumb,
580     Paginator, TreeList, TreeElement
581 };
582 pub use self::display::{
583     TextLabel, Label, StyledLabel, LabelPrim, TextItem, UsageBar,
584     InfoBox, StatusDot, InteractiveListItem,
585     GraphNode, Graph, TaggedQuad, Float3, ProgressBar, StatusBar, Splitter, Node, Separator,
586     DotStatus, Panel, ImageView, serialize_widgets,
587     truncate_head, truncate_tail,
588 };
589 
590 pub trait PageSelector {
591     fn selected_page(&self) -> usize;
592     fn set_selected_page(&mut self, page: usize);
593     fn sidebar_w(&self) -> f32;
594 }
595 
596 pub trait MenuController {
597     fn menu_click(&mut self) -> Option<(usize, usize)>;
598     fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize);
599     fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool);
600     fn set_menu_items(&mut self, menu_idx: usize, items: &[String]);
601     fn is_menu_bar(&self) -> bool;
602     fn is_menu_open(&self) -> bool;
603     fn menu_items(&self) -> Vec<String>;
604     fn menu_item_checked(&self) -> Vec<Option<bool>>;
605     fn is_vertical(&self) -> bool;
606     fn menu_names(&self) -> Vec<String>;
607     fn menu_items_list(&self) -> Vec<Vec<String>>;
608     fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>>;
609     fn take_context_change(&mut self) -> Option<usize>;
610     fn set_context_selected(&mut self, selected: usize);
611     fn set_center_items(&mut self, center: bool);
612     fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)>;
613 }
614 
615 pub trait GraphController {
616     fn set_nodes(&mut self, nodes: &[GraphNode]);
617     fn get_nodes(&self) -> Vec<GraphNode>;
618     fn selected_node(&self) -> Option<usize>;
619     fn set_selected_node(&mut self, idx: Option<usize>);
620     fn double_clicked_node(&self) -> Option<usize>;
621     fn clear_double_clicked_node(&mut self);
622     fn set_grid_snap_enabled(&mut self, enabled: bool);
623     fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)>;
624     fn set_grid_snap(&mut self, gx: f32, gy: f32);
625     /// The grid's ONE size per axis: the pitch, from the centre of one grid
626     /// line to the centre of the next. Nodes are centred on the lattice
627     /// intersections. Leaves the node size alone.
628     fn set_grid_pitch(&mut self, px: f32, py: f32);
629     /// The node body's size, independent of the pitch — hosts scale it with
630     /// their zoom as they scale the pitch.
631     fn set_node_size(&mut self, w: f32, h: f32);
632     /// The older cell-and-gap description of the same lattice — a cell plus
633     /// its gap is a pitch, and the node body is the cell. Kept for hosts
634     /// that still speak it (cce-files, cce-graph); new code sets the pitch.
635     fn set_grid_sizes(&mut self, gx: f32, gy: f32);
636     /// The gap half of the cell-and-gap description; see [`set_grid_sizes`].
637     fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32);
638     /// The lattice intersection node (0, 0) is centred on, window-absolute.
639     fn set_grid_origin(&mut self, ox: f32, oy: f32);
640     fn grid_origin(&self) -> (f32, f32);
641     fn set_show_network_grid(&mut self, show: bool);
642     fn take_pending_connection(&mut self) -> Option<(String, String)>;
643     /// A node dropped onto a wire, to be spliced in between its ends:
644     /// (dragged node id, the wire's upstream node NAME — what Input params
645     /// store, the wire's downstream node id). The host rewires both Input
646     /// params: dragged.Input = upstream name, downstream.Input = dragged's
647     /// name. Default None for hosts whose graphs have no wires to splice.
648     fn take_pending_splice(&mut self) -> Option<(String, String, String)> {
649         None
650     }
651     fn cancel_connecting(&mut self);
652     fn is_node_rect(&self, qx: f32, qy: f32, qw: f32, qh: f32) -> bool;
653     /// The topmost node whose body contains (px, py), window-absolute coords.
654     fn node_at(&self, px: f32, py: f32) -> Option<usize>;
655     /// The corner radius of anything node-shaped on the grid at the current
656     /// zoom — the cursor, the drop-target highlight (0 = square).
657     fn cell_corner_radius(&self) -> f32;
658     /// The flat-geometry emission with grid cells tagged by their surviving
659     /// rounded corners — for hosts that draw the graph's quads themselves
660     /// (the designer) and want cells as superellipse tiles.
661     fn geometry_quads_tagged(&self, rect: crate::scene::layout::Rect) -> Vec<TaggedQuad>;
662     /// The grid lines and origin axes, flat, over what is beneath — for
663     /// hosts that draw the graph's quads themselves, called at the point in
664     /// their walk where the grid goes (under the wires and nodes).
665     fn paint_grid(&self, rect: crate::scene::layout::Rect, pc: &mut crate::scene::paint::PaintCtx);
666     /// The pixel rect the in-flight node drag will deposit its body on
667     /// (`commit_drag`'s resolution), for hosts' drop-target highlight.
668     /// None outside a node drag.
669     fn drop_target_cell_rect(&self) -> Option<(f32, f32, f32, f32)>;
670 }
671 
672 pub trait SpreadsheetController {
673     fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>);
674 }
675 
676 pub trait PathController {
677     fn set_path(&mut self, segments: &[String]);
678     fn path_click(&mut self) -> Option<usize>;
679 }
680 
681 pub trait ParamController {
682     fn node_params(&self) -> Vec<(String, String, String)>;
683     fn set_display_params(&mut self, params: &[(String, String, String)]);
684 }
685 
686 pub trait GeomController {
687     fn set_geom_visible(&mut self, visible: bool);
688     fn geom_visible(&self) -> bool;
689     fn take_geom_toggle(&mut self) -> bool;
690 }
691 
692 #[derive(Debug, Clone, Copy, PartialEq)]
693 pub struct CornerRadii {
694     pub top_left: f32,
695     pub top_right: f32,
696     pub bottom_right: f32,
697     pub bottom_left: f32,
698 }
699 
700 impl CornerRadii {
701     pub fn new(tl: f32, tr: f32, br: f32, bl: f32) -> Self {
702         Self { top_left: tl, top_right: tr, bottom_right: br, bottom_left: bl }
703     }
704 
705     pub fn uniform(radius: f32) -> Self {
706         Self::new(radius, radius, radius, radius)
707     }
708 }
709 
710 pub fn match_key_shortcut(event: &KeyEvent, shortcut_str: &str) -> bool {
711     let shortcut_lower = shortcut_str.to_lowercase();
712     let parts: Vec<&str> = shortcut_lower.split('+').collect();
713     
714     let mut req_ctrl = false;
715     let mut req_shift = false;
716     let mut req_alt = false;
717     let mut req_key = "";
718 
719     for part in parts {
720         match part {
721             "ctrl" | "control" => req_ctrl = true,
722             "shift" => req_shift = true,
723             "alt" | "meta" => req_alt = true,
724             // Super chords belong to the compositor; a client never sees them.
725             "super" | "win" | "logo" => {}
726             k => req_key = k,
727         }
728     }
729 
730     if event.ctrl != req_ctrl { return false; }
731     if event.shift != req_shift { return false; }
732     if event.alt != req_alt { return false; }
733     
734     if let Key::Character(ref ch) = event.logical_key {
735         let ch_lower = ch.to_lowercase();
736         if req_key.len() == 1 {
737             return ch_lower == req_key;
738         } else {
739             let mapped_key = match req_key {
740                 "slash" => "/",
741                 "enter" => "enter",
742                 "escape" => "escape",
743                 "space" => " ",
744                 k => k,
745             };
746             return ch_lower == mapped_key;
747         }
748     } else if let Key::Named(nk) = event.logical_key {
749         let nk_str = format!("{:?}", nk).to_lowercase();
750         return nk_str == req_key;
751     }
752     false
753 }
754