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