Skip to main content

kui_core/
spec.rs

1//! [`NodeSpec`] and [`TextStyle`]: everything a node and a text declare,
2//! as plain data with a builder.
3//!
4//! A `NodeSpec` is the contract every frontend lowers into, whether a Rust
5//! builder chain, a Lua table or a JSX element. It groups the layout
6//! ([`LayoutSpec`]: sizing, direction, padding, gap, alignment, overflow),
7//! the look ([`VisualStyle`]: background, border, radius, shadow, opacity)
8//! and, boxed because most nodes declare none of it, the events, animation,
9//! accessibility and interaction styling. Every builder method sets one of
10//! those fields; the field docs below say what each means.
11//!
12//! ```rust
13//! use kui_core::{Align, Color, NodeSpec, Sizing, TextStyle, TextWrap};
14//!
15//! // A card: grows across its parent, fits its content down, 12 px padding
16//! // and gap, rounded, with a hover background and a click payload.
17//! let card = NodeSpec::column()
18//!     .width(Sizing::GROW)
19//!     .pad(12.0)
20//!     .gap(8.0)
21//!     .bg(Color::hex(0x1e2230ff))
22//!     .hover_bg(Color::hex(0x262b3aff))
23//!     .radius(8.0)
24//!     .on_click("open-card");
25//!
26//! // A toolbar row: fixed height, children centred across it.
27//! let toolbar = NodeSpec::row().size(Sizing::GROW, 36.0).cross_align(Align::Center);
28//!
29//! // A one-line title, cut with an ellipsis when it does not fit.
30//! let title = TextStyle::new(16.0).ellipsis().color(Color::WHITE);
31//! let code = TextStyle::new(13.0).mono().wrap(TextWrap::Glyph);
32//!
33//! assert_eq!(card.layout.padding.l, 12.0);
34//! assert!(card.events().on_click.is_some());
35//! assert_eq!(toolbar.layout.height, Sizing::Fixed(36.0));
36//! assert_eq!(title.max_lines, 0); // `ellipsis` alone means one line
37//! assert!(title.ellipsis && code.wrap == TextWrap::Glyph);
38//! ```
39
40use crate::anim::{Easing, Repeat, Transition};
41use crate::color::Color;
42use crate::cursor::CursorShape;
43use crate::enter::Enter;
44use crate::geom::Edges;
45use crate::input::Buttons;
46use crate::keyframes::Keyframe;
47use crate::value::Value;
48use crate::window::{WindowButton, WindowRole};
49
50pub use crate::access::{Label, Live, Role};
51
52/// How a node sizes one axis. A plain number converts to `Fixed`, so
53/// `.width(120.0)` and `.width(Sizing::Fixed(120.0))` are the same.
54#[derive(Clone, Copy, Debug, Default, PartialEq)]
55pub enum Sizing {
56    /// Size to content.
57    #[default]
58    Fit,
59    /// Share leftover space, weighted by factor.
60    Grow(f32),
61    /// Absolute logical pixels.
62    Fixed(f32),
63    /// Fraction of the parent's content box (0.0..=1.0).
64    Percent(f32),
65    /// A size expression resolved against the parent's content box, the
66    /// box a `Percent` takes its cut of: `"clamp(400px, 80%, 1000px)"`
67    /// (see [`crate::calc`]). Layout treats it as it treats a `Percent`:
68    /// nothing in the fit pass, its size once the parent's is known, and
69    /// a parent that overflows shrinks it as it would one.
70    Calc(crate::calc::Calc),
71}
72
73/// What a `min_width` / `max_width` / `min_height` / `max_height`
74/// declares: px, the node's own fit size (a min only), or a size
75/// expression that layout resolves against the parent's content box when
76/// it sizes the node. Until then a calc bound clamps like none, as a
77/// percentage clamp does in CSS's intrinsic sizing.
78///
79/// A calc clamp rides in the clamp's own `f32` as a negative, the way
80/// [`Min::FIT`] does (`min_w` below -1, [`Min::calc`]; `max_w` below 0,
81/// [`max_calc`]), and layout writes the resolved px over it. A reader
82/// after layout reads a number; one before it goes through
83/// [`LayoutSpec::max_w_px`].
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub enum Bound {
86    Px(f32),
87    Fit,
88    Calc(crate::calc::Calc),
89}
90
91/// A ceiling that is a size expression, as `max_w` / `max_h` hold it
92/// until layout resolves it: -1 minus its number.
93pub fn max_of_calc(c: crate::calc::Calc) -> f32 {
94    -1.0 - c.id() as f32
95}
96
97/// A px ceiling as `max_w` / `max_h` hold it: never below zero, so a
98/// negative there is only ever [`max_of_calc`]'s. A negative or `NaN`
99/// ceiling is a ceiling of 0. Every binding's max clamp reaches the field
100/// through [`NodeSpec::max_width`] / [`NodeSpec::max_height`], which call
101/// this.
102pub fn px_ceiling(px: f32) -> f32 {
103    // `f32::max` takes the other operand over a `NaN`.
104    px.max(0.0)
105}
106
107/// The size expression a `max_w` / `max_h` waits on, if it is one.
108pub fn max_calc(v: f32) -> Option<crate::calc::Calc> {
109    (v < 0.0)
110        .then(|| crate::calc::Calc::from_id((-v - 1.0) as u32))
111        .flatten()
112}
113
114impl From<f32> for Bound {
115    fn from(px: f32) -> Self {
116        Bound::Px(px)
117    }
118}
119
120impl From<Min> for Bound {
121    fn from(m: Min) -> Self {
122        if m.is_fit() {
123            Bound::Fit
124        } else if let Some(c) = m.as_calc() {
125            Bound::Calc(c)
126        } else if m.is_auto() {
127            // Undeclared stays undeclared through `min_width`.
128            Bound::Px(-0.0)
129        } else {
130            Bound::Px(m.resolved())
131        }
132    }
133}
134
135/// A lower clamp on one axis: a number of logical px, or the node's own
136/// fit size on that axis ([`Min::FIT`], `minWidth: "fit"` in the
137/// bindings). `FIT` is what lets a `Grow` child keep a content floor, like
138/// CSS's `flex: 1 0 auto`: tabs split a bar evenly while they fit and sit
139/// at their label's width, scrolling, once they do not. Layout resolves it
140/// to a number in the fit pass of its axis, so every later clamp reads
141/// one; until then it clamps like no floor at all.
142///
143/// Undeclared is [`Min::AUTO`], which clamps as 0 everywhere but one
144/// place: a child giving in an overflowing row that holds a share of the
145/// room, where it is CSS's `min-width: auto`, the child's min-content. A
146/// declared 0 (`Min::px(0.0)`) is no floor there either, as CSS's
147/// `min-width: 0` is how a flex item is let go below its content.
148///
149/// Stored as one `f32` with `FIT` and a calc as negatives (the form the
150/// C struct's `min_w` takes too), because `LayoutSpec` is copied per node
151/// per frame.
152#[derive(Clone, Copy, Debug, PartialEq)]
153pub struct Min(f32);
154
155impl Default for Min {
156    fn default() -> Self {
157        Min::AUTO
158    }
159}
160
161impl Min {
162    /// The node's own fit size on this axis.
163    pub const FIT: Min = Min(-1.0);
164
165    /// No floor declared: the node's min-content where a share of the room
166    /// gives in CSS's way and down a column of fit children; 0 across a
167    /// run of fit children alone, across a column, where a fit node is
168    /// held to the column's box, and for a node that scrolls or clips.
169    /// Negative zero, so it is 0 to every clamp and to `==`, and told
170    /// apart from a declared 0 by its sign alone ([`Min::is_auto`]).
171    pub const AUTO: Min = Min(-0.0);
172
173    /// A floor of `v` logical px; a negative (or `NaN`) is a floor of 0,
174    /// declared — no automatic one either.
175    pub fn px(v: f32) -> Min {
176        Min(if v > 0.0 { v } else { 0.0 })
177    }
178
179    /// Whether no floor was declared ([`Min::AUTO`]).
180    pub fn is_auto(self) -> bool {
181        self.0 == 0.0 && self.0.is_sign_negative()
182    }
183
184    /// Whether this is the unresolved fit floor.
185    pub fn is_fit(self) -> bool {
186        self.0 == Min::FIT.0
187    }
188
189    /// A floor that is a size expression, until layout resolves it: -2
190    /// minus its number, below `FIT`'s -1.
191    pub fn calc(c: crate::calc::Calc) -> Min {
192        Min(-2.0 - c.id() as f32)
193    }
194
195    /// The size expression this floor waits on, if it is one.
196    pub fn as_calc(self) -> Option<crate::calc::Calc> {
197        (self.0 <= -2.0)
198            .then(|| crate::calc::Calc::from_id((-self.0 - 2.0) as u32))
199            .flatten()
200    }
201
202    /// Whether layout has yet to resolve this floor — a `FIT` or a calc,
203    /// the negatives — without asking the table which calc it is.
204    pub(crate) fn deferred(self) -> bool {
205        self.0 < 0.0
206    }
207
208    /// The clamp as a number: the px it holds, or 0 for a `FIT` or a calc
209    /// layout has not resolved yet (nothing to floor at).
210    pub fn resolved(self) -> f32 {
211        if self.0 > 0.0 { self.0 } else { 0.0 }
212    }
213}
214
215impl From<f32> for Min {
216    fn from(v: f32) -> Self {
217        Min::px(v)
218    }
219}
220
221/// A number is that many logical px, so `.width(120.0)` is
222/// `.width(Sizing::Fixed(120.0))`, the way `min_width(120.0)` already read.
223impl From<f32> for Sizing {
224    fn from(px: f32) -> Self {
225        Sizing::Fixed(px)
226    }
227}
228
229impl Sizing {
230    /// `Grow(1.0)`: an equal share of the leftover space, the weight
231    /// nearly every grow declares.
232    pub const GROW: Sizing = Sizing::Grow(1.0);
233}
234
235impl Sizing {
236    /// The sizing the way a spec spells it — `fit`, `grow(1)`, `120px`,
237    /// `50%` — for a reader: the devtools' inspector and a `nodes()` row.
238    pub fn describe(self) -> String {
239        match self {
240            Sizing::Fit => "fit".into(),
241            Sizing::Grow(w) => format!("grow({w})"),
242            Sizing::Fixed(px) => format!("{px}px"),
243            Sizing::Percent(p) => format!("{}%", p * 100.0),
244            Sizing::Calc(c) => c.describe(),
245        }
246    }
247
248    /// The animatable number inside: a grow factor, a px size, a fraction.
249    /// None for `Fit` and a `Calc`, which have nothing to ease.
250    pub fn amount(self) -> Option<f32> {
251        match self {
252            Sizing::Fit | Sizing::Calc(_) => None,
253            Sizing::Grow(v) | Sizing::Fixed(v) | Sizing::Percent(v) => Some(v),
254        }
255    }
256
257    /// The same form with a different amount (`Fit` and a `Calc` stay).
258    pub fn with_amount(self, v: f32) -> Self {
259        match self {
260            Sizing::Fit => Sizing::Fit,
261            Sizing::Calc(c) => Sizing::Calc(c),
262            Sizing::Grow(_) => Sizing::Grow(v),
263            Sizing::Fixed(_) => Sizing::Fixed(v),
264            Sizing::Percent(_) => Sizing::Percent(v),
265        }
266    }
267}
268
269/// Which way a container stacks its children.
270#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
271pub enum Dir {
272    Row,
273    #[default]
274    Column,
275}
276
277impl Dir {
278    /// The `dir` row's spelling.
279    pub fn name(self) -> &'static str {
280        match self {
281            Dir::Row => "row",
282            Dir::Column => "column",
283        }
284    }
285}
286
287/// Where children sit along an axis, and where a float attaches.
288///
289/// The first three apply to every axis. The rest each mean something on
290/// one axis only: the three spreads on `main_align`, `Baseline` on a row's
291/// `cross_align`. Anywhere else one lays out as `Start`
292/// (`SpaceAround`/`SpaceEvenly` as `Center`), with an `align-ignored`
293/// warning.
294#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
295pub enum Align {
296    #[default]
297    Start,
298    Center,
299    End,
300    /// Main axis: the free space goes between the children, none at the
301    /// ends — CSS's `space-between`. One child sits at the start.
302    SpaceBetween,
303    /// Main axis: each child gets an equal share of the free space, half
304    /// on either side, so the ends get half what a gap does — CSS's
305    /// `space-around`. One child is centred.
306    SpaceAround,
307    /// Main axis: the free space splits into equal gaps between the
308    /// children and at both ends — CSS's `space-evenly`. One child is
309    /// centred.
310    SpaceEvenly,
311    /// A row's cross axis: the first baselines of the children's text line
312    /// up, so a label and a larger value on one row read as one line. A
313    /// child with no text inside it aligns by its bottom edge; a `grow` or
314    /// percent height fills the line and sits at its top.
315    Baseline,
316}
317
318impl Align {
319    /// The `align` rows' spelling (`schema::ALIGNS`).
320    pub fn name(self) -> &'static str {
321        crate::schema::ALIGNS[self as usize]
322    }
323}
324
325/// What a floating node is positioned against.
326#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
327pub enum FloatAnchor {
328    /// The parent node's border box.
329    #[default]
330    Parent,
331    /// The whole viewport.
332    Viewport,
333    /// The border box of the node `Key` names, wherever it is in the
334    /// tree, even after this one in preorder: such a float is laid out
335    /// again in a sixth pass once its anchor is placed. What a devtools
336    /// tab's content is: built in the app's part of the tree, shown over
337    /// the panel's tab body. No binding spells it; the core builds it.
338    Node(crate::key::Key),
339}
340
341/// Takes a node out of flex flow: it does not consume space in its parent,
342/// sizes Grow/Percent against its anchor, is positioned by attach points,
343/// and escapes ancestor clips unless [`FloatConfig::clip`] keeps it in its
344/// parent's. It paints as a layer of its own, above the in-flow tree and
345/// every float that opened before it and under every one that opened
346/// after, and takes input in the same order.
347///
348/// ```rust
349/// use kui_core::{Align, FloatConfig, NodeSpec};
350///
351/// let tooltip = NodeSpec::column().float(FloatConfig::below().fit());
352/// let toast = NodeSpec::row().float(FloatConfig::viewport().inside(Align::End, Align::End).offset(-16.0, -16.0));
353/// assert!(tooltip.layout.float.unwrap().fit);
354/// assert_eq!(toast.layout.float.unwrap().offset.x, -16.0);
355/// ```
356#[derive(Clone, Copy, Debug, PartialEq)]
357pub struct FloatConfig {
358    pub anchor: FloatAnchor,
359    /// Attach point on the anchor rect (horizontal, vertical).
360    pub anchor_point: (Align, Align),
361    /// Attach point on the floating node itself.
362    pub self_point: (Align, Align),
363    /// Extra offset applied after attaching, logical px.
364    pub offset: Vec2Offset,
365    /// Keep the float on screen: if the attached placement leaves the
366    /// viewport on an axis, mirror the attachment across the anchor on that
367    /// axis (below ↔ above, after ↔ before) when that fits better, then
368    /// clamp whatever still overflows. Tooltips/menus want this.
369    ///
370    /// This is the in-window approximation of an OS popup: a float costs
371    /// one tree, one hit list and one draw call. What it cannot do is leave
372    /// the window, so a dropdown taller than the viewport, or a menu with
373    /// nowhere in-window to go, is clamped rather than placed.
374    pub fit: bool,
375    /// Take the parent's clip, as a child does, instead of escaping every
376    /// ancestor's: a node on a `clip` canvas panned past the canvas's edge
377    /// is cut there, and its hit region with it, rather than drawn over
378    /// and clicked through the toolbar beside it. Only a
379    /// [`FloatAnchor::Parent`] float reads it; a viewport or node anchor is
380    /// placed against something other than the parent, and escapes with it
381    /// set or not. A declared float's paint order is unchanged: it is a
382    /// layer of its own still. A `line`, `polygon` or `path` anchored in
383    /// its parent's box has it set by the core, and with it paints in the
384    /// parent's layer at its place in the tree, as a child does — it opens
385    /// no layer (backlog F123; `Tree::opens_layer`).
386    pub clip: bool,
387}
388
389/// Plain offset pair (kept separate from geometry to stay `Copy` + FFI-flat).
390#[derive(Clone, Copy, Debug, Default, PartialEq)]
391pub struct Vec2Offset {
392    pub x: f32,
393    pub y: f32,
394}
395
396impl Default for FloatConfig {
397    fn default() -> Self {
398        Self {
399            anchor: FloatAnchor::Parent,
400            anchor_point: (Align::Start, Align::Start),
401            self_point: (Align::Start, Align::Start),
402            offset: Vec2Offset::default(),
403            fit: false,
404            clip: false,
405        }
406    }
407}
408
409impl FloatConfig {
410    /// Anchored to the parent's top-left corner.
411    pub fn parent() -> Self {
412        Self::default()
413    }
414
415    /// Anchored to the viewport's top-left corner.
416    pub fn viewport() -> Self {
417        Self {
418            anchor: FloatAnchor::Viewport,
419            ..Self::default()
420        }
421    }
422
423    /// Tooltip-style: hang below the parent, centered.
424    pub fn below() -> Self {
425        Self {
426            anchor: FloatAnchor::Parent,
427            anchor_point: (Align::Center, Align::End),
428            self_point: (Align::Center, Align::Start),
429            offset: Vec2Offset { x: 0.0, y: 6.0 },
430            ..Self::default()
431        }
432    }
433
434    /// Tooltip-style: hover above the parent, centered.
435    pub fn above() -> Self {
436        Self {
437            anchor: FloatAnchor::Parent,
438            anchor_point: (Align::Center, Align::Start),
439            self_point: (Align::Center, Align::End),
440            offset: Vec2Offset { x: 0.0, y: -6.0 },
441            ..Self::default()
442        }
443    }
444
445    /// The attach point on the anchor.
446    pub fn at(mut self, x: Align, y: Align) -> Self {
447        self.anchor_point = (x, y);
448        self
449    }
450
451    /// The attach point on the float itself.
452    pub fn self_at(mut self, x: Align, y: Align) -> Self {
453        self.self_point = (x, y);
454        self
455    }
456
457    /// [`Self::at`] and [`Self::self_at`] at the same point: the float sits
458    /// inside the anchor against that edge or corner — `(End, End)` is a
459    /// toast in the viewport's bottom-right, `(Center, Center)` a dialog.
460    #[inline]
461    pub fn inside(self, x: Align, y: Align) -> Self {
462        self.at(x, y).self_at(x, y)
463    }
464
465    /// An extra offset after attaching, logical px.
466    pub fn offset(mut self, x: f32, y: f32) -> Self {
467        self.offset = Vec2Offset { x, y };
468        self
469    }
470
471    /// Flip across the anchor / clamp as needed to stay in the viewport;
472    /// see [`FloatConfig::fit`] for where that stops being enough.
473    pub fn fit(mut self) -> Self {
474        self.fit = true;
475        self
476    }
477
478    /// Cut by the parent's clip instead of escaping it; see
479    /// [`FloatConfig::clip`] for which anchors read it.
480    pub fn clipped(mut self) -> Self {
481        self.clip = true;
482        self
483    }
484
485    /// Whether this float takes its parent's clip: [`FloatConfig::clip`]
486    /// declared on a parent-anchored float. The one reading both paint
487    /// passes and the hit regions share.
488    pub fn clipped_by_parent(&self) -> bool {
489        self.clip && self.anchor == FloatAnchor::Parent
490    }
491
492    /// A preset by its wire index — its position in [`FLOAT_PRESETS`], which
493    /// is what the binary protocol carries and what `KUI_FLOAT_*` counts
494    /// from. `None` for an index no preset claims.
495    pub fn preset_at(i: usize) -> Option<Self> {
496        Some(match i {
497            0 => Self::parent(),
498            1 => Self::viewport(),
499            2 => Self::below(),
500            3 => Self::above(),
501            _ => return None,
502        })
503    }
504
505    /// A preset by name. Every binding that spells a float as a word —
506    /// `float="below"`, `float = "above"`, `kui_spec_float_preset` — resolves
507    /// it here, so "below" cannot mean one thing in JS and another in Lua.
508    pub fn preset(name: &str) -> Option<Self> {
509        Self::preset_at(FLOAT_PRESETS.iter().position(|p| *p == name)?)
510    }
511
512    /// One config from the pieces a binding can extract without deciding
513    /// anything: a `base` preset (from [`FloatConfig::preset`] or
514    /// [`FloatConfig::preset_at`]) and the overrides that were actually
515    /// declared. `None` leaves the base's own value — that is what lets
516    /// `float="below"` keep its 6px gap while `{ anchor: "below", dx: 2 }`
517    /// moves it sideways without flattening the gap to zero. `fit` and
518    /// `clip` are ORed in: no preset sets either.
519    pub fn build(
520        base: FloatConfig,
521        anchor_at: Option<(Align, Align)>,
522        self_at: Option<(Align, Align)>,
523        dx: Option<f32>,
524        dy: Option<f32>,
525        fit: bool,
526        clip: bool,
527    ) -> Self {
528        let mut cfg = base;
529        if let Some((x, y)) = anchor_at {
530            cfg.anchor_point = (x, y);
531        }
532        if let Some((x, y)) = self_at {
533            cfg.self_point = (x, y);
534        }
535        if let Some(x) = dx {
536            cfg.offset.x = x;
537        }
538        if let Some(y) = dy {
539            cfg.offset.y = y;
540        }
541        cfg.fit |= fit;
542        cfg.clip |= clip;
543        cfg
544    }
545}
546
547/// The float preset names, in wire order: the index of a name here is what
548/// the binary protocol writes for it and what `KUI_FLOAT_*` counts from.
549pub const FLOAT_PRESETS: &[&str] = &["parent", "viewport", "below", "above"];
550
551/// `overflow` as bits: the C struct's field, the binary wire's payload and
552/// what the `clip` / `scrollX` / `scrollY` booleans OR together. One set of
553/// values so a binding cannot invent its own numbering.
554pub const OVERFLOW_CLIP: u32 = 1 << 0;
555pub const OVERFLOW_SCROLL_X: u32 = 1 << 1;
556pub const OVERFLOW_SCROLL_Y: u32 = 1 << 2;
557
558/// The `pad` shorthand family as declared — any subset of the seven names,
559/// each `None` when the frontend did not see it. [`PadShorthand::resolve`]
560/// decides what a missing edge falls back to; a binding only reports what
561/// it found.
562#[derive(Clone, Copy, Debug, Default, PartialEq)]
563pub struct PadShorthand {
564    /// `pad`: all four edges.
565    pub all: Option<f32>,
566    /// `padX`: left and right.
567    pub x: Option<f32>,
568    /// `padY`: top and bottom.
569    pub y: Option<f32>,
570    pub l: Option<f32>,
571    pub r: Option<f32>,
572    pub t: Option<f32>,
573    pub b: Option<f32>,
574}
575
576impl PadShorthand {
577    /// True when the frontend saw any of the seven names.
578    pub fn declared(self) -> bool {
579        [self.all, self.x, self.y, self.l, self.r, self.t, self.b]
580            .iter()
581            .any(Option::is_some)
582    }
583
584    /// Four edges: an edge falls back to its axis, an axis to the all-round
585    /// `pad`, and `pad` to zero. The specific value always wins.
586    pub fn resolve(self) -> Edges {
587        let all = self.all.unwrap_or(0.0);
588        let (x, y) = (self.x.unwrap_or(all), self.y.unwrap_or(all));
589        Edges {
590            l: self.l.unwrap_or(x),
591            r: self.r.unwrap_or(x),
592            t: self.t.unwrap_or(y),
593            b: self.b.unwrap_or(y),
594        }
595    }
596}
597
598/// The layout half of a [`NodeSpec`]: sizing, clamps, direction, padding,
599/// gap, alignment, overflow and floating. Set through the `NodeSpec`
600/// builders; read by the solver.
601#[derive(Clone, Copy, Debug, PartialEq)]
602pub struct LayoutSpec {
603    pub width: Sizing,
604    pub height: Sizing,
605    /// Clamps applied after `width`/`height` resolve (Fit, Grow, Percent and
606    /// Fixed alike), so "grow but at most N" and "fit but at least N" work.
607    /// A min may also be [`Min::FIT`]: "grow but never below my content".
608    pub min_w: Min,
609    /// A ceiling in px, or, as a negative, a size expression layout has
610    /// yet to resolve ([`max_of_calc`]); set it through
611    /// [`NodeSpec::max_width`], which keeps a px one at or above zero
612    /// ([`px_ceiling`]). `max_h` is the same.
613    pub max_w: f32,
614    pub min_h: Min,
615    pub max_h: f32,
616    pub dir: Dir,
617    pub padding: Edges,
618    pub gap: f32,
619    /// Row children that don't fit the main-axis content box start a new
620    /// line instead of overflowing (or shrinking). Rows only: breaking
621    /// needs a definite main size, and the pass order gives a row one —
622    /// its width is final before its height is measured — where a column
623    /// would need its height first. Ignored on a column and on a
624    /// `scroll_x` row, both with a warning (`diag::WRAP_IGNORED`).
625    pub wrap: bool,
626    /// A column whose rows' children line up in columns: the nth in-flow
627    /// child of every in-flow row is a cell of column n, and
628    /// a column is as wide as its widest cell — its cells' own `width`s
629    /// say how the column sizes (a `Fixed` or `Fit` cell is content that
630    /// sets the column's fit width, a `Grow` cell makes the column grow,
631    /// a `Percent` one takes its cut of the row) and their `minWidth` /
632    /// `maxWidth` clamp it. The rows are ordinary rows — a row's `gap` is
633    /// the space between its cells, its `padding` its own, and it takes
634    /// its background, its click and its hover as any row does — except
635    /// that a row of a table never wraps (`diag::WRAP_IGNORED`). Set by
636    /// [`NodeSpec::table`], which is a column; `dir` stays `Column`, and
637    /// the flag is read on a column only ([`LayoutSpec::is_table`]): a
638    /// `Row` carrying it — a shape only these public fields can build —
639    /// is the row it says it is. The rows are the in-flow `Row` children;
640    /// a column, a table or a leaf straight under the table is a child
641    /// with its own width and no cells.
642    pub table: bool,
643    /// Space between wrap lines, across the main axis. `gap` is still the
644    /// space between children along it.
645    pub cross_gap: f32,
646    /// Alignment of children along the main axis, the spreads
647    /// (`SpaceBetween` / `SpaceAround` / `SpaceEvenly`) included.
648    pub main_align: Align,
649    /// Alignment of children across the main axis; `Baseline` on a row.
650    pub cross_align: Align,
651    /// Width over height (CSS's `aspect-ratio`); 0 = none.
652    /// It sizes the axis whose sizing is `Fit`: a fit height is the final
653    /// width over the ratio, and a fit width under a `Fixed` height is
654    /// that height times it. With both axes declared it has nothing to set.
655    /// The derived axis is neither shrunk nor fitted to the children,
656    /// which overflow it — `min_h: Min::FIT` floors it at them.
657    pub aspect: f32,
658    /// Clip children to this node's rect.
659    pub clip: bool,
660    /// Overflowing content scrolls (implies clipping). Offsets are retained
661    /// across frames in the core, keyed by this node's `Key`.
662    pub scroll_x: bool,
663    pub scroll_y: bool,
664    /// Scroll anchoring (CSS's `overflow-anchor`): the
665    /// first child in view keeps its place on screen when the content
666    /// before it changes size — a chat that prepends history, a log that
667    /// inserts above the viewport, a row whose estimate was corrected.
668    /// On the scroll axis that is the container's main axis only.
669    pub anchor: bool,
670    /// Out-of-flow positioning; see [`FloatConfig`].
671    pub float: Option<FloatConfig>,
672}
673
674impl Default for LayoutSpec {
675    fn default() -> Self {
676        Self {
677            width: Sizing::Fit,
678            height: Sizing::Fit,
679            min_w: Min::AUTO,
680            max_w: f32::INFINITY,
681            min_h: Min::AUTO,
682            max_h: f32::INFINITY,
683            dir: Dir::Column,
684            padding: Edges::default(),
685            gap: 0.0,
686            wrap: false,
687            table: false,
688            cross_gap: 0.0,
689            main_align: Align::Start,
690            cross_align: Align::Start,
691            aspect: 0.0,
692            clip: false,
693            scroll_x: false,
694            scroll_y: false,
695            anchor: false,
696            float: None,
697        }
698    }
699}
700
701impl LayoutSpec {
702    /// Whether this node clips its children.
703    pub fn clips(&self) -> bool {
704        self.clip || self.scroll_x || self.scroll_y
705    }
706
707    /// Whether this node is a table: the flag, on a column. Every reader
708    /// asks this and not the field, so a `Row` with the field set is a row
709    /// everywhere.
710    pub fn is_table(&self) -> bool {
711        self.table && self.dir == Dir::Column
712    }
713}
714
715impl LayoutSpec {
716    /// The width a declared aspect ratio gives a `Fit` width: its `Fixed`
717    /// height times the ratio. `None` when the ratio has no say over the
718    /// width.
719    pub(crate) fn aspect_width(&self) -> Option<f32> {
720        match (self.width, self.height) {
721            (Sizing::Fit, Sizing::Fixed(h)) if self.aspect > 0.0 => {
722                Some(self.clamp_h(h) * self.aspect)
723            }
724            _ => None,
725        }
726    }
727
728    /// Whether a declared aspect ratio sizes the height: a `Fit` height,
729    /// read off the final width. `aspect_width`'s pair.
730    pub(crate) fn aspect_height(&self) -> bool {
731        self.aspect > 0.0 && self.height == Sizing::Fit
732    }
733
734    pub(crate) fn clamp_w(&self, w: f32) -> f32 {
735        let min = self.min_w.resolved();
736        w.clamp(min, self.max_w_px().max(min))
737    }
738
739    pub(crate) fn clamp_h(&self, h: f32) -> f32 {
740        let min = self.min_h.resolved();
741        h.clamp(min, self.max_h_px().max(min))
742    }
743
744    /// `max_w` as px: a calc layout has not resolved yet is no ceiling.
745    pub fn max_w_px(&self) -> f32 {
746        if self.max_w < 0.0 {
747            f32::INFINITY
748        } else {
749            self.max_w
750        }
751    }
752
753    /// `max_h` as px, as [`Self::max_w_px`].
754    pub fn max_h_px(&self) -> f32 {
755        if self.max_h < 0.0 {
756            f32::INFINITY
757        } else {
758            self.max_h
759        }
760    }
761}
762
763/// The paint half of a [`NodeSpec`]: background, border, corner radii,
764/// opacity, shadow and pixel snapping.
765#[derive(Clone, Copy, Debug, PartialEq)]
766pub struct VisualStyle {
767    pub bg: Color,
768    pub border_color: Color,
769    pub border_w: f32,
770    /// Corner radii (logical px), clockwise from the top-left:
771    /// `[tl, tr, br, bl]`. `NodeSpec::radius` sets all four; the per-corner
772    /// builders (`radius_tl`, ...) override one — later calls win, like CSS
773    /// `border-radius` followed by `border-top-left-radius`.
774    pub radius: [f32; 4],
775    /// Group opacity, 0..=1 and 1 by default: multiplied into the alpha of
776    /// every quad this node and its subtree emit, and into every
777    /// descendant's own opacity. This is the *cheap* group opacity — a
778    /// per-quad alpha multiply, not an offscreen composite — so a subtree
779    /// whose own pieces overlap shows its seams through the fade where a
780    /// compositing implementation would not. Nothing else changes: an
781    /// invisible subtree still lays out, still takes clicks and is still
782    /// read by assistive technology, exactly like CSS `opacity: 0`.
783    pub opacity: f32,
784    /// The drop shadow cast behind this node (see [`Shadow`]).
785    pub shadow: Shadow,
786    /// Paint the background, border, shadow and fragment with each edge on
787    /// a whole physical pixel (`pixelSnap`). Off by default: a box is drawn where
788    /// layout put it, so a gap between two boxes is there at any scale.
789    /// On, its edges land where a text's backgrounds' do and where every
790    /// other snapped box's do, so snapped boxes that share an edge in
791    /// layout, and a snapped box beside a text's background, meet without
792    /// a seam. Layout, hit-testing and the children are untouched.
793    pub pixel_snap: bool,
794}
795
796impl Default for VisualStyle {
797    fn default() -> Self {
798        Self {
799            bg: Color::TRANSPARENT,
800            border_color: Color::TRANSPARENT,
801            border_w: 0.0,
802            radius: [0.0; 4],
803            opacity: 1.0,
804            shadow: Shadow::default(),
805            pixel_snap: false,
806        }
807    }
808}
809
810/// One outer drop shadow: the node's rounded rect, moved by `dx`/`dy`,
811/// grown by `spread` and its edge blurred over `blur`, painted in `color`
812/// behind the node. CSS's `box-shadow` without the inset and multi-shadow
813/// forms.
814///
815/// It draws whenever `color` is visible, so `shadowColor` alone is a hard
816/// shadow sitting exactly behind the node. The shape is not knocked out of
817/// the middle the way CSS knocks it out, so a translucent background shows
818/// the shadow through itself.
819#[derive(Clone, Copy, Debug, Default, PartialEq)]
820pub struct Shadow {
821    pub color: Color,
822    /// Offset (logical px); positive `dy` casts downward.
823    pub dx: f32,
824    pub dy: f32,
825    /// Blur radius (logical px): the edge ramps over this distance and the
826    /// shadow reaches this far past its shape. 0 = a hard edge.
827    pub blur: f32,
828    /// Grows (or, negative, shrinks) the shape before blurring.
829    pub spread: f32,
830}
831
832impl Shadow {
833    /// Whether anything would be painted.
834    pub fn is_visible(&self) -> bool {
835        self.color.is_visible()
836    }
837}
838
839/// Corner indices into `VisualStyle::radius` / `Quad::radius`.
840pub mod corner {
841    pub const TL: usize = 0;
842    pub const TR: usize = 1;
843    pub const BR: usize = 2;
844    pub const BL: usize = 3;
845}
846
847/// Everything one node declares. This, not any Rust trait, is the contract
848/// every frontend (Rust builders, Lua, JSX, C) lowers into. Start from
849/// [`NodeSpec::row`], [`NodeSpec::column`] or [`NodeSpec::table`] and
850/// chain builders; see the [module docs](self) for an example.
851#[derive(Clone, Debug, Default)]
852pub struct NodeSpec {
853    pub layout: LayoutSpec,
854    pub style: VisualStyle,
855    /// Track pointer hover for this node (`Ui::is_hovered`) without making
856    /// it clickable — tooltips on passive badges. Nodes with `on_click` /
857    /// `on_key` / `window` are always hover-tracked; clicks on a merely
858    /// hoverable node emit nothing.
859    pub hoverable: bool,
860    /// Ask the driver for another frame after this one, every frame this
861    /// node is declared (`animate`). What a `fragment` reading `time`
862    /// needs; opt-in, because it takes the loop off input-driven.
863    pub animate: bool,
864    /// Paint this node's background in the theme's accent: the OS's where
865    /// the host reported one, the app's where it pinned one, kui's blue
866    /// otherwise. A view says *that* this node is the accented one and the
867    /// palette says which colour that is. The stock button also repaints
868    /// its hover, pressed shade and label from the same accent.
869    pub accent: bool,
870    /// Window-chrome role (drag handle / window button). A chrome node's
871    /// interactions become `WindowCommand`s for the frame driver instead of
872    /// `UiEvent`s; `on_click` is ignored on such nodes.
873    pub window: Option<WindowRole>,
874    /// Eases this node's sizing amounts, colors and radius toward what the
875    /// view declares instead of snapping (see [`crate::anim`]). Keyed by
876    /// node identity, so the node needs a stable key across frames.
877    pub transition: Option<Transition>,
878    /// With `transition`: also ease this node's laid-out *position*, moving
879    /// its whole subtree — reordered siblings slide into their new slots.
880    /// Opt-in because a node whose position follows an already-easing
881    /// sibling (a split's second half) would lag twice.
882    pub slide: bool,
883    /// Paint this node and its subtree after its siblings, over any of
884    /// them it overlaps: a tab that glides past its neighbour (`slide`)
885    /// passes over it whichever way it moved, the active tab of a strip
886    /// whose tabs overlap stays on top. Its parent still paints under
887    /// it and whatever follows the parent still paints over it — the
888    /// sibling order is all it changes, and the hit test reads the same
889    /// order, so where it overlaps a sibling it takes the press. Two
890    /// such siblings paint in tree order among themselves. Not a layer:
891    /// a float still opens above it, and it keeps its parent's clip.
892    pub over_siblings: bool,
893    /// Reachable by Tab, and focused by a click or an assistive-technology
894    /// request, without a click payload or a control role — a list row
895    /// that opens on Enter, a card. Controls (editors, key sinks,
896    /// `on_click` boxes, the control roles) are focusable already.
897    pub focusable: bool,
898    /// Where focus lands when the `modal` scope containing this node is
899    /// entered: the first node in the modal's Tab ring declaring it,
900    /// instead of simply the ring's first — a destructive confirm opening
901    /// on its Cancel rather than on whichever control is declared first.
902    /// Read on entry only, so a Tab press afterwards stands; a node the
903    /// ring skips (disabled, decoration, not focusable) is not a
904    /// candidate, and with no candidate the entry is the ring's first
905    /// node as before.
906    pub initial_focus: bool,
907    /// Inert: keeps its hit region (so a tooltip can say why) and loses
908    /// everything else — no click, drag or key sink, no hover / pressed /
909    /// focus background, no place in the Tab ring; the access tree
910    /// reports it disabled.
911    pub disabled: bool,
912    /// The pointer shape over this node (see [`crate::cursor`]). Unset,
913    /// the pointer is the I-beam over an editor or a selection scope and
914    /// the plain arrow over everything else — a clickable or draggable
915    /// node included — so a hand over a button, a grab over a handle
916    /// (`grabbing` while its drag runs), a splitter's resize arrows and a
917    /// disabled control's `notAllowed` are all the view's to declare. The
918    /// stock button declares `Pointer` itself.
919    pub cursor: Option<CursorShape>,
920
921    /// See [`EventSpec`]. `None` when the node declares none of it.
922    pub events: Option<Box<EventSpec>>,
923
924    /// See [`AnimSpec`]. `None` when the node declares none of it.
925    pub anim: Option<Box<AnimSpec>>,
926
927    /// See [`AccessSpec`]. `None` when the node declares none of it.
928    pub access: Option<Box<AccessSpec>>,
929
930    /// See [`InteractSpec`]. `None` when the node declares none of it.
931    pub interact: Option<Box<InteractSpec>>,
932}
933
934/// Event payloads a node declares. Boxed on `NodeSpec` because most
935/// nodes declare none.
936#[derive(Clone, Debug, Default, PartialEq)]
937pub struct EventSpec {
938    /// Payload emitted as a `UiEvent` when this node is clicked.
939    pub on_click: Option<Value>,
940    /// Makes this node draggable: pressing it starts a pointer-captured
941    /// drag, and cursor motion until release emits `UiEvent`s of the form
942    /// `{kind="drag", phase="start"|"move"|"end", x, y, dx, dy, tag}` with
943    /// this payload merged in under `tag`. A drag past the click slop
944    /// suppresses the node's `on_click`, so both can coexist.
945    pub on_drag: Option<Value>,
946    /// Marks this node as a key sink: while it holds key focus, key
947    /// presses arrive as `UiEvent`s on it as `{kind="key", phase="down",
948    /// code, ...}`, with this payload merged in under `tag`. Clicking the
949    /// node takes key focus. Releases are not delivered unless the sink
950    /// also declares [`key_up`](Self::key_up): a keymap is the common
951    /// case, and a keymap that heard both halves would run every binding
952    /// twice.
953    pub on_key: Option<Value>,
954    /// With `on_key`: the sink hears releases too, as the same payload
955    /// with `phase="up"` (`text` null, `repeat` false). For a held-key
956    /// interaction — WASD, press-and-hold to preview, a key that arms a
957    /// mode while it is down. A key only comes up where it went down: a
958    /// release whose press the sink never got is dropped, and focus
959    /// leaving while a key is held delivers the `up` first, so nothing is
960    /// left stuck down. Without it a sink hears presses only, which is
961    /// what a keymap wants.
962    pub key_up: bool,
963    /// With `on_key`: the modifier and lock keys themselves arrive too
964    /// (Shift, Ctrl, Alt, Super, Caps Lock, Num Lock, Scroll Lock), as
965    /// codes of their own with their side in `location`.
966    /// Without it a modifier is only ever held — in the next key's
967    /// `shift` / `ctrl` / … and the `modifiers` event — so a keymap
968    /// mid-sequence does not read Shift as a key between two others. A
969    /// terminal speaking kitty's keyboard protocol asks, and so does a
970    /// game that binds a lone Shift.
971    pub modifier_keys: bool,
972    /// Asks for a context menu: a secondary-button press over this node
973    /// emits `{kind="contextmenu", x, y, tag}` on it with this payload
974    /// under `tag`, and does nothing else — the press moves no focus,
975    /// places no caret and produces no click, so right-clicking a
976    /// selection leaves it selected. `x`/`y` are the press in logical
977    /// viewport coordinates, which is where the menu goes. Asked of the
978    /// topmost node under the pointer; when that node offers no menu the
979    /// press reaches the nearest enclosing node that does, the way an
980    /// unclaimed key reaches the enclosing sink:
981    /// the event carries the owner's key and tag, a nested declaration
982    /// wins over its ancestor's, a disabled node's own is skipped, and
983    /// the walk stops at the modal boundary. Null = the behaviour without
984    /// a tag.
985    pub on_context_menu: Option<Value>,
986    /// Force-click events: a press that deepened past the second stage of
987    /// a Force Touch trackpad over this node emits `{kind="forceclick",
988    /// x, y, tag}` with this payload under `tag`. Routed as a secondary
989    /// press is — no focus moved, no caret, no click — but asked of the
990    /// topmost node only, with no walk to an enclosing declaration — and
991    /// the ordinary click the press produces still follows,
992    /// which is what macOS does.
993    ///
994    /// Text does not need this: a force click over an editor or a
995    /// `selectable` scope selects the word and asks the host to look it
996    /// up, which is what the gesture means on that platform. This is for
997    /// what the core cannot guess — a force click on a chart, a map, a
998    /// timeline.
999    pub on_force_click: Option<Value>,
1000    /// The non-primary buttons as events: a press of a
1001    /// button in [`buttons`](Self::buttons) over this node emits
1002    /// `{kind="button", phase="press", button, x, y, clicks, tag}` on it
1003    /// with this payload under `tag`, and the button is then captured by
1004    /// the node — every pointer move while it is held arrives as
1005    /// `phase="move"` and its release as `phase="release"`, on this node
1006    /// wherever the pointer is. `button` is `"secondary"`, `"middle"` or,
1007    /// for a button past those, its [`crate::MouseButton::code`]; `x`/`y`
1008    /// are logical viewport coordinates, and on a `cells` grid the events
1009    /// carry `cell: {row, col}` as a click does. Several buttons may be
1010    /// held at once, each its own capture; a primary drag is untouched.
1011    ///
1012    /// Asked of the topmost node under the pointer, and when that node
1013    /// claims no such button the press reaches the nearest enclosing node
1014    /// that does, as a context menu's does: a disabled node's own is
1015    /// skipped and the walk stops at the modal boundary. A claimed
1016    /// secondary press is this event *instead of* a `contextmenu` event
1017    /// and the stock menu — a nearer `on_context_menu` still wins, being
1018    /// the nested declaration. Like every non-primary press it moves no
1019    /// focus, places no caret and touches no selection or scrollbar. What
1020    /// it is for: a terminal's middle-click paste, and the mouse reports a
1021    /// program in it asked for. Null = the behaviour without a tag.
1022    pub on_button: Option<Value>,
1023    /// Which non-primary buttons [`on_button`](Self::on_button) claims:
1024    /// all three kinds unless the node says otherwise. A pane that wants
1025    /// the middle button and leaves the secondary one to its context menu
1026    /// says [`Buttons::MIDDLE`].
1027    pub buttons: Buttons,
1028    /// Scroll events: the wheel over this node emits `{kind="scroll", x,
1029    /// y, dx, dy, lines, tag}` with this payload under `tag` — the delta
1030    /// in logical px as the driver reported it (positive `dy` is the wheel
1031    /// rolling up, toward earlier content), the pointer's position, and
1032    /// on a `cells` grid the whole lines the delta covers (`null` on any
1033    /// other node), the fraction carried to the next notch. The node
1034    /// *takes* the wheel on the axes [`scroll_axes`](Self::scroll_axes)
1035    /// names: a scroll gesture that starts over it is its own, and stays
1036    /// its own until it ends wherever the pointer goes, except on an axis
1037    /// it also scrolls as a container (`scroll_x`, `scroll_y`, the offset
1038    /// the app sets from what it hears), where it is answered by its room
1039    /// as a container is: at its edge, a gesture that way passes to the
1040    /// scroller around it; it
1041    /// reaches no scroll container above it, and a container inside it
1042    /// still takes the axes it scrolls while it can move that way,
1043    /// passing this node the rest: the other axis, and a gesture that
1044    /// begins with the container at its limit (unless it says
1045    /// [`Overscroll::Contain`]). The core moves nothing — a grid's
1046    /// `origin_line` and a canvas's zoom are the app's to change. A
1047    /// drag-select held past a grid's top or bottom edge arrives here
1048    /// too, as the lines the frame scrolled by.
1049    pub on_scroll: Option<Value>,
1050    /// Which axes [`on_scroll`](Self::on_scroll) takes: both unless the
1051    /// node says otherwise. A scroll gesture on an axis
1052    /// the node does not take passes it by, to the scroller around it —
1053    /// a terminal that scrolls its history on `y` says
1054    /// [`ScrollAxes::Y`], and a sideways swipe that meets it goes on
1055    /// moving the strip it sits in. Meaningless without `on_scroll`.
1056    pub scroll_axes: ScrollAxes,
1057    /// The modifiers [`on_scroll`](Self::on_scroll) is for (backlog
1058    /// F122): with any named, the node hears only a scroll gesture that
1059    /// began with one of them held — and hears it *first*, wherever in
1060    /// the tree under the pointer the gesture began: ahead of every
1061    /// scroll container and every handler that names none, the innermost
1062    /// such node winning. A wheel with none of them held passes it by as
1063    /// if it declared no `on_scroll` — a node that is a scroll container
1064    /// too scrolls for it as any other does. What a Ctrl-wheel zoom is declared
1065    /// with, on the window's root or on the one canvas it zooms: a
1066    /// modified wheel is the more specific ask, and no list under the
1067    /// pointer scrolls for it. Its events carry `mods`, the modifiers
1068    /// held when the gesture began — the gesture stays this node's to
1069    /// its end, glide included, whatever is let go meanwhile. None named
1070    /// (the default): a handler like any other. Meaningless without
1071    /// `on_scroll`.
1072    pub scroll_mods: crate::input::KeyMods,
1073    /// Hover events: the pointer entering or leaving this node emits
1074    /// `{kind="hover", phase="enter"|"leave", tag}` with this payload under
1075    /// `tag` — for hover-dependent *layout* (a close button that appears)
1076    /// where a color swap isn't enough. Implies hover tracking.
1077    pub on_hover: Option<Value>,
1078    /// Drop-zone events: files dragged in from the OS over this node emit
1079    /// `{kind="drop", phase="enter"|"move"|"leave"|"drop", paths, x, y,
1080    /// tag}` with this payload under `tag` — `paths` the OS paths as
1081    /// strings, `x`/`y` the pointer in viewport coordinates (absent on
1082    /// `leave`). The zone under the files is the topmost *zone* by paint
1083    /// order: a node inside a zone resolves to it, and a node that is no
1084    /// zone and has none enclosing it is looked past, so an overlay shown
1085    /// on `enter` cannot make the zone lose the files. No `leave` follows
1086    /// a `drop`. Implies hover tracking.
1087    pub on_drop: Option<Value>,
1088    /// Layout events: the rect layout gave this node arrives as
1089    /// `{kind="layout", x, y, w, h, parent: {x, y, w, h}, tag}` (logical px,
1090    /// viewport coordinates, after scrolling and position easing) — on the
1091    /// node's first frame and again whenever the rect changes, never on a
1092    /// frame that left it alone. The view reads the numbers layout already
1093    /// produced instead of re-deriving them; a transition that moves the
1094    /// node reports every frame it moves. Needs a stable key across frames.
1095    pub on_layout: Option<Value>,
1096    /// Slider changes: on a node whose role is `Slider`, the core turns a
1097    /// press into the value under the pointer, a drag into the value under
1098    /// it, the arrows and assistive technology's Increment / Decrement
1099    /// into one `value_step`, PageUp / PageDown into ten, Home / End into
1100    /// the range's ends — clamped to `value_min..value_max`, snapped to the
1101    /// step — and emits `{kind="change", value, phase="move"|"end", tag}`
1102    /// with this payload under `tag`. The value is proposed, never
1103    /// applied: nothing moves until the view declares it as `value_now`.
1104    /// Without it a slider's keys reach the app as the `access` nudge.
1105    /// Ignored on any other role.
1106    pub on_change: Option<Value>,
1107    /// Modal: while this node is declared, the Tab ring is its subtree,
1108    /// everything outside it is inert to the pointer, the wheel and
1109    /// assistive technology, and Escape or a press outside emits
1110    /// `{kind="dismiss", reason, tag}` on it with this payload under
1111    /// `tag`. The last node declaring it in tree order is the one in
1112    /// effect (a confirm inside a dialog). Null = modal without a tag.
1113    pub modal: Option<Value>,
1114    /// A press on this node, or anywhere inside it, leaves keyboard focus
1115    /// where it was (`keepFocus`): a toolbar button, a tab, a divider
1116    /// that acts without taking the keyboard from the editor beside it.
1117    /// Its click, drag and hover are unchanged, and Tab
1118    /// and assistive technology still reach a focusable node in it — it
1119    /// is the pointer's press alone that stops moving focus.
1120    pub keep_focus: bool,
1121    /// Keyboard focus entering or leaving this node's subtree — the node
1122    /// itself or anything focused inside it — emits `{kind="focus",
1123    /// phase="in"|"out", by, tag}` with this payload under `tag`, where
1124    /// `by` is `"pointer"`, `"keyboard"`, `"assistive"` or `"program"`:
1125    /// what moved it. Reported once the move has settled —
1126    /// after the input that made it, or at the end of the frame that
1127    /// declared it — so a view reads a change instead of diffing
1128    /// `key_focus` every frame. Declares nothing interactive.
1129    pub on_focus: Option<Value>,
1130}
1131
1132impl EventSpec {
1133    /// The group as a node that declares none of it — what
1134    /// `NodeSpec`'s accessor hands back when the box is `None`.
1135    pub const EMPTY: Self = Self {
1136        on_click: None,
1137        on_drag: None,
1138        on_key: None,
1139        key_up: false,
1140        modifier_keys: false,
1141        on_context_menu: None,
1142        on_force_click: None,
1143        on_button: None,
1144        buttons: Buttons::ALL,
1145        on_scroll: None,
1146        scroll_axes: ScrollAxes::Both,
1147        scroll_mods: crate::input::KeyMods::NONE,
1148        on_hover: None,
1149        on_drop: None,
1150        on_layout: None,
1151        on_change: None,
1152        modal: None,
1153        keep_focus: false,
1154        on_focus: None,
1155    };
1156}
1157
1158/// Per-node animation declarations. Boxed on `NodeSpec`: `enter` and
1159/// `exit` are 60 bytes each and almost every node has neither.
1160#[derive(Clone, Debug, Default, PartialEq)]
1161pub struct AnimSpec {
1162    /// CSS-style stops for the animatable slots (see [`crate::keyframes`]):
1163    /// with a `transition`, the slots a stop names cycle through the stops
1164    /// over the transition's duration, in its `repeat` direction, offset by
1165    /// its `delay_ms` — forever, and without the view redrawing. Slots no
1166    /// stop names still tween toward what the view declares. Empty = none.
1167    pub keyframes: Vec<Keyframe>,
1168    /// Where this node's slots start the first frame it is seen (see
1169    /// [`crate::enter`]): with a `transition`, the slots it names ease in
1170    /// from there instead of snapping — `dx`/`dy` slide the node in from
1171    /// that far away, `bg` fades it in. A node that vanishes and returns
1172    /// enters again. None = first sight snaps.
1173    pub enter: Option<Enter>,
1174    /// Where this node's slots *end* the frame after the view stops
1175    /// declaring it (see [`crate::depart`]). An exit is an [`Enter`] read
1176    /// the other way: with a `transition`, the departing subtree is copied
1177    /// out of the last frame that had it and replayed — frozen where
1178    /// layout left it, on top, inert — while the slots this names ease
1179    /// from where they were toward what it declares. Needs a stable key
1180    /// across frames, and a `transition` with a duration; without both, a
1181    /// removed node vanishes at once as it always did.
1182    pub exit: Option<Enter>,
1183    /// How many times the `keyframes` cycle runs (CSS's
1184    /// `animation-iteration-count`, backlog F133); `None`, the default, is
1185    /// for ever. A finite cycle plays from the first frame its node is
1186    /// declared with it, holds its first stop through its `delay`, and
1187    /// rests where its last iteration ended once it is over, owing no
1188    /// frame (CSS's `animation-fill-mode: both`). A fraction of an
1189    /// iteration ends partway through one.
1190    pub iterations: Option<f32>,
1191}
1192
1193impl AnimSpec {
1194    /// The group as a node that declares none of it — what
1195    /// `NodeSpec`'s accessor hands back when the box is `None`.
1196    pub const EMPTY: Self = Self {
1197        keyframes: Vec::new(),
1198        enter: None,
1199        exit: None,
1200        iterations: None,
1201    };
1202}
1203
1204/// Accessibility properties a view states outright, as opposed to the
1205/// ones the core derives (see [`crate::access`]). Boxed on `NodeSpec`:
1206/// read only while an access tree is being built, and unset on nearly
1207/// every node.
1208#[derive(Clone, Debug, Default, PartialEq)]
1209pub struct AccessSpec {
1210    /// What this node is to assistive technology (see [`crate::access`]).
1211    /// Unset, the core derives one: a node with `on_click` is a button,
1212    /// an editor a text input, a scrolling container a scroll view, and
1213    /// a plain box is structure that leaves no trace. `Role::None` hides
1214    /// the node and its subtree from the access tree (decoration).
1215    pub role: Option<Role>,
1216    /// The accessible name. Without one a button, link, tab or heading is
1217    /// named by the text inside it; an image or an icon button has no
1218    /// name at all, and the core says so (`control-without-name`,
1219    /// `image-without-label` warnings).
1220    pub label: Option<Label>,
1221    /// The accessible description, read after the name — what the
1222    /// `description` prop sets, and what the `tooltip` prop sets on the way
1223    /// to drawing the same string.
1224    pub description: Option<Label>,
1225    /// For checkbox / radio / switch roles: the on state.
1226    pub checked: bool,
1227    /// For a checkbox: neither on nor off, like the select-all box over a
1228    /// list some of whose rows are selected. Wins over `checked`, which it
1229    /// leaves as it was.
1230    pub mixed: bool,
1231    /// The current one of a set: a `Role::Tab`, a picked `Role::ListItem`,
1232    /// the `Role::Link` for the page you are on. A tab reports the state
1233    /// either way; a row or a link reports it only where it is set (see
1234    /// [`crate::access`]).
1235    pub selected: bool,
1236    /// For a node that shows and hides something: expanded, or collapsed.
1237    /// None = it does not expand, and says nothing about it.
1238    pub expanded: Option<bool>,
1239    /// For a slider role: the current value and its range, so assistive
1240    /// technology can read the position (the drawing stays the view's).
1241    pub value_now: Option<f32>,
1242    pub value_min: Option<f32>,
1243    pub value_max: Option<f32>,
1244    /// For a slider role: what the position *reads as* (ARIA's
1245    /// `aria-valuetext`). Without one a reader has only the three numbers
1246    /// above and says a percentage — 25 in [5..60] is "36 percent" — so a
1247    /// value whose unit matters says it here: "25 minutes". It replaces
1248    /// the number rather than joining it (see [`crate::access`]), and a
1249    /// nudge announces the new text, not the new number.
1250    pub value_text: Option<Label>,
1251    /// For a slider role: how far one arrow key moves it, and the grid a
1252    /// value set by the pointer snaps to. None = a hundredth of the range.
1253    pub value_step: Option<f32>,
1254    /// On a `Role::Line` of a custom editor: the caret's byte offset into
1255    /// the line's text, and the byte offset of the selection's other end
1256    /// (see [`crate::access`]).
1257    pub caret: Option<u32>,
1258    pub selection_anchor: Option<u32>,
1259    /// With `caret`: the caret this line declares does not blink — a
1260    /// block caret in a modal editor's normal mode — so a driver's blink
1261    /// clock is not armed on it (`Core::has_caret` leaves it out) while
1262    /// the offset still anchors the IME and reads to assistive
1263    /// technology. Without it a declared caret is a caret to blink.
1264    pub caret_solid: bool,
1265    /// Float `description` below the node while it is hovered — the
1266    /// `tooltip` prop's third effect, set by [`NodeSpec::tooltip`]. The core
1267    /// reads it as the node opens (`Core::hint`); a binding that lowers the
1268    /// prop onto a box floats the hint itself and leaves this off. A leaf
1269    /// reads it as it is pushed and floats the hint beside itself, anchored
1270    /// to it, since it holds no children for the hint to be the last of —
1271    /// which is how a binding asks it of a leaf (`PropsOut::for_leaf`,
1272    /// backlog RG113).
1273    pub tooltip: bool,
1274    /// When the text inside this node changes, a reader reads the change
1275    /// without being asked (ARIA's `aria-live`). Off by default; a node
1276    /// that declares it is semantic, so a plain box marked live is not
1277    /// elided.
1278    pub live: Live,
1279}
1280
1281impl AccessSpec {
1282    /// The group as a node that declares none of it — what
1283    /// `NodeSpec`'s accessor hands back when the box is `None`.
1284    pub const EMPTY: Self = Self {
1285        role: None,
1286        label: None,
1287        description: None,
1288        checked: false,
1289        mixed: false,
1290        selected: false,
1291        expanded: None,
1292        value_now: None,
1293        value_min: None,
1294        value_max: None,
1295        value_text: None,
1296        value_step: None,
1297        caret: None,
1298        selection_anchor: None,
1299        caret_solid: false,
1300        tooltip: false,
1301        live: Live::Off,
1302    };
1303}
1304
1305/// Hover / pressed / focus styling and the sounds that go with them.
1306/// Boxed on `NodeSpec` so the common node — which declares none of it —
1307/// costs one null check in `resolve_hover_style` instead of reading
1308/// several `Option`s spread across the struct.
1309#[derive(Clone, Debug, Default, PartialEq)]
1310pub struct InteractSpec {
1311    /// Background while the pointer hovers this node (or any node sharing
1312    /// its `hover_group`). Resolved by the core when the node opens, so a
1313    /// data-only view gets hover styling without querying `is_hovered` —
1314    /// and with `transition` the swap eases. Implies hover tracking.
1315    pub hover_bg: Option<Color>,
1316    /// Background while this node (or its group) is pressed. Implies hover
1317    /// tracking. Without a `hover_bg`, hover keeps the plain `bg`. An
1318    /// `on_drag` node holds this state for the whole captured drag, even
1319    /// while the cursor is off it.
1320    pub pressed_bg: Option<Color>,
1321    /// Hover group: nodes sharing an id count as one for `hover_bg` /
1322    /// `pressed_bg` — a two-piece elbow, a split button, a row whose cells
1323    /// highlight together. The id is a hash of a name (`hover_group`).
1324    /// Implies hover tracking.
1325    pub hover_group: Option<u64>,
1326    /// A registered sound played when this node is clicked (see
1327    /// [`crate::audio`]); the click itself still emits `on_click` if one is
1328    /// declared. Implies hover tracking.
1329    pub click_sound: Option<crate::resources::SoundId>,
1330    /// A registered sound played when the pointer enters this node.
1331    /// Implies hover tracking.
1332    pub hover_sound: Option<crate::resources::SoundId>,
1333    /// Background while this node holds keyboard-visible focus (focus
1334    /// moved by Tab or by assistive technology, not by a click).
1335    /// Declaring one replaces the ring the core draws by default. Pressed
1336    /// wins over focus wins over hover; eases with `transition`.
1337    pub focus_bg: Option<Color>,
1338    /// Background while files dragged in from the OS are over this node.
1339    /// Wins over pressed, focus and hover — a press
1340    /// cannot be held while the OS holds a drag — and eases with
1341    /// `transition`; clears when the files leave, land or the drag is
1342    /// cancelled. Implies hover tracking.
1343    pub drop_bg: Option<Color>,
1344    /// Makes this node a *selection scope*: the text of every node inside
1345    /// it is one selectable run of text, in tree order, and a press-drag
1346    /// inside it selects across all of them. Declared on the container
1347    /// rather than on each label, because what a reader selects is a
1348    /// paragraph or a card, not one run of it.
1349    ///
1350    /// Scopes do not nest: the innermost one containing a run owns it, and
1351    /// an outer one is warned about (`nested-selection-scope`). An `edit`
1352    /// is already its own scope and ignores this.
1353    pub selectable: bool,
1354    /// Makes this node's subtree a *focus region*: a Tab ring of its own
1355    /// that the ring outside never enters, and that never leaves — a
1356    /// devtools dock, an inspector beside the app. Entered on purpose:
1357    /// `Ui::focus_region`, a press inside it, or an explicit focus on a
1358    /// node in it. Nothing else about the node changes — it lays out,
1359    /// paints, takes the pointer and appears in the access tree as before,
1360    /// and keys bubble through it to the sink above.
1361    pub focus_region: bool,
1362    /// How this node's scrollbars look, when it scrolls (see
1363    /// [`Scrollbar`]). Cold: read once per scroller when its layer's
1364    /// chrome is emitted, which is why it lives in this box rather than
1365    /// beside `scroll_y` on every node.
1366    pub scrollbar: Scrollbar,
1367    /// Whether a scroll gesture that starts over this scroller while it
1368    /// is at its limit that way goes on to the scroller around it (see
1369    /// [`Overscroll`]). Cold like `scrollbar`: read once
1370    /// per scroller a gesture starts over.
1371    pub overscroll: Overscroll,
1372    /// On a table: lines of this colour between its columns and between
1373    /// its rows, drawn with the table's own box, under its cells, down the
1374    /// middle of each gap, so a table with a `gap` of at least `rule_w`
1375    /// gets a grid with no rule cells. The
1376    /// columns come from the row with the most cells; the lines run the
1377    /// table's content box. Ignored on anything that is not a table.
1378    pub rules: Option<Color>,
1379    /// The rules' width in logical px; 0 is 1.
1380    pub rule_w: f32,
1381    /// A gradient painted over the node's `bg` and under its border and
1382    /// its children (`docs/adr/0042-a-gradient-is-an-image-the-core-paints.md`):
1383    /// one image quad from the atlas, rounded by the node's radius. It
1384    /// does not tween, and the state backgrounds replace `bg`, not it.
1385    pub gradient: Option<crate::gradient::Gradient>,
1386    /// Blur what the app drew beneath this node, inside its rounded rect,
1387    /// by this radius in logical px — CSS's `backdrop-filter: blur()`
1388    /// (backlog F129). The blurred pixels are what was painted before the
1389    /// node (earlier siblings, ancestors, the window's backdrop), never
1390    /// the node's own `bg` or children, which paint over them; give it a
1391    /// translucent `bg` for frosted glass. Clipped as the node is; faded
1392    /// by its group opacity. 0 is none. A renderer with no way to read
1393    /// back its frame (the CPU raster) draws the node unblurred.
1394    pub backdrop_blur: f32,
1395    /// A turn and a uniform scale about a pivot, applied to everything
1396    /// the node and its subtree draw, after layout (ADR 0043): `None`
1397    /// when the node declares none of `rotate`, `scale` and `pivot`.
1398    /// Boxed, as the group is: inline, its twenty bytes moved every node
1399    /// that declares any interaction — a gradient, a hover — into a larger
1400    /// allocation class (`frame_10k_rects_with_gradient` +2%).
1401    pub transform: Option<Box<TransformSpec>>,
1402}
1403
1404/// A node's `rotate`, `scale` and `pivot` (ADR 0043): paint-only, so the
1405/// node takes the room its upright self takes and everything after layout
1406/// — its quads, its clip, its hit region, its access rect — is drawn, cut,
1407/// hit and read through the turn. Nests by composition; tweens with
1408/// `transition` as one slot; an entrance, an exit and a keyframe stop name
1409/// `rotate` and `scale`.
1410#[derive(Clone, Copy, Debug, PartialEq)]
1411pub struct TransformSpec {
1412    /// Turns, clockwise with y down; 0 is none.
1413    pub rotate: f32,
1414    /// The uniform factor; 1 is none.
1415    pub scale: f32,
1416    /// The point the turn and the scale are about, as fractions of the
1417    /// node's box: `(0.5, 0.5)` is the centre.
1418    pub pivot: crate::geom::Vec2,
1419}
1420
1421impl Default for TransformSpec {
1422    fn default() -> Self {
1423        Self::NONE
1424    }
1425}
1426
1427impl TransformSpec {
1428    /// No turn and no scale, about the centre.
1429    pub const NONE: Self = Self {
1430        rotate: 0.0,
1431        scale: 1.0,
1432        pivot: crate::geom::Vec2 { x: 0.5, y: 0.5 },
1433    };
1434
1435    /// Whether this moves anything: a turn, or a scale that is not 1. A
1436    /// turn or a scale that is not a finite number is none.
1437    #[inline]
1438    pub fn active(&self) -> bool {
1439        (self.rotate != 0.0 && self.rotate.is_finite())
1440            || (self.scale != 1.0 && self.scale.is_finite())
1441    }
1442
1443    /// The transform for a node laid out at `rect` (logical px, in the
1444    /// space its parent paints in).
1445    pub fn at(&self, rect: crate::geom::Rect) -> crate::geom::Transform {
1446        let pivot = crate::geom::Vec2::new(
1447            rect.x + rect.w * self.pivot.x,
1448            rect.y + rect.h * self.pivot.y,
1449        );
1450        crate::geom::Transform::about(
1451            pivot,
1452            crate::geom::finite_or(self.rotate, 0.0),
1453            crate::geom::finite_or(self.scale, 1.0),
1454        )
1455    }
1456}
1457
1458/// A scrolling node's bars, per node. Every field's default is the stock
1459/// bar — the theme's `scrollbar` / `scrollbar_active` colours, 4 px at
1460/// rest and 6 px under the pointer, always drawn while the content
1461/// overflows — so a binding that sets none of the four rows gets exactly
1462/// what it always had. The bars are overlays and take no layout space
1463/// whatever their width; the grabbable track is at least as wide as the
1464/// active thumb plus its inset.
1465#[derive(Clone, Copy, Debug, Default, PartialEq)]
1466pub struct Scrollbar {
1467    /// Whether the bars are drawn at all, and when (see [`ScrollbarMode`]).
1468    pub mode: ScrollbarMode,
1469    /// The thumb's width at rest, logical px; under the pointer or dragged
1470    /// it is 2 px wider. `None` is the stock 4.
1471    pub width: Option<f32>,
1472    /// The thumb at rest; `None` is `theme.scrollbar`.
1473    pub color: Option<Color>,
1474    /// The thumb under the pointer or dragged; `None` is
1475    /// `theme.scrollbar_active`.
1476    pub active_color: Option<Color>,
1477}
1478
1479impl Scrollbar {
1480    /// The stock bar: visible, 4 px, in the theme's colours.
1481    pub const DEFAULT: Self = Self {
1482        mode: ScrollbarMode::Visible,
1483        width: None,
1484        color: None,
1485        active_color: None,
1486    };
1487}
1488
1489/// When a scrolling node's bars are drawn. Spelled by the `scrollbar` row
1490/// (`crate::schema::SCROLLBARS`, in this order).
1491#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1492pub enum ScrollbarMode {
1493    /// The stock bar: drawn whenever the content overflows.
1494    #[default]
1495    Visible,
1496    /// No thumb is drawn and no track takes the press. The wheel, the
1497    /// keyboard, `reveal` and the caret still scroll the node — a list
1498    /// drawing its own indicator, or one whose bar would sit on a border.
1499    Hidden,
1500    /// The bar shows while the scroll state is changing — the offset or
1501    /// the content's extent moved since the last frame, the pointer is on
1502    /// its track, or a thumb is being dragged — and for a second after,
1503    /// then fades out over a quarter of one. A node first seen shows its
1504    /// bar the same second. What an overlay bar does on macOS. Needs the
1505    /// driver's clock (`Core::set_time`); without one it is `Visible`,
1506    /// since a fade with no clock could never end.
1507    Auto,
1508}
1509
1510impl ScrollbarMode {
1511    /// Every mode, in the `scrollbar` row's order: what a binding that
1512    /// spells modes as numbers indexes (C's `KUI_SCROLLBAR_*` are these
1513    /// plus one).
1514    pub const ALL: [ScrollbarMode; 3] = [
1515        ScrollbarMode::Visible,
1516        ScrollbarMode::Hidden,
1517        ScrollbarMode::Auto,
1518    ];
1519}
1520
1521/// What a scroll gesture that starts over a scroller already at its limit
1522/// does: CSS's `overscroll-behavior`, spelled by the `overscroll` row.
1523///
1524/// A gesture picks its target when it starts: the innermost scroller
1525/// under the pointer that can still move the way it goes. One at its
1526/// limit is passed by, to the scroller around it — *chaining* — and
1527/// `Contain` stops that: the gesture is this scroller's, and moves
1528/// nothing until it turns back. Only on the axes the node scrolls, so a
1529/// `scroll_y` list that contains still passes a sideways swipe to the
1530/// strip it sits in, where CSS would stop that too. Decided at
1531/// the start only: a gesture that reaches a limit midway stops there
1532/// whatever this says, as a browser's does.
1533#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1534pub enum Overscroll {
1535    /// The gesture goes on to the scroller around this one when this one
1536    /// is at its limit that way.
1537    #[default]
1538    Auto,
1539    /// A gesture that starts here stays here: a panel, a popup's list or
1540    /// a sheet whose scrolling must never move the page behind it.
1541    Contain,
1542}
1543
1544impl Overscroll {
1545    /// Every value, in the `overscroll` row's order (C's
1546    /// `KUI_OVERSCROLL_*` are these plus one).
1547    pub const ALL: [Overscroll; 2] = [Overscroll::Auto, Overscroll::Contain];
1548}
1549
1550/// Which axes an `on_scroll` node takes, spelled by the `scrollAxes` row.
1551#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1552pub enum ScrollAxes {
1553    /// Both: the node hears every scroll gesture that starts over it.
1554    #[default]
1555    Both,
1556    /// Only sideways; a vertical gesture passes it by.
1557    X,
1558    /// Only vertical; a sideways gesture passes it by — a terminal's
1559    /// history, a log.
1560    Y,
1561}
1562
1563impl ScrollAxes {
1564    /// Every value, in the `scrollAxes` row's order (C's
1565    /// `KUI_SCROLL_AXES_*` are these plus one).
1566    pub const ALL: [ScrollAxes; 3] = [ScrollAxes::Both, ScrollAxes::X, ScrollAxes::Y];
1567
1568    /// Whether the node takes `x` (true) or `y` (false).
1569    pub fn takes(self, x: bool) -> bool {
1570        match self {
1571            ScrollAxes::Both => true,
1572            ScrollAxes::X => x,
1573            ScrollAxes::Y => !x,
1574        }
1575    }
1576}
1577
1578impl InteractSpec {
1579    /// The group as a node that declares none of it — what
1580    /// `NodeSpec`'s accessor hands back when the box is `None`.
1581    pub const EMPTY: Self = Self {
1582        hover_bg: None,
1583        pressed_bg: None,
1584        hover_group: None,
1585        click_sound: None,
1586        hover_sound: None,
1587        focus_bg: None,
1588        drop_bg: None,
1589        selectable: false,
1590        focus_region: false,
1591        scrollbar: Scrollbar::DEFAULT,
1592        overscroll: Overscroll::Auto,
1593        rules: None,
1594        rule_w: 0.0,
1595        gradient: None,
1596        backdrop_blur: 0.0,
1597        transform: None,
1598    };
1599}
1600
1601impl NodeSpec {
1602    // -- Boxed groups ------------------------------------------------------
1603    // The four cold groups are behind a pointer each: a node that
1604    // declares none of a group pays 8 bytes for it rather than its full
1605    // width. Reads go through the `&` accessor, which hands back a shared
1606    // empty group instead of allocating, so a caller reads
1607    // `spec.events().on_click` exactly as it used to read `spec.on_click`.
1608    // Writes go through the `_mut` accessor, which allocates on first use.
1609
1610    /// Event payloads this node declares, empty if it declares none.
1611    #[inline]
1612    pub fn events(&self) -> &EventSpec {
1613        static EMPTY: EventSpec = EventSpec::EMPTY;
1614        self.events.as_deref().unwrap_or(&EMPTY)
1615    }
1616
1617    /// Allocates the group on first write.
1618    #[inline]
1619    pub fn events_mut(&mut self) -> &mut EventSpec {
1620        self.events.get_or_insert_with(Box::default)
1621    }
1622
1623    /// Animation declarations, empty if this node has none.
1624    #[inline]
1625    pub fn anim(&self) -> &AnimSpec {
1626        static EMPTY: AnimSpec = AnimSpec::EMPTY;
1627        self.anim.as_deref().unwrap_or(&EMPTY)
1628    }
1629
1630    /// Allocates the group on first write.
1631    #[inline]
1632    pub fn anim_mut(&mut self) -> &mut AnimSpec {
1633        self.anim.get_or_insert_with(Box::default)
1634    }
1635
1636    /// Declared accessibility properties, empty if this node declares none.
1637    #[inline]
1638    pub fn access(&self) -> &AccessSpec {
1639        static EMPTY: AccessSpec = AccessSpec::EMPTY;
1640        self.access.as_deref().unwrap_or(&EMPTY)
1641    }
1642
1643    /// Allocates the group on first write.
1644    #[inline]
1645    pub fn access_mut(&mut self) -> &mut AccessSpec {
1646        self.access.get_or_insert_with(Box::default)
1647    }
1648
1649    /// Hover / pressed / focus styling, empty if this node declares none.
1650    #[inline]
1651    pub fn interact(&self) -> &InteractSpec {
1652        static EMPTY: InteractSpec = InteractSpec::EMPTY;
1653        self.interact.as_deref().unwrap_or(&EMPTY)
1654    }
1655
1656    /// Allocates the group on first write.
1657    #[inline]
1658    pub fn interact_mut(&mut self) -> &mut InteractSpec {
1659        self.interact.get_or_insert_with(Box::default)
1660    }
1661
1662    /// Whether the core registers a hit region for this node (any of the
1663    /// interaction props, or an explicit `hoverable`).
1664    pub fn hover_tracked(&self) -> bool {
1665        // Asked of every node at emission; the inline flags first, then
1666        // each boxed group once — a node that declares neither group is
1667        // answered by two null checks rather than a read per field.
1668        self.hoverable
1669            || self.focusable
1670            || self.window.is_some()
1671            // A `cursor` override has to be found under the pointer to be
1672            // read, even on an otherwise inert box.
1673            || self.cursor.is_some()
1674            || self.events.as_deref().is_some_and(|e| {
1675                // A modal's own background is not "outside" it: a press
1676                // there must find a region.
1677                e.modal.is_some()
1678                    || e.on_click.is_some()
1679                    || e.on_drag.is_some()
1680                    || e.on_key.is_some()
1681                    || e.on_context_menu.is_some()
1682                    || e.on_force_click.is_some()
1683                    || e.on_button.is_some()
1684                    || e.on_hover.is_some()
1685                    || e.on_drop.is_some()
1686                    || e.on_change.is_some()
1687            })
1688            || self.interact.as_deref().is_some_and(|i| {
1689                i.hover_bg.is_some()
1690                    || i.pressed_bg.is_some()
1691                    || i.drop_bg.is_some()
1692                    || i.hover_group.is_some()
1693                    || i.click_sound.is_some()
1694                    || i.hover_sound.is_some()
1695                    // A selection scope has to be found under the pointer:
1696                    // the press that starts a drag-select lands on it.
1697                    || i.selectable
1698                    // So does a focus region: a press on its dead space
1699                    // settles the ring there.
1700                    || i.focus_region
1701            })
1702    }
1703
1704    /// The group id `hover_group(name)` assigns — reproducible from any
1705    /// binding (the same FNV mix as `Key`).
1706    pub fn hover_group_id(name: &str) -> u64 {
1707        crate::key::Key::ROOT.str(name).0
1708    }
1709
1710    /// A container that lays its children out left to right.
1711    pub fn row() -> Self {
1712        Self {
1713            layout: LayoutSpec {
1714                dir: Dir::Row,
1715                ..Default::default()
1716            },
1717            ..Default::default()
1718        }
1719    }
1720
1721    /// A container that lays its children out top to bottom (the default).
1722    pub fn column() -> Self {
1723        Self {
1724            layout: LayoutSpec {
1725                dir: Dir::Column,
1726                ..Default::default()
1727            },
1728            ..Default::default()
1729        }
1730    }
1731
1732    /// A table: a column whose rows' children line up in columns. Its
1733    /// in-flow `Row` children are the rows and each row's in-flow children
1734    /// its cells; the nth cell of every row is column n, and a column is
1735    /// as wide as its widest cell, so a label column sits at its longest
1736    /// label with no width picked by hand.
1737    ///
1738    /// A cell's `width` says how its column sizes: `Fit` (the default) and
1739    /// `Fixed` are content the column's fit width is the max of, `Grow`
1740    /// makes the whole column grow with the table, and a column's
1741    /// `min_width` / `max_width` are the strictest its cells declared.
1742    /// Give the rows `width: grow` for the columns to grow into; a `Fit`
1743    /// row sits at the columns' fit width. Rows keep their own `gap`,
1744    /// padding, background, click and hover, and never wrap. A bare text
1745    /// or image straight inside a row is a cell too. A text, a column or a
1746    /// table straight under the table is a child with its own width and no
1747    /// cells. Everything else is a column's: `gap` is the space between
1748    /// rows, `scroll_y` scrolls them.
1749    ///
1750    /// ```rust
1751    /// use kui_core::{NodeSpec, Sizing};
1752    /// let table = NodeSpec::table().gap(4.0).rules(kui_core::Color::hex(0x80808080));
1753    /// let row = NodeSpec::row().width(Sizing::GROW).gap(12.0);
1754    /// assert!(table.layout.is_table() && !row.layout.is_table());
1755    /// ```
1756    pub fn table() -> Self {
1757        Self {
1758            layout: LayoutSpec {
1759                dir: Dir::Column,
1760                table: true,
1761                ..Default::default()
1762            },
1763            ..Default::default()
1764        }
1765    }
1766
1767    /// A [`Sizing`], or a number of px (`.width(120.0)`).
1768    #[inline]
1769    pub fn width(mut self, s: impl Into<Sizing>) -> Self {
1770        self.layout.width = s.into();
1771        self
1772    }
1773
1774    /// A [`Sizing`], or a number of px (`.height(24.0)`).
1775    #[inline]
1776    pub fn height(mut self, s: impl Into<Sizing>) -> Self {
1777        self.layout.height = s.into();
1778        self
1779    }
1780
1781    /// Both axes at once: `.size(20.0, 20.0)` for a fixed box,
1782    /// `.size(Sizing::GROW, 24.0)` for a strip.
1783    #[inline]
1784    pub fn size(self, w: impl Into<Sizing>, h: impl Into<Sizing>) -> Self {
1785        self.width(w).height(h)
1786    }
1787
1788    /// An equal share of the leftover width: `.width(Sizing::GROW)`.
1789    #[inline]
1790    pub fn grow_width(self) -> Self {
1791        self.width(Sizing::GROW)
1792    }
1793
1794    /// An equal share of the leftover height: `.height(Sizing::GROW)`.
1795    #[inline]
1796    pub fn grow_height(self) -> Self {
1797        self.height(Sizing::GROW)
1798    }
1799
1800    /// Grow along both axes.
1801    #[inline]
1802    pub fn fill(self) -> Self {
1803        self.grow_width().grow_height()
1804    }
1805
1806    /// A number of px, [`Min::FIT`] for the node's own fit width, or a
1807    /// [`Bound::Calc`].
1808    pub fn min_width(mut self, v: impl Into<Bound>) -> Self {
1809        self.layout.min_w = match v.into() {
1810            Bound::Px(px) if px == 0.0 && px.is_sign_negative() => Min::AUTO,
1811            Bound::Px(px) => Min::px(px),
1812            Bound::Fit => Min::FIT,
1813            Bound::Calc(c) => Min::calc(c),
1814        };
1815        self
1816    }
1817
1818    /// A number of px, or a [`Bound::Calc`]; a max has no fit size, so
1819    /// [`Bound::Fit`] is no clamp. A px ceiling below zero, or `NaN`, is
1820    /// a ceiling of 0 ([`px_ceiling`]).
1821    pub fn max_width(mut self, v: impl Into<Bound>) -> Self {
1822        self.layout.max_w = match v.into() {
1823            Bound::Px(px) => px_ceiling(px),
1824            Bound::Fit => f32::INFINITY,
1825            Bound::Calc(c) => max_of_calc(c),
1826        };
1827        self
1828    }
1829
1830    /// A number of px, [`Min::FIT`] for the node's own fit height, or a
1831    /// [`Bound::Calc`].
1832    pub fn min_height(mut self, v: impl Into<Bound>) -> Self {
1833        self.layout.min_h = match v.into() {
1834            Bound::Px(px) if px == 0.0 && px.is_sign_negative() => Min::AUTO,
1835            Bound::Px(px) => Min::px(px),
1836            Bound::Fit => Min::FIT,
1837            Bound::Calc(c) => Min::calc(c),
1838        };
1839        self
1840    }
1841
1842    /// The four clamps where each is given, a transport's shape (C's
1843    /// `*_size` fields): `None` leaves that clamp as it is.
1844    pub fn with_bounds(
1845        self,
1846        min_w: Option<Bound>,
1847        max_w: Option<Bound>,
1848        min_h: Option<Bound>,
1849        max_h: Option<Bound>,
1850    ) -> Self {
1851        let mut s = self;
1852        if let Some(b) = min_w {
1853            s = s.min_width(b);
1854        }
1855        if let Some(b) = max_w {
1856            s = s.max_width(b);
1857        }
1858        if let Some(b) = min_h {
1859            s = s.min_height(b);
1860        }
1861        if let Some(b) = max_h {
1862            s = s.max_height(b);
1863        }
1864        s
1865    }
1866
1867    /// Clip children to this node's rect without scrolling. A `radius` on
1868    /// the same node rounds the clip, so children stay inside its corners
1869    /// (see `display::Clip`).
1870    pub fn clip(mut self) -> Self {
1871        self.layout.clip = true;
1872        self
1873    }
1874
1875    /// Vertical scrolling (and clipping) for overflowing content.
1876    pub fn scroll_y(mut self) -> Self {
1877        self.layout.scroll_y = true;
1878        self
1879    }
1880
1881    /// Horizontal scrolling (and clipping) for overflowing content.
1882    pub fn scroll_x(mut self) -> Self {
1883        self.layout.scroll_x = true;
1884        self
1885    }
1886
1887    /// Scroll anchoring on this container (see [`LayoutSpec::anchor`]):
1888    /// the first child in view stays where it is on screen when the
1889    /// content before it changes size. Needs `scroll_y` on a column or
1890    /// `scroll_x` on a row.
1891    pub fn anchor(mut self) -> Self {
1892        self.layout.anchor = true;
1893        self
1894    }
1895
1896    /// Take this node out of flex flow; see [`FloatConfig`].
1897    pub fn float(mut self, cfg: FloatConfig) -> Self {
1898        self.layout.float = Some(cfg);
1899        self
1900    }
1901
1902    /// Apply the overflow bits ([`OVERFLOW_CLIP`] and friends). What a bit
1903    /// means is decided here: a binding ORs together whatever its own
1904    /// surface spells (`clip`, `scrollX`, `overflow`) and hands the number
1905    /// over, rather than each one re-deciding that scrolling clips too.
1906    pub fn overflow_bits(mut self, bits: u32) -> Self {
1907        if bits & OVERFLOW_CLIP != 0 {
1908            self = self.clip();
1909        }
1910        if bits & OVERFLOW_SCROLL_X != 0 {
1911            self = self.scroll_x();
1912        }
1913        if bits & OVERFLOW_SCROLL_Y != 0 {
1914            self = self.scroll_y();
1915        }
1916        self
1917    }
1918
1919    /// A tooltip: the node tracks hover, `hint` is its accessible
1920    /// description, and the core floats the hint below it while it is
1921    /// hovered. On a box or a fragment the float is the node's last child;
1922    /// on a leaf — an image, an editor, a cells grid, a line, a polygon, a
1923    /// path — which holds no children, it floats beside the leaf, anchored
1924    /// to it ([`FloatAnchor::Node`]), and lands below the leaf's box all
1925    /// the same (backlog RG113).
1926    #[inline]
1927    pub fn tooltip(self, hint: &str) -> Self {
1928        let mut spec = self.apply_tooltip(hint);
1929        spec.access_mut().tooltip = true;
1930        spec
1931    }
1932
1933    /// The spec half of the `tooltip` prop: the hint is hover-gated, so the
1934    /// node tracks hover, and it is what assistive technology should say, so
1935    /// it is the accessible description too — and nothing floats. For a
1936    /// caller that floats the hint itself: [`crate::schema::PropsOut::apply_tooltip`]
1937    /// for the parsers, `kui_close` for C, a widget that draws its own. A
1938    /// Rust view wants [`Self::tooltip`], which is all three.
1939    pub fn apply_tooltip(self, hint: &str) -> Self {
1940        self.hoverable().description(hint)
1941    }
1942
1943    /// A number of px, or a [`Bound::Calc`]; [`Bound::Fit`] is no clamp.
1944    /// A px ceiling below zero, or `NaN`, is a ceiling of 0.
1945    pub fn max_height(mut self, v: impl Into<Bound>) -> Self {
1946        self.layout.max_h = match v.into() {
1947            Bound::Px(px) => px_ceiling(px),
1948            Bound::Fit => f32::INFINITY,
1949            Bound::Calc(c) => max_of_calc(c),
1950        };
1951        self
1952    }
1953
1954    /// The same padding on all four edges, logical px.
1955    pub fn pad(mut self, v: f32) -> Self {
1956        self.layout.padding = Edges::all(v);
1957        self
1958    }
1959
1960    /// Padding `x` on the left and right, `y` on the top and bottom.
1961    pub fn pad_xy(mut self, x: f32, y: f32) -> Self {
1962        self.layout.padding = Edges::xy(x, y);
1963        self
1964    }
1965
1966    /// Padding per edge.
1967    pub fn padding(mut self, e: Edges) -> Self {
1968        self.layout.padding = e;
1969        self
1970    }
1971
1972    /// Space between children along the main axis, logical px.
1973    pub fn gap(mut self, v: f32) -> Self {
1974        self.layout.gap = v;
1975        self
1976    }
1977
1978    /// Wrap overflowing children onto more lines; see [`LayoutSpec::wrap`].
1979    pub fn wrap(mut self) -> Self {
1980        self.layout.wrap = true;
1981        self
1982    }
1983
1984    /// Space between wrap lines (across the main axis).
1985    pub fn cross_gap(mut self, v: f32) -> Self {
1986        self.layout.cross_gap = v;
1987        self
1988    }
1989
1990    /// Where children sit along the main axis; the spreads are allowed.
1991    pub fn main_align(mut self, a: Align) -> Self {
1992        self.layout.main_align = a;
1993        self
1994    }
1995
1996    /// Where children sit across the main axis; `Baseline` on a row.
1997    pub fn cross_align(mut self, a: Align) -> Self {
1998        self.layout.cross_align = a;
1999        self
2000    }
2001
2002    /// Width over height (CSS's `aspect-ratio`): see
2003    /// [`LayoutSpec::aspect`]. `16.0 / 9.0` for a video, `1.0` for a
2004    /// square. A ratio that is not positive and finite clears it.
2005    pub fn aspect_ratio(mut self, ratio: f32) -> Self {
2006        self.layout.aspect = if ratio.is_finite() && ratio > 0.0 {
2007            ratio
2008        } else {
2009            0.0
2010        };
2011        self
2012    }
2013
2014    /// Center children on both axes.
2015    pub fn center(self) -> Self {
2016        self.main_align(Align::Center).cross_align(Align::Center)
2017    }
2018
2019    /// The background colour.
2020    pub fn bg(mut self, c: Color) -> Self {
2021        self.style.bg = c;
2022        self
2023    }
2024
2025    /// Rounds all four corners by `r`. On a node that also clips or
2026    /// scrolls it rounds the clip too, so its children are cut to the same
2027    /// corners rather than poking square out of them.
2028    pub fn radius(mut self, r: f32) -> Self {
2029        self.style.radius = [r; 4];
2030        self
2031    }
2032
2033    /// Per-corner radii, clockwise from the top-left.
2034    pub fn radii(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
2035        self.style.radius = [tl, tr, br, bl];
2036        self
2037    }
2038
2039    pub fn radius_tl(mut self, r: f32) -> Self {
2040        self.style.radius[corner::TL] = r;
2041        self
2042    }
2043
2044    pub fn radius_tr(mut self, r: f32) -> Self {
2045        self.style.radius[corner::TR] = r;
2046        self
2047    }
2048
2049    pub fn radius_br(mut self, r: f32) -> Self {
2050        self.style.radius[corner::BR] = r;
2051        self
2052    }
2053
2054    pub fn radius_bl(mut self, r: f32) -> Self {
2055        self.style.radius[corner::BL] = r;
2056        self
2057    }
2058
2059    /// Rounds the two top corners (tabs, headers).
2060    pub fn radius_top(self, r: f32) -> Self {
2061        self.radius_tl(r).radius_tr(r)
2062    }
2063
2064    /// Rounds the two bottom corners.
2065    pub fn radius_bottom(self, r: f32) -> Self {
2066        self.radius_br(r).radius_bl(r)
2067    }
2068
2069    /// A border `w` px wide in `c`, inside the node's box.
2070    pub fn border(mut self, w: f32, c: Color) -> Self {
2071        self.style.border_w = w;
2072        self.style.border_color = c;
2073        self
2074    }
2075
2076    /// Paints this node's background, border, shadow and fragment on whole pixels
2077    /// (see `VisualStyle::pixel_snap`).
2078    pub fn pixel_snap(mut self) -> Self {
2079        self.style.pixel_snap = true;
2080        self
2081    }
2082
2083    /// Fades this node and everything under it (see `VisualStyle::opacity`);
2084    /// clamped to 0..=1.
2085    pub fn opacity(mut self, o: f32) -> Self {
2086        self.style.opacity = o.clamp(0.0, 1.0);
2087        self
2088    }
2089
2090    /// The whole drop shadow at once: color, offset, blur, spread.
2091    pub fn shadow(mut self, shadow: Shadow) -> Self {
2092        self.style.shadow = shadow;
2093        self
2094    }
2095
2096    /// The shadow's color — on its own, a hard shadow exactly behind the
2097    /// node. Nothing else about a shadow draws without it.
2098    pub fn shadow_color(mut self, c: Color) -> Self {
2099        self.style.shadow.color = c;
2100        self
2101    }
2102
2103    pub fn shadow_blur(mut self, blur: f32) -> Self {
2104        self.style.shadow.blur = blur;
2105        self
2106    }
2107
2108    pub fn shadow_offset(mut self, dx: f32, dy: f32) -> Self {
2109        self.style.shadow.dx = dx;
2110        self.style.shadow.dy = dy;
2111        self
2112    }
2113
2114    pub fn shadow_x(mut self, dx: f32) -> Self {
2115        self.style.shadow.dx = dx;
2116        self
2117    }
2118
2119    pub fn shadow_y(mut self, dy: f32) -> Self {
2120        self.style.shadow.dy = dy;
2121        self
2122    }
2123
2124    pub fn shadow_spread(mut self, spread: f32) -> Self {
2125        self.style.shadow.spread = spread;
2126        self
2127    }
2128
2129    /// Hover-track this node without making it clickable; see the
2130    /// `hoverable` field.
2131    pub fn hoverable(mut self) -> Self {
2132        self.hoverable = true;
2133        self
2134    }
2135
2136    /// Ask for another frame after this one, for as long as this node is
2137    /// declared. See the `animate` prop.
2138    pub fn animate(mut self) -> Self {
2139        self.animate = true;
2140        self
2141    }
2142
2143    /// Background from the OS accent colour where the host knows it; see
2144    /// the field.
2145    pub fn accent(mut self) -> Self {
2146        self.accent = true;
2147        self
2148    }
2149
2150    /// Makes this node clickable: a click emits `payload` as a
2151    /// [`UiEvent`](crate::input::UiEvent), and the node joins the Tab ring
2152    /// and reads as a button to assistive technology.
2153    pub fn on_click(mut self, payload: impl Into<Value>) -> Self {
2154        self.events_mut().on_click = Some(payload.into());
2155        self
2156    }
2157
2158    /// Background while hovered (see the `hover_bg` field).
2159    pub fn hover_bg(mut self, c: Color) -> Self {
2160        self.interact_mut().hover_bg = Some(c);
2161        self
2162    }
2163
2164    /// Background while dragged files are over this node (see the
2165    /// `drop_bg` field).
2166    pub fn drop_bg(mut self, c: Color) -> Self {
2167        self.interact_mut().drop_bg = Some(c);
2168        self
2169    }
2170
2171    /// Paints `gradient` over this node's `bg`, under its border and
2172    /// its children (see the `gradient` field).
2173    pub fn gradient(mut self, gradient: crate::gradient::Gradient) -> Self {
2174        self.interact_mut().gradient = Some(gradient);
2175        self
2176    }
2177
2178    /// Blurs what was drawn beneath this node, inside its rounded rect,
2179    /// by `radius` logical px (see the `backdrop_blur` field). A
2180    /// translucent `bg` over it makes frosted glass:
2181    ///
2182    /// ```
2183    /// # use kui_core::{Color, NodeSpec};
2184    /// let bar = NodeSpec::row().backdrop_blur(16.0).bg(Color::hex(0xffffff40));
2185    /// assert_eq!(bar.interact().backdrop_blur, 16.0);
2186    /// ```
2187    pub fn backdrop_blur(mut self, radius: f32) -> Self {
2188        self.interact_mut().backdrop_blur = radius.max(0.0);
2189        self
2190    }
2191
2192    /// Turns this node and everything under it by `turns`, clockwise,
2193    /// about its pivot (the centre unless `pivot` says), after layout
2194    /// (see [`TransformSpec`], ADR 0043). Tweens with `transition`:
2195    ///
2196    /// ```
2197    /// # use kui_core::NodeSpec;
2198    /// let card = NodeSpec::column().rotate(0.02).transition(300.0);
2199    /// assert_eq!(card.transform_spec().unwrap().rotate, 0.02);
2200    /// ```
2201    pub fn rotate(mut self, turns: f32) -> Self {
2202        self.transform_mut().rotate = turns;
2203        self
2204    }
2205
2206    /// Scales this node and everything under it by `scale` about its
2207    /// pivot, after layout (see [`TransformSpec`]); 1 is none.
2208    pub fn scale(mut self, scale: f32) -> Self {
2209        self.transform_mut().scale = scale;
2210        self
2211    }
2212
2213    /// The point `rotate` and `scale` are about, as fractions of the box:
2214    /// `(0.5, 0.5)` — the default — is the centre, `(0.0, 1.0)` the
2215    /// bottom-left corner.
2216    pub fn pivot(mut self, fx: f32, fy: f32) -> Self {
2217        self.transform_mut().pivot = crate::geom::Vec2::new(fx, fy);
2218        self
2219    }
2220
2221    /// The node's transform, allocated on first write.
2222    #[inline]
2223    pub(crate) fn transform_mut(&mut self) -> &mut TransformSpec {
2224        self.interact_mut()
2225            .transform
2226            .get_or_insert_with(|| Box::new(TransformSpec::NONE))
2227    }
2228
2229    /// The node's `rotate`, `scale` and `pivot` (ADR 0043), when it
2230    /// declares any.
2231    #[inline]
2232    pub fn transform_spec(&self) -> Option<TransformSpec> {
2233        self.interact
2234            .as_deref()
2235            .and_then(|i| i.transform.as_deref().copied())
2236    }
2237
2238    /// A table's grid rules in `c` (see the `rules` field).
2239    pub fn rules(mut self, c: Color) -> Self {
2240        self.interact_mut().rules = Some(c);
2241        self
2242    }
2243
2244    /// The rules' width in logical px; 1 when unset (see `rules`).
2245    pub fn rule_width(mut self, w: f32) -> Self {
2246        self.interact_mut().rule_w = w;
2247        self
2248    }
2249
2250    /// Makes this node a selection scope (see the `selectable` field):
2251    /// the text inside it becomes one selectable run.
2252    pub fn selectable(mut self) -> Self {
2253        self.interact_mut().selectable = true;
2254        self
2255    }
2256
2257    /// Makes this node's subtree a focus region (see the `focus_region`
2258    /// field): a Tab ring of its own, entered on purpose.
2259    pub fn focus_region(mut self) -> Self {
2260        self.interact_mut().focus_region = true;
2261        self
2262    }
2263
2264    /// Whether a scroll gesture starting over this scroller at its limit
2265    /// goes on to the one around it (see [`Overscroll`]):
2266    /// `Overscroll::Contain` keeps it here.
2267    pub fn overscroll(mut self, o: Overscroll) -> Self {
2268        self.interact_mut().overscroll = o;
2269        self
2270    }
2271
2272    /// When this node's scrollbars are drawn (see [`ScrollbarMode`]).
2273    /// Scrolling itself is unchanged whatever the mode.
2274    pub fn scrollbar(mut self, mode: ScrollbarMode) -> Self {
2275        self.interact_mut().scrollbar.mode = mode;
2276        self
2277    }
2278
2279    /// The thumb's width at rest, logical px (see [`Scrollbar::width`]).
2280    pub fn scrollbar_width(mut self, w: f32) -> Self {
2281        self.interact_mut().scrollbar.width = Some(w);
2282        self
2283    }
2284
2285    /// The thumb's colour at rest (see [`Scrollbar::color`]).
2286    pub fn scrollbar_color(mut self, c: Color) -> Self {
2287        self.interact_mut().scrollbar.color = Some(c);
2288        self
2289    }
2290
2291    /// The thumb's colour under the pointer or dragged (see
2292    /// [`Scrollbar::active_color`]).
2293    pub fn scrollbar_active_color(mut self, c: Color) -> Self {
2294        self.interact_mut().scrollbar.active_color = Some(c);
2295        self
2296    }
2297
2298    /// Background while pressed (see the `pressed_bg` field).
2299    pub fn pressed_bg(mut self, c: Color) -> Self {
2300        self.interact_mut().pressed_bg = Some(c);
2301        self
2302    }
2303
2304    /// Background while keyboard-visibly focused (see the `focus_bg`
2305    /// field); replaces the default focus ring.
2306    pub fn focus_bg(mut self, c: Color) -> Self {
2307        self.interact_mut().focus_bg = Some(c);
2308        self
2309    }
2310
2311    /// Puts this node in the Tab ring (see the `focusable` field).
2312    pub fn focusable(mut self) -> Self {
2313        self.focusable = true;
2314        self
2315    }
2316
2317    /// Hears focus entering and leaving this subtree (see the `on_focus`
2318    /// field).
2319    pub fn on_focus(mut self, tag: impl Into<Value>) -> Self {
2320        self.events_mut().on_focus = Some(tag.into());
2321        self
2322    }
2323
2324    /// A press here leaves keyboard focus where it was (see the
2325    /// `keep_focus` field).
2326    pub fn keep_focus(mut self) -> Self {
2327        self.events_mut().keep_focus = true;
2328        self
2329    }
2330
2331    /// Makes this node the modal scope's entry focus (see the
2332    /// `initial_focus` field).
2333    pub fn initial_focus(mut self) -> Self {
2334        self.initial_focus = true;
2335        self
2336    }
2337
2338    /// Makes this node inert (see the `disabled` field).
2339    pub fn disabled(mut self, disabled: bool) -> Self {
2340        self.disabled = disabled;
2341        self
2342    }
2343
2344    /// Makes this node the frame's modal surface (see the `modal` field).
2345    #[inline]
2346    pub fn modal(mut self, tag: impl Into<Value>) -> Self {
2347        self.events_mut().modal = Some(tag.into());
2348        self
2349    }
2350
2351    /// Joins the hover group `name` (see the `hover_group` field).
2352    pub fn hover_group(mut self, name: &str) -> Self {
2353        self.interact_mut().hover_group = Some(Self::hover_group_id(name));
2354        self
2355    }
2356
2357    /// Emits enter/leave events for this node (see the `on_hover` field).
2358    /// Pass a tag the handler can match on; `Value::Null` if the node key
2359    /// is identification enough.
2360    pub fn on_hover(mut self, tag: impl Into<Value>) -> Self {
2361        self.events_mut().on_hover = Some(tag.into());
2362        self
2363    }
2364
2365    /// Makes this node a drop zone for files dragged in from the OS (see
2366    /// the `on_drop` field).
2367    pub fn on_drop(mut self, tag: impl Into<Value>) -> Self {
2368        self.events_mut().on_drop = Some(tag.into());
2369        self
2370    }
2371
2372    /// Plays a registered sound when this node is clicked (see the
2373    /// `click_sound` field).
2374    pub fn click_sound(mut self, sound: crate::resources::SoundId) -> Self {
2375        self.interact_mut().click_sound = Some(sound);
2376        self
2377    }
2378
2379    /// Plays a registered sound when the pointer enters this node (see the
2380    /// `hover_sound` field).
2381    pub fn hover_sound(mut self, sound: crate::resources::SoundId) -> Self {
2382        self.interact_mut().hover_sound = Some(sound);
2383        self
2384    }
2385
2386    /// Makes this node draggable (see the `on_drag` field). Pass a tag the
2387    /// handler can match on; `Value::Null` if the node key is enough.
2388    pub fn on_drag(mut self, tag: impl Into<Value>) -> Self {
2389        self.events_mut().on_drag = Some(tag.into());
2390        self
2391    }
2392
2393    /// Reports this node's laid-out rect as an event whenever it changes
2394    /// (see the `on_layout` field). Pass a tag the handler can match on;
2395    /// `Value::Null` if the node key is identification enough.
2396    pub fn on_layout(mut self, tag: impl Into<Value>) -> Self {
2397        self.events_mut().on_layout = Some(tag.into());
2398        self
2399    }
2400
2401    /// What this node is to assistive technology (see the `role` field).
2402    pub fn role(mut self, role: Role) -> Self {
2403        self.access_mut().role = Some(role);
2404        self
2405    }
2406
2407    /// The accessible name (see the `label` field). Takes an `Arc<str>`
2408    /// as well as a `&str`, so a view can keep one and hand it out every
2409    /// frame without allocating.
2410    pub fn label(mut self, label: impl Into<Label>) -> Self {
2411        self.access_mut().label = Some(label.into());
2412        self
2413    }
2414
2415    /// The accessible description (see the `description` field).
2416    pub fn description(mut self, description: impl Into<Label>) -> Self {
2417        self.access_mut().description = Some(description.into());
2418        self
2419    }
2420
2421    /// The on state for a checkbox / radio / switch role.
2422    pub fn checked(mut self, checked: bool) -> Self {
2423        self.access_mut().checked = checked;
2424        self
2425    }
2426
2427    /// A checkbox that is neither on nor off (see the `mixed` field).
2428    pub fn mixed(mut self, mixed: bool) -> Self {
2429        self.access_mut().mixed = mixed;
2430        self
2431    }
2432
2433    /// The current one of a set (see the `selected` field).
2434    pub fn selected(mut self, selected: bool) -> Self {
2435        self.access_mut().selected = selected;
2436        self
2437    }
2438
2439    /// A disclosure's state (see the `expanded` field).
2440    pub fn expanded(mut self, expanded: bool) -> Self {
2441        self.access_mut().expanded = Some(expanded);
2442        self
2443    }
2444
2445    /// Marks this node a live region (see the `live` field).
2446    pub fn live(mut self, live: Live) -> Self {
2447        self.access_mut().live = live;
2448        self
2449    }
2450
2451    /// A slider role's current value.
2452    pub fn value_now(mut self, v: f32) -> Self {
2453        self.access_mut().value_now = Some(v);
2454        self
2455    }
2456
2457    pub fn value_min(mut self, v: f32) -> Self {
2458        self.access_mut().value_min = Some(v);
2459        self
2460    }
2461
2462    pub fn value_max(mut self, v: f32) -> Self {
2463        self.access_mut().value_max = Some(v);
2464        self
2465    }
2466
2467    /// How far one arrow key moves a slider (see the `value_step` field).
2468    /// A step that is not positive and finite clears it.
2469    pub fn value_step(mut self, v: f32) -> Self {
2470        self.access_mut().value_step = (v.is_finite() && v > 0.0).then_some(v);
2471        self
2472    }
2473
2474    /// Asks the core for a slider's changes (see the `on_change` field).
2475    pub fn on_change(mut self, tag: impl Into<Value>) -> Self {
2476        self.events_mut().on_change = Some(tag.into());
2477        self
2478    }
2479
2480    /// What a slider's position reads as (see the `value_text` field).
2481    pub fn value_text(mut self, text: impl Into<Label>) -> Self {
2482        self.access_mut().value_text = Some(text.into());
2483        self
2484    }
2485
2486    /// On a `Role::Line` of a custom editor: the caret's byte offset into
2487    /// this line's text (see the `caret` field).
2488    pub fn caret(mut self, offset: u32) -> Self {
2489        self.access_mut().caret = Some(offset);
2490        self
2491    }
2492
2493    /// On a `Role::Line` of a custom editor: the byte offset where the
2494    /// selection's other end sits (see the `selection_anchor` field).
2495    pub fn selection_anchor(mut self, offset: u32) -> Self {
2496        self.access_mut().selection_anchor = Some(offset);
2497        self
2498    }
2499
2500    /// On a `Role::Line` declaring `caret`: the caret is solid, not a
2501    /// caret to blink (see the `caret_solid` field). A block caret in a
2502    /// modal editor's normal mode — the one thing that otherwise asks an
2503    /// idle app for a frame twice a second.
2504    pub fn caret_solid(mut self) -> Self {
2505        self.access_mut().caret_solid = true;
2506        self
2507    }
2508
2509    /// Makes this node a key sink (see `NodeSpec::on_key` field). Pass a
2510    /// tag the handler can match on; `Value::Null` if the node key is
2511    /// identification enough.
2512    pub fn on_key(mut self, tag: impl Into<Value>) -> Self {
2513        self.events_mut().on_key = Some(tag.into());
2514        self
2515    }
2516
2517    /// A key sink with no tag — `on_key(Value::Null)`, for a handler that
2518    /// knows the node by its key.
2519    #[inline]
2520    pub fn key_sink(self) -> Self {
2521        self.on_key(Value::Null)
2522    }
2523
2524    /// Delivers releases to this key sink as well as presses (see the
2525    /// `key_up` field). Meaningless without `on_key`.
2526    pub fn key_up(mut self) -> Self {
2527        self.events_mut().key_up = true;
2528        self
2529    }
2530
2531    /// Delivers the modifier and lock keys to this key sink as keys of
2532    /// their own (see the `modifier_keys` field). Meaningless without
2533    /// `on_key`.
2534    pub fn modifier_keys(mut self) -> Self {
2535        self.events_mut().modifier_keys = true;
2536        self
2537    }
2538
2539    /// Asks for force-click events on this node (see the
2540    /// `on_force_click` field).
2541    pub fn on_force_click(mut self, tag: impl Into<Value>) -> Self {
2542        self.events_mut().on_force_click = Some(tag.into());
2543        self
2544    }
2545
2546    /// Asks for the non-primary buttons pressed over this node as events,
2547    /// each captured by it until its release (see the `on_button` field):
2548    /// `{kind="button", phase, button, x, y, tag}`. Every such button
2549    /// unless [`NodeSpec::buttons`] narrows it.
2550    pub fn on_button(mut self, tag: impl Into<Value>) -> Self {
2551        self.events_mut().on_button = Some(tag.into());
2552        self
2553    }
2554
2555    /// Which non-primary buttons `on_button` claims (see the `buttons`
2556    /// field): `Buttons::MIDDLE`, `Buttons::SECONDARY | Buttons::MIDDLE`.
2557    /// Meaningless without `on_button`.
2558    pub fn buttons(mut self, buttons: Buttons) -> Self {
2559        self.events_mut().buttons = buttons;
2560        self
2561    }
2562
2563    /// Asks for the wheel over this node as events (see the `on_scroll`
2564    /// field): `{kind="scroll", x, y, dx, dy, lines, tag}`.
2565    pub fn on_scroll(mut self, tag: impl Into<Value>) -> Self {
2566        self.events_mut().on_scroll = Some(tag.into());
2567        self
2568    }
2569
2570    /// Which axes `on_scroll` takes (see the `scroll_axes` field):
2571    /// `ScrollAxes::Y` for a terminal's history, so a sideways gesture
2572    /// passes it by. Meaningless without `on_scroll`.
2573    pub fn scroll_axes(mut self, axes: ScrollAxes) -> Self {
2574        self.events_mut().scroll_axes = axes;
2575        self
2576    }
2577
2578    /// The modifiers `on_scroll` is for (see the `scroll_mods` field):
2579    /// `KeyMods::NONE.with_ctrl().with_super()` hears a wheel turned with
2580    /// Ctrl or ⌘ held, ahead of every scroller under the pointer, and no
2581    /// other wheel. Meaningless without `on_scroll`.
2582    pub fn scroll_mods(mut self, mods: crate::input::KeyMods) -> Self {
2583        self.events_mut().scroll_mods = mods;
2584        self
2585    }
2586
2587    /// Asks for a context menu on this node (see the `on_context_menu`
2588    /// field). Pass a tag the handler can match on; `Value::Null` if the
2589    /// node key is identification enough.
2590    pub fn on_context_menu(mut self, tag: impl Into<Value>) -> Self {
2591        self.events_mut().on_context_menu = Some(tag.into());
2592        self
2593    }
2594
2595    /// Animates changes to this node's sizing amounts, colors and radius
2596    /// over `duration_ms` (cubic ease-out); see [`crate::anim`]. Only the
2597    /// duration: an easing or repeat already declared survives, so the
2598    /// transition props compose in any order.
2599    ///
2600    /// On a scroll container it also eases the offset a `reveal` or a
2601    /// `set_scroll` moves it to; the wheel, the thumb and a drag past the
2602    /// edge still land whole.
2603    ///
2604    /// ```rust
2605    /// use kui_core::{Easing, NodeSpec, Sizing};
2606    /// // Eases toward whatever width the view declares next frame.
2607    /// let pane = NodeSpec::column().width(Sizing::Fixed(240.0)).transition(180.0).easing(Easing::Smooth);
2608    /// assert_eq!(pane.transition.unwrap().duration_ms, 180.0);
2609    /// ```
2610    pub fn transition(mut self, duration_ms: f32) -> Self {
2611        let t = self.transition.get_or_insert(Transition::ms(duration_ms));
2612        t.duration_ms = duration_ms;
2613        self
2614    }
2615
2616    /// The whole [`Transition`] at once, replacing any declared so far.
2617    pub fn transition_with(mut self, t: Transition) -> Self {
2618        self.transition = Some(t);
2619        self
2620    }
2621
2622    /// Also ease this node's position (see the `slide` field); sets a
2623    /// default 200ms transition if none was declared yet.
2624    pub fn slide(mut self) -> Self {
2625        self.transition.get_or_insert(Transition::ms(200.0));
2626        self.slide = true;
2627        self
2628    }
2629
2630    /// Paint after the siblings (see the `over_siblings` field).
2631    pub fn over_siblings(mut self) -> Self {
2632        self.over_siblings = true;
2633        self
2634    }
2635
2636    /// Easing for the node's transition (sets a default 200ms one if none
2637    /// was declared yet).
2638    pub fn easing(mut self, easing: Easing) -> Self {
2639        let t = self.transition.get_or_insert(Transition::ms(200.0));
2640        t.easing = easing;
2641        self
2642    }
2643
2644    /// How far the node's spring overshoots, 0 (glides in) to
2645    /// [`crate::anim::MAX_BOUNCE`] — in place of the spring easing's own
2646    /// bounce, and on a timed easing in place of the curve, making it a
2647    /// spring (see [`Transition::bounce`]). Sets a default 200ms
2648    /// transition if none was declared yet.
2649    pub fn bounce(mut self, bounce: f32) -> Self {
2650        let t = self.transition.get_or_insert(Transition::ms(200.0));
2651        *t = t.bounce(bounce);
2652        self
2653    }
2654
2655    /// How this node's `keyframes` cycle (CSS's `animation-direction`);
2656    /// sets a default 200ms transition if none was declared yet.
2657    pub fn repeat(mut self, repeat: Repeat) -> Self {
2658        let t = self.transition.get_or_insert(Transition::ms(200.0));
2659        t.repeat = repeat;
2660        self
2661    }
2662
2663    /// Holds this node's keyframe cycle back by `delay_ms` (CSS's
2664    /// `animation-delay`), so siblings given different delays run out of
2665    /// phase — a cascade, a chase light. Sets a default 200ms transition if
2666    /// none was declared yet.
2667    pub fn delay(mut self, delay_ms: f32) -> Self {
2668        let t = self.transition.get_or_insert(Transition::ms(200.0));
2669        t.delay_ms = delay_ms;
2670        self
2671    }
2672
2673    /// Cycles the slots these stops name (see the `keyframes` field); sets
2674    /// a default 200ms transition if none was declared yet.
2675    pub fn keyframes(mut self, stops: Vec<Keyframe>) -> Self {
2676        self.transition.get_or_insert(Transition::ms(200.0));
2677        self.anim_mut().keyframes = stops;
2678        self
2679    }
2680
2681    /// Runs this node's keyframe cycle `n` times and then rests where the
2682    /// last one ended (see the `iterations` field, backlog F133): a burst,
2683    /// a shake, a bounce that plays once. A count that is not a positive
2684    /// number is the default, for ever.
2685    ///
2686    /// ```
2687    /// # use kui_core::{Keyframe, NodeSpec};
2688    /// let pop = NodeSpec::column()
2689    ///     .transition(300.0)
2690    ///     .keyframes(vec![Keyframe::default().scale(1.0), Keyframe::default().scale(1.2)])
2691    ///     .iterations(1.0);
2692    /// assert_eq!(pop.anim().iterations, Some(1.0));
2693    /// ```
2694    pub fn iterations(mut self, n: f32) -> Self {
2695        self.anim_mut().iterations = (n.is_finite() && n > 0.0).then_some(n);
2696        self
2697    }
2698
2699    /// Eases this node in from `enter` on its first sight (see the `enter`
2700    /// field); sets a default 200ms transition if none was declared yet.
2701    pub fn enter(mut self, enter: Enter) -> Self {
2702        self.transition.get_or_insert(Transition::ms(200.0));
2703        self.anim_mut().enter = Some(enter);
2704        self
2705    }
2706
2707    /// Eases this node out to `exit` after the view stops declaring it
2708    /// (see the `exit` field); sets a default 200ms transition if none was
2709    /// declared yet.
2710    pub fn exit(mut self, exit: Enter) -> Self {
2711        self.transition.get_or_insert(Transition::ms(200.0));
2712        self.anim_mut().exit = Some(exit);
2713        self
2714    }
2715
2716    /// Pressing this node starts an OS window drag (drivers promote a quick
2717    /// second press to a maximize toggle).
2718    pub fn window_drag(mut self) -> Self {
2719        self.window = Some(WindowRole::Drag);
2720        self
2721    }
2722
2723    /// Clicking this node emits the button's `WindowCommand`.
2724    pub fn window_button(mut self, button: WindowButton) -> Self {
2725        self.window = Some(WindowRole::Button(button));
2726        self
2727    }
2728
2729    /// The pointer shape over this node (see the `cursor` field).
2730    pub fn cursor(mut self, shape: CursorShape) -> Self {
2731        self.cursor = Some(shape);
2732        self
2733    }
2734}
2735
2736/// Which face a text shapes with: one of the three stock families, or a
2737/// font registered with the core.
2738#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2739pub enum FontFamily {
2740    #[default]
2741    Sans,
2742    Serif,
2743    Mono,
2744    /// A font registered with the core (`Core::add_font_data` from file
2745    /// bytes, or `Core::add_system_font` by installed family name). A
2746    /// stale handle shapes as sans-serif. Not on the wire as a family:
2747    /// the `font` row carries the handle.
2748    Custom(crate::resources::FontId),
2749}
2750
2751impl FontFamily {
2752    /// The three stock families, in wire order: a binding sends the index.
2753    /// `Custom` is not a family on the wire; the `font` row is.
2754    pub const ALL: &'static [FontFamily] = &[FontFamily::Sans, FontFamily::Serif, FontFamily::Mono];
2755
2756    /// The spelling every binding uses; a registered font has none.
2757    pub fn name(self) -> Option<&'static str> {
2758        match self {
2759            FontFamily::Sans => Some("sans"),
2760            FontFamily::Serif => Some("serif"),
2761            FontFamily::Mono => Some("mono"),
2762            FontFamily::Custom(_) => None,
2763        }
2764    }
2765
2766    /// The variant `schema::FAMILIES` index `i` names; `Sans` for an
2767    /// index this build lacks.
2768    pub fn from_index(i: usize) -> FontFamily {
2769        Self::ALL.get(i).copied().unwrap_or_default()
2770    }
2771}
2772
2773/// How a text node breaks lines at its width.
2774#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2775pub enum TextWrap {
2776    /// Break between words; a word wider than the line breaks by glyph.
2777    #[default]
2778    Word,
2779    /// Break anywhere (URLs, hashes, code).
2780    Glyph,
2781    /// Never break at the width: one line per paragraph, clipped to the
2782    /// node's box (explicit newlines still break).
2783    None,
2784    /// `Word`, but whitespace takes its room like any glyph — CSS's
2785    /// `white-space: break-spaces`: a space that does not fit starts the
2786    /// next row instead of hanging past the edge or vanishing at the
2787    /// break, so every byte has a place inside the box. An editor's
2788    /// wrapped line, whose caret stands on each space. A text with line
2789    /// breaks of its own, a `max_lines` or an `ellipsis` wraps as `Word`.
2790    BreakSpaces,
2791}
2792
2793/// The OpenType features a style asks the shaper for: up to
2794/// [`FontFeatures::MAX`] four-letter tags with a value each. `liga` 0
2795/// keeps a coding font from joining `->`, `tnum` 1 gives tabular figures,
2796/// `ss01` 1 turns on a stylistic set. Plain data and `Copy`, since a
2797/// `TextStyle` is; the spelling every binding shares is
2798/// [`FontFeatures::parse`]'s.
2799///
2800/// ```rust
2801/// use kui_core::{FontFeatures, TextStyle};
2802/// let code = TextStyle::new(13.0).mono().features(FontFeatures::parse("-liga -calt tnum"));
2803/// assert_eq!(code.features.len(), 3);
2804/// assert_eq!(code.features.to_string_spelling(), "liga=0 calt=0 tnum=1");
2805/// ```
2806#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
2807pub struct FontFeatures {
2808    tags: [[u8; 4]; FontFeatures::MAX],
2809    values: [u32; FontFeatures::MAX],
2810    len: u8,
2811}
2812
2813impl FontFeatures {
2814    /// How many features a style can carry. Eight is more than any one
2815    /// text asks for and keeps the style small.
2816    pub const MAX: usize = 8;
2817
2818    /// No features: the font's own defaults.
2819    pub const fn new() -> Self {
2820        Self {
2821            tags: [[b' '; 4]; Self::MAX],
2822            values: [0; Self::MAX],
2823            len: 0,
2824        }
2825    }
2826
2827    /// Adds or replaces `tag` (four ASCII characters; shorter is padded
2828    /// with spaces, longer is cut) with `value`. Past `MAX` the feature is
2829    /// dropped rather than the style refused.
2830    pub fn set(mut self, tag: &str, value: u32) -> Self {
2831        let mut t = [b' '; 4];
2832        for (i, b) in tag.bytes().take(4).enumerate() {
2833            t[i] = b;
2834        }
2835        let n = self.len as usize;
2836        if let Some(i) = self.tags[..n].iter().position(|x| *x == t) {
2837            self.values[i] = value;
2838        } else if n < Self::MAX {
2839            self.tags[n] = t;
2840            self.values[n] = value;
2841            self.len += 1;
2842        }
2843        self
2844    }
2845
2846    /// The spelling every binding shares: tags separated by whitespace or
2847    /// commas, each `tag=value`, a bare `tag` meaning 1 and `-tag` meaning
2848    /// 0 — `"liga=0 calt=0 tnum"`, `"-liga,-calt"`. Anything else in the
2849    /// string is skipped.
2850    pub fn parse(s: &str) -> Self {
2851        let mut out = Self::new();
2852        for item in s.split(|c: char| c.is_whitespace() || c == ',') {
2853            if item.is_empty() {
2854                continue;
2855            }
2856            let (tag, value) = if let Some((t, v)) = item.split_once('=') {
2857                (t, v.trim().parse::<u32>().unwrap_or(0))
2858            } else if let Some(t) = item.strip_prefix('-') {
2859                (t, 0)
2860            } else if let Some(t) = item.strip_prefix('+') {
2861                (t, 1)
2862            } else {
2863                (item, 1)
2864            };
2865            let tag = tag.trim();
2866            if !tag.is_empty() {
2867                out = out.set(tag, value);
2868            }
2869        }
2870        out
2871    }
2872
2873    pub fn is_empty(&self) -> bool {
2874        self.len == 0
2875    }
2876
2877    pub fn len(&self) -> usize {
2878        self.len as usize
2879    }
2880
2881    /// Every feature, in the order set, as `(tag, value)`.
2882    pub fn iter(&self) -> impl Iterator<Item = (&[u8; 4], u32)> + '_ {
2883        let n = self.len as usize;
2884        self.tags[..n].iter().zip(self.values[..n].iter().copied())
2885    }
2886
2887    /// What `parse` reads: `tag=value` pairs joined by spaces.
2888    pub fn to_string_spelling(&self) -> String {
2889        self.iter()
2890            .map(|(t, v)| format!("{}={}", String::from_utf8_lossy(t).trim_end(), v))
2891            .collect::<Vec<_>>()
2892            .join(" ")
2893    }
2894}
2895
2896/// How a run of text is shaped and painted: size, line height, colour,
2897/// family, wrapping, line limits, OpenType features and decorations.
2898///
2899/// [`TextStyle::new`] takes the font size and derives a line height; the
2900/// rest are builders. A style with no colour paints in the theme's
2901/// foreground, so plain text is legible on both the light and dark base.
2902///
2903/// ```rust
2904/// use kui_core::{Color, FontFamily, TextStyle, TextWrap};
2905///
2906/// let body = TextStyle::new(15.0).line_height(22.0);
2907/// let caption = TextStyle::new(12.0).color(Color::hex(0x8a8fa3ff)).max_lines(2).ellipsis();
2908/// let code = TextStyle::new(13.0).family(FontFamily::Mono).wrap(TextWrap::None);
2909/// let link = TextStyle::new(15.0).underline().color(Color::hex(0x3b5bd4ff));
2910///
2911/// assert_eq!(body.color, None); // the theme's foreground
2912/// assert_eq!(caption.max_lines, 2);
2913/// assert!(code.wrap == TextWrap::None && link.underline);
2914/// ```
2915#[derive(Clone, Copy, Debug, PartialEq)]
2916pub struct TextStyle {
2917    pub size: f32,
2918    pub line_height: f32,
2919    /// `None` is the theme's foreground, filled in as the text enters the
2920    /// tree so nothing downstream of that ever sees a `None`.
2921    pub color: Option<Color>,
2922    pub family: FontFamily,
2923    pub wrap: TextWrap,
2924    /// At most this many lines are laid out; 0 = unlimited.
2925    pub max_lines: u32,
2926    /// End the last line with "…" when the text was cut off. Alone it
2927    /// means a single line (`max_lines` 1); with `max_lines` it clamps.
2928    pub ellipsis: bool,
2929    /// OpenType features for the shaper; none by default, which is the
2930    /// font's own defaults (ligatures on, where it has them).
2931    pub features: FontFeatures,
2932    /// A line under every glyph, where the face puts its underline. Paint
2933    /// only: not part of what the text is shaped as.
2934    pub underline: bool,
2935    /// The underline's own colour; `None` is the text's.
2936    pub underline_color: Option<Color>,
2937    /// The underline's shape: a line, a wave, dots.
2938    pub underline_style: UnderlineStyle,
2939    /// A line through every glyph, where the face puts its strikeout.
2940    pub strikethrough: bool,
2941    /// The family's bold, as a span's `bold` is (backlog F150): its bold
2942    /// face, or its regular drawn bold where it has none (backlog F100).
2943    /// A span inside is bold too; it cannot be lighter than its text.
2944    pub bold: bool,
2945    /// The first letter as an initial — a drop cap — this many lines
2946    /// tall (backlog F164): its top level with the first line's capitals,
2947    /// its foot on the last one's baseline, the lines beside it moved in
2948    /// past it and the ones below at full width again, as CSS's
2949    /// `initial-letter`. The letter is the text's first, with any opening
2950    /// punctuation before it; it keeps its span's face and colour and is
2951    /// sized to the drop. Still one text: a caret, a hit, a selection and
2952    /// the access tree read it where it is drawn. 0 and 1 are none; a text
2953    /// with a `max_lines` or an `ellipsis`, a right-to-left one and an
2954    /// editor have none.
2955    pub initial_letter: u32,
2956}
2957
2958/// The shape of an underline: the face's line, a wave under a diagnostic,
2959/// dots. Where it goes and how thick it is are the face's
2960/// recommendation either way; a wave is three strokes tall around the
2961/// line's centre with a six-stroke period, dots two strokes across and
2962/// four apart (`crate::deco`).
2963#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2964pub enum UnderlineStyle {
2965    #[default]
2966    Solid,
2967    Wavy,
2968    Dotted,
2969}
2970
2971impl UnderlineStyle {
2972    /// The spellings, in discriminant order (`underlineStyle` /
2973    /// `underline_style`; C's `KUI_UNDERLINE_*`).
2974    pub const NAMES: &[&str] = &["solid", "wavy", "dotted"];
2975
2976    pub fn from_index(i: u32) -> Self {
2977        match i {
2978            1 => Self::Wavy,
2979            2 => Self::Dotted,
2980            _ => Self::Solid,
2981        }
2982    }
2983
2984    pub fn name(self) -> &'static str {
2985        Self::NAMES[self as usize]
2986    }
2987}
2988
2989impl Default for TextStyle {
2990    fn default() -> Self {
2991        Self::new(16.0)
2992    }
2993}
2994
2995impl TextStyle {
2996    /// A style at `size` logical px, sans, in the theme's foreground, with
2997    /// a line height of 1.35 times the size, rounded.
2998    pub fn new(size: f32) -> Self {
2999        Self {
3000            size,
3001            line_height: (size * 1.35).round(),
3002            color: None,
3003            family: FontFamily::Sans,
3004            wrap: TextWrap::Word,
3005            max_lines: 0,
3006            ellipsis: false,
3007            features: FontFeatures::new(),
3008            underline: false,
3009            underline_color: None,
3010            underline_style: UnderlineStyle::Solid,
3011            strikethrough: false,
3012            bold: false,
3013            initial_letter: 0,
3014        }
3015    }
3016
3017    /// Underlines the text in its own colour.
3018    pub fn underline(mut self) -> Self {
3019        self.underline = true;
3020        self
3021    }
3022
3023    /// An underline in its own colour rather than the text's (a
3024    /// diagnostic's red under keyword-coloured text). Turns the underline
3025    /// on.
3026    pub fn underline_color(mut self, c: Color) -> Self {
3027        self.underline = true;
3028        self.underline_color = Some(c);
3029        self
3030    }
3031
3032    /// An underline of this shape. Turns the underline on.
3033    pub fn underline_style(mut self, s: UnderlineStyle) -> Self {
3034        self.underline = true;
3035        self.underline_style = s;
3036        self
3037    }
3038
3039    /// A line through the text.
3040    pub fn strikethrough(mut self) -> Self {
3041        self.strikethrough = true;
3042        self
3043    }
3044
3045    /// The family's bold (backlog F150).
3046    pub fn bold(mut self) -> Self {
3047        self.bold = true;
3048        self
3049    }
3050
3051    /// The first letter dropped through `lines` lines, an initial
3052    /// (backlog F164): see the field of that name.
3053    pub fn initial_letter(mut self, lines: u32) -> Self {
3054        self.initial_letter = lines;
3055        self
3056    }
3057
3058    /// OpenType features for the shaper; see [`FontFeatures`].
3059    pub fn features(mut self, f: FontFeatures) -> Self {
3060        self.features = f;
3061        self
3062    }
3063
3064    /// The face to shape with.
3065    pub fn family(mut self, f: FontFamily) -> Self {
3066        self.family = f;
3067        self
3068    }
3069
3070    /// The stock monospace family.
3071    pub fn mono(self) -> Self {
3072        self.family(FontFamily::Mono)
3073    }
3074
3075    /// Shape with a registered font (see `FontFamily::Custom`).
3076    pub fn font(self, id: crate::resources::FontId) -> Self {
3077        self.family(FontFamily::Custom(id))
3078    }
3079
3080    /// The line height in logical px, replacing the derived one.
3081    pub fn line_height(mut self, lh: f32) -> Self {
3082        self.line_height = lh;
3083        self
3084    }
3085
3086    /// How lines break at the node's width.
3087    pub fn wrap(mut self, wrap: TextWrap) -> Self {
3088        self.wrap = wrap;
3089        self
3090    }
3091
3092    /// One line per paragraph, clipped to the node (`TextWrap::None`).
3093    pub fn nowrap(self) -> Self {
3094        self.wrap(TextWrap::None)
3095    }
3096
3097    /// Lay out at most `n` lines (0 = unlimited).
3098    pub fn max_lines(mut self, n: u32) -> Self {
3099        self.max_lines = n;
3100        self
3101    }
3102
3103    /// Truncate with "…" instead of overflowing: a single line unless
3104    /// `max_lines` says otherwise.
3105    pub fn ellipsis(mut self) -> Self {
3106        self.ellipsis = true;
3107        self
3108    }
3109
3110    /// The text colour, instead of the theme's foreground.
3111    pub fn color(mut self, c: Color) -> Self {
3112        self.color = Some(c);
3113        self
3114    }
3115
3116    /// This style with `fg` where it named no colour of its own: what the
3117    /// core stamps on as the text enters the tree, so the shaping caches,
3118    /// the display list and every binding see a resolved colour.
3119    pub fn or_fg(mut self, fg: Color) -> Self {
3120        self.color = Some(self.color.unwrap_or(fg));
3121        self
3122    }
3123
3124    /// The colour this style paints in, falling back to what kui painted
3125    /// before there were themes. For the handful of places that hold a
3126    /// style the core never stamped.
3127    pub fn color_or_default(&self) -> Color {
3128        self.color.unwrap_or(crate::theme::Theme::DEFAULT_FG)
3129    }
3130}
3131
3132/// Hand-written so that a group whose box was allocated but left at its
3133/// defaults — `.checked(false)` on a fresh spec — equals one whose box was
3134/// never allocated at all. The derived version compared `None` against
3135/// `Some(EMPTY)` and called them different, which is a difference the boxing
3136/// introduced rather than one a caller declared.
3137impl PartialEq for NodeSpec {
3138    fn eq(&self, other: &Self) -> bool {
3139        self.layout == other.layout
3140            && self.style == other.style
3141            && self.hoverable == other.hoverable
3142            && self.animate == other.animate
3143            && self.accent == other.accent
3144            && self.window == other.window
3145            && self.transition == other.transition
3146            && self.slide == other.slide
3147            && self.over_siblings == other.over_siblings
3148            && self.focusable == other.focusable
3149            && self.initial_focus == other.initial_focus
3150            && self.disabled == other.disabled
3151            && self.cursor == other.cursor
3152            && self.events() == other.events()
3153            && self.anim() == other.anim()
3154            && self.access() == other.access()
3155            && self.interact() == other.interact()
3156    }
3157}
3158
3159#[cfg(test)]
3160mod size_tests {
3161    use super::*;
3162
3163    /// `NodeSpec` is moved by value for every node a frame builds — through
3164    /// the builder chain, `Core::open`, `open_with_key` and into
3165    /// `Vec<NodeSpec>` in `Tree::push` — so its size is a per-node cost that
3166    /// every app pays whether or not it declares the fields. It reached 728
3167    /// bytes one feature at a time and cost ~2.5x on the frame benches before
3168    /// anyone measured it.
3169    ///
3170    /// This is the number a review can fail. Adding a prop is fine; adding it
3171    /// *inline* past this bound is the thing to notice. Put cold fields in one
3172    /// of the boxed groups instead, and only raise this if the field is read
3173    /// on every node of every frame.
3174    #[test]
3175    fn node_spec_stays_small() {
3176        const BOUND: usize = 256;
3177        let size = std::mem::size_of::<NodeSpec>();
3178        assert!(
3179            size <= BOUND,
3180            "NodeSpec is {size} bytes, over the {BOUND}-byte bound. It is copied \
3181             per node per frame; put cold fields in EventSpec / AnimSpec / \
3182             AccessSpec / InteractSpec rather than inline. See C15."
3183        );
3184    }
3185
3186    /// The groups exist to be absent: a node declaring none of them carries
3187    /// four null pointers, not four structs.
3188    #[test]
3189    fn an_undeclared_group_costs_a_pointer() {
3190        let spec = NodeSpec::default();
3191        assert!(spec.events.is_none() && spec.anim.is_none());
3192        assert!(spec.access.is_none() && spec.interact.is_none());
3193        // and reads still work, without allocating
3194        assert!(spec.events().on_click.is_none());
3195        assert_eq!(spec.access().role, None);
3196    }
3197
3198    /// An allocated-but-default group is still equal to no group at all.
3199    #[test]
3200    fn an_empty_group_equals_no_group() {
3201        assert_eq!(NodeSpec::default().checked(false), NodeSpec::default());
3202    }
3203}