Skip to main content

kui_core/
widgets.rs

1//! Stock widgets built from the primitives: buttons, toggles, text input,
2//! select, slider, splitter, tooltips, menus, a titlebar and virtual lists.
3//!
4//! Every widget here is a plain function over a [`Ui`] that opens ordinary
5//! nodes with ordinary [`NodeSpec`]s; there is no widget trait and no
6//! retained object. State lives in the core by key (focus, hover, an edit
7//! buffer, a scroll offset), and the app's model is the only other state.
8//! A custom widget follows the same pattern, and the `*_spec` functions
9//! ([`button_spec`], [`toggle_spec`], [`slider_spec`], [`menu_panel_spec`])
10//! are the starting points for one that should look like the stock set.
11//!
12//! ```rust
13//! use kui_core::{Core, NodeSpec, Size, Value, widgets};
14//!
15//! let mut core = Core::new();
16//! let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
17//! ui.configure_root(NodeSpec::column().fill().pad(12.0).gap(8.0));
18//!
19//! widgets::label(&mut ui, "Settings");
20//! let name = widgets::text_input(&mut ui, "name", "Ada");
21//! widgets::checkbox(&mut ui, "Dark mode", true, "toggle-dark");
22//! widgets::slider(&mut ui, "volume", 40.0, 0.0, 100.0, 1.0, "volume");
23//! widgets::button(&mut ui, "Save", Value::str("save"));
24//!
25//! assert_eq!(ui.edit_text(name).as_deref(), Some("Ada"));
26//! ui.finish();
27//! ```
28//!
29//! Each control posts the payload it was given as a
30//! [`UiEvent`](crate::input::UiEvent) when it is used, and the view redraws
31//! from its model; a checkbox does not flip itself.
32
33use crate::access::Role;
34use crate::color::Color;
35use crate::cursor::CursorShape;
36use crate::edit::EditOptions;
37use crate::geom::{Edges, Vec2};
38use crate::key::Key;
39use crate::menu::{MenuBar, MenuItem, MenuRole};
40use crate::metrics::Metrics;
41use crate::spec::{Align, FloatConfig, NodeSpec, Sizing, TextStyle};
42use crate::stats::{FrameSample, STATS_CAPACITY};
43use crate::theme::Theme;
44use crate::tree::OriginId;
45use crate::ui::Ui;
46use crate::value::Value;
47use crate::window::WindowButton;
48
49/// Floating latency HUD: `latency_graph` in a translucent panel pinned to a
50/// viewport corner, above all content and out of layout flow. Call anywhere
51/// in the view; pick the corner with `latency_hud_at`.
52pub fn latency_hud(ui: &mut Ui<'_>) {
53    latency_hud_at(ui, Align::End, Align::End);
54}
55
56pub fn latency_hud_at(ui: &mut Ui<'_>, x: Align, y: Align) {
57    // Under custom chrome the top of the viewport is the app's titlebar;
58    // keep the HUD below it.
59    let top_inset = if ui.env().window.custom_chrome {
60        titlebar_height(ui)
61    } else {
62        0.0
63    };
64    // The same attach points a float takes, which place the spreads and
65    // `Baseline` as the start or the centre (`layout::align_factor`).
66    let dx = match x {
67        Align::Start | Align::SpaceBetween | Align::Baseline => 12.0,
68        Align::Center | Align::SpaceAround | Align::SpaceEvenly => 0.0,
69        Align::End => -12.0,
70    };
71    let dy = match y {
72        Align::Start | Align::SpaceBetween | Align::Baseline => 12.0 + top_inset,
73        Align::Center | Align::SpaceAround | Align::SpaceEvenly => 0.0,
74        Align::End => -12.0,
75    };
76    // Translucent over whatever the app is painting, so the panel takes
77    // the theme's backmost surface and its strong border at the alphas
78    // the HUD has always used.
79    let t = ui.theme();
80    ui.with(
81        NodeSpec::column()
82            .float(
83                crate::spec::FloatConfig::viewport()
84                    .inside(x, y)
85                    .offset(dx, dy),
86            )
87            .pad(10.0)
88            .bg(t.bg.with_alpha(0.71))
89            .radius(8.0)
90            .border(1.0, t.border_strong.with_alpha(0.5)),
91        latency_graph,
92    );
93}
94
95/// Frame-latency graph: the last ~120 frames as stacked per-phase bars
96/// (input / view / layout / render, bottom to top) against the display's
97/// frame budget (`env.refresh_hz`, 120 Hz fallback) — a bar that blows the
98/// budget turns red. Feed `core.stats` (and `core.env`) from your frame
99/// driver (the built-in runner does this automatically).
100pub fn latency_graph(ui: &mut Ui<'_>) {
101    const GRAPH_H: f32 = 34.0;
102    const INPUT: Color = Color {
103        r: 0.45,
104        g: 0.85,
105        b: 0.55,
106        a: 1.0,
107    };
108    const VIEW: Color = Color {
109        r: 0.28,
110        g: 0.42,
111        b: 0.88,
112        a: 1.0,
113    };
114    const LAYOUT: Color = Color {
115        r: 0.60,
116        g: 0.42,
117        b: 0.88,
118        a: 1.0,
119    };
120    const RENDER: Color = Color {
121        r: 0.94,
122        g: 0.72,
123        b: 0.35,
124        a: 1.0,
125    };
126    const WAIT: Color = Color {
127        r: 0.42,
128        g: 0.45,
129        b: 0.52,
130        a: 0.7,
131    };
132    const OVER: Color = Color {
133        r: 0.91,
134        g: 0.36,
135        b: 0.36,
136        a: 1.0,
137    };
138
139    let theme = ui.theme();
140    let budget_ms = ui.env().frame_budget_ms(); // full graph height
141    let stats = &ui.core().stats;
142    let samples: Vec<FrameSample> = stats.iter().collect();
143    let (avg_work, max_work) = (stats.avg_work(), stats.max_work());
144    let avg_wait = if samples.is_empty() {
145        0.0
146    } else {
147        samples.iter().map(|s| s.wait_ms).sum::<f32>() / samples.len() as f32
148    };
149
150    // A development overlay, not app content: kept out of the access tree
151    // so a screen reader does not read frame timings between the controls.
152    ui.with(
153        NodeSpec::column()
154            .gap(3.0)
155            .cross_align(Align::End)
156            .role(crate::access::Role::None),
157        |ui| {
158            let mut label = format!("work {avg_work:.2}ms avg · {max_work:.2}ms max");
159            if avg_wait > 0.05 {
160                label.push_str(&format!(" · +{avg_wait:.2}ms vsync"));
161            }
162            ui.with(NodeSpec::row().gap(6.0).cross_align(Align::Center), |ui| {
163                ui.text(&label, TextStyle::new(10.0).color(theme.muted));
164                // "?" badge: hover for the color legend. Also the dynamic-float
165                // showcase — in the default bottom-right HUD the tooltip has no
166                // room below or to the right, so it flips above and slides left.
167                let badge = ui.child_key("kui:latency-legend");
168                let badge_bg =
169                    theme
170                        .muted
171                        .with_alpha(if ui.is_hovered(badge) { 0.31 } else { 0.16 });
172                ui.with_keyed(
173                    "kui:latency-legend",
174                    NodeSpec::column()
175                        .size(13.0, 13.0)
176                        .center()
177                        .bg(badge_bg)
178                        .radius(6.5)
179                        .hoverable(),
180                    |ui| {
181                        ui.text("?", TextStyle::new(9.0).color(theme.fg));
182                        if ui.is_hovered(badge) {
183                            tooltip_with(ui, |ui| {
184                                ui.with(NodeSpec::column().gap(5.0), |ui| {
185                                    for (color, name) in [
186                                        (INPUT, "input — events & edits"),
187                                        (VIEW, "view — rebuilding the tree"),
188                                        (LAYOUT, "layout — sizing & positions"),
189                                        (RENDER, "render — encode + submit"),
190                                        (WAIT, "vsync wait (not work)"),
191                                        (OVER, "cap: work over frame budget"),
192                                    ] {
193                                        ui.with(
194                                            NodeSpec::row().gap(7.0).cross_align(Align::Center),
195                                            |ui| {
196                                                ui.leaf(
197                                                    NodeSpec::column()
198                                                        .size(9.0, 9.0)
199                                                        .bg(color)
200                                                        .radius(2.0),
201                                                );
202                                                ui.text(name, TextStyle::new(11.0).color(theme.fg));
203                                            },
204                                        );
205                                    }
206                                });
207                            });
208                        }
209                    },
210                );
211            });
212            ui.with(
213                NodeSpec::row()
214                    .size(STATS_CAPACITY as f32 * 2.0, GRAPH_H)
215                    .gap(1.0)
216                    .main_align(Align::End)
217                    .cross_align(Align::End)
218                    .bg(theme.sunken.with_alpha(0.6))
219                    .radius(3.0)
220                    .clip(),
221                |ui| {
222                    let px_per_ms = GRAPH_H / budget_ms;
223                    for s in &samples {
224                        // Phases keep their colors even over budget — a spike
225                        // you can't attribute is a spike you can't fix. Work
226                        // (not vsync pacing) over budget gets a red cap.
227                        let over = s.work() > budget_ms;
228                        ui.with(
229                            NodeSpec::column()
230                                .width(1.0)
231                                .main_align(Align::End)
232                                .max_height(GRAPH_H),
233                            |ui| {
234                                if over {
235                                    ui.leaf(NodeSpec::column().size(1.0, 3.0).bg(OVER));
236                                }
237                                // Column children run top->bottom; push in
238                                // reverse so input sits at the bottom.
239                                for (ms, color) in [
240                                    (s.wait_ms, WAIT),
241                                    (s.render_ms, RENDER),
242                                    (s.layout_ms, LAYOUT),
243                                    (s.view_ms, VIEW),
244                                    (s.input_ms, INPUT),
245                                ] {
246                                    if ms <= 0.0 {
247                                        continue;
248                                    }
249                                    let h = (ms * px_per_ms).max(1.0);
250                                    ui.leaf(NodeSpec::column().size(1.0, h).bg(color));
251                                }
252                            },
253                        );
254                    }
255                },
256            );
257        },
258    );
259}
260
261/// Small floating label hanging below the node it's declared inside. The
262/// placement is dynamic (`FloatConfig::fit`): it flips above when the
263/// viewport bottom is too close and slides sideways off window edges.
264/// Typical use: `if ui.is_hovered(key) { widgets::tooltip(ui, "..."); }`
265pub fn tooltip(ui: &mut Ui<'_>, text: &str) {
266    let size = ui.metrics().hint_text;
267    tooltip_with(ui, |ui| {
268        ui.text(text, TextStyle::new(size));
269    });
270}
271
272/// [`tooltip`] chrome around arbitrary content (legends, shortcut hints, …).
273pub fn tooltip_with(ui: &mut Ui<'_>, content: impl FnOnce(&mut Ui<'_>)) {
274    let spec = tooltip_spec(ui);
275    ui.with(spec, content);
276}
277
278/// The hint the `tooltip` prop floats under a hovered node, and the stock
279/// button under a hovered button: [`tooltip`]'s chrome and text, kept out
280/// of the access tree (`Role::None`). The prop has already set the same
281/// string as the node's description, which is where a reader hears it;
282/// as content it would be read twice under a group and, under a control
283/// named from its content, become part of the *name* whenever the pointer
284/// crossed it. The `tooltip` element keeps its text, since
285/// it is drawn with no description behind it.
286pub(crate) fn hover_hint(ui: &mut Ui<'_>, text: &str) {
287    let size = ui.metrics().hint_text;
288    let spec = tooltip_spec(ui).role(crate::access::Role::None);
289    ui.text_in(spec, text, TextStyle::new(size));
290}
291
292/// [`hover_hint`] for a leaf — a `line`, a `polygon`, a `path`, a `cells`
293/// grid, an image, an editor — which holds no children for the hint to
294/// float as the last of (backlog RG113). The hint is opened beside the
295/// leaf, in the leaf's parent, and anchored to the leaf by key
296/// (`FloatAnchor::Node`), so it is laid out against the leaf's box once
297/// that is placed and lands below it as a box's lands below the box. Its
298/// key is the leaf's own child key: an auto-keyed one would take the
299/// parent's next sibling slot while the pointer is over the leaf and
300/// shift the key of every sibling declared after it.
301pub(crate) fn leaf_hint(ui: &mut Ui<'_>, leaf: Key, text: &str) {
302    let size = ui.metrics().hint_text;
303    let mut spec = tooltip_spec(ui).role(crate::access::Role::None);
304    if let Some(float) = spec.layout.float.as_mut() {
305        float.anchor = crate::spec::FloatAnchor::Node(leaf);
306    }
307    ui.core().open_key(leaf.str("tooltip"), spec);
308    ui.text(text, TextStyle::new(size));
309    ui.close();
310}
311
312fn tooltip_spec(ui: &Ui<'_>) -> NodeSpec {
313    let t = ui.theme();
314    let m = ui.metrics();
315    NodeSpec::column()
316        .float(crate::spec::FloatConfig::below().fit())
317        .pad_xy(m.hint_pad_x, m.hint_pad_y)
318        .bg(t.raised)
319        .radius(m.radius)
320        .border(1.0, t.border_strong)
321}
322
323/// A line of text in the default style: `ui.text(text, TextStyle::default())`.
324pub fn label(ui: &mut Ui<'_>, text: &str) {
325    ui.text(text, TextStyle::default());
326}
327
328/// Single-line text input with chrome (background, focus ring).
329/// Read the value with `ui.edit_text(key)`; "changed"/"submit" events arrive
330/// in `on_event` with this key. The `label` is the key and the accessible
331/// name both (`"search"`, `"name"`), so a screen reader has something to
332/// announce; use `ui.text_edit` with `NodeSpec::label` when they differ.
333pub fn text_input(ui: &mut Ui<'_>, label: &str, initial: &str) -> Key {
334    let key = ui.child_key(label);
335    let t = ui.theme();
336    let m = ui.metrics();
337    let border = if ui.is_focused(key) {
338        t.accent
339    } else {
340        t.border
341    };
342    ui.text_edit(
343        label,
344        initial,
345        &EditOptions {
346            multiline: false,
347            ..Default::default()
348        },
349        NodeSpec::column()
350            .grow_width()
351            .pad_xy(m.field_pad_x, m.field_pad_y)
352            .bg(t.sunken)
353            .radius(m.radius)
354            .border(1.0, border)
355            .clip()
356            .label(label),
357    )
358}
359
360/// What a select's trigger posts when it is clicked; the core takes it
361/// back and opens the menu (`Core::consume_select_events`).
362pub(crate) fn select_tag() -> Value {
363    Value::map([("select", Value::Bool(true))])
364}
365
366/// A choice among a few named options: a field that shows the one in
367/// force and, clicked, drops a menu of them all with the current one
368/// checked. `options` are the labels, `current` the index in force (or
369/// none). Keyed by `label`, which is the accessible name too.
370///
371/// The menu is the core's own — the same one a right-click opens
372/// (`Core::open_menu`): drawn in the frame, or the platform's where the
373/// host shows menus itself, dismissed by Escape or a press outside, its
374/// rows walked by the arrows. So the app holds no open state; what it
375/// hears is the choice, as the `menu` event a menu row posts, on this
376/// key: `{kind: "menu", role: "custom", item: <the option>}`. A view
377/// that then draws the select with the new `current` is the whole loop.
378///
379/// [`select_items`] is the same field over [`MenuItem`]s, for an option
380/// that posts an `id` of its own rather than its label.
381pub fn select(ui: &mut Ui<'_>, label: &str, options: &[&str], current: Option<usize>) -> Key {
382    let items: Vec<MenuItem> = options.iter().map(|o| MenuItem::new(*o)).collect();
383    select_items(ui, label, &items, current)
384}
385
386/// [`select`] over items the caller built: their labels are the rows,
387/// their `id`s what a choice posts, and the `current`th is drawn checked
388/// whatever the item said. A separator is a separator here too.
389pub fn select_items(
390    ui: &mut Ui<'_>,
391    label: &str,
392    items: &[MenuItem],
393    current: Option<usize>,
394) -> Key {
395    let t = ui.theme();
396    let m = ui.metrics();
397    select_with(
398        ui,
399        label,
400        items,
401        current,
402        select_spec(&t, &m),
403        TextStyle::new(m.chrome_text),
404    )
405}
406
407/// The stock select field's spec: a sunken field with the stock radius
408/// and padding, as [`button_spec`] is the stock button's. What
409/// [`select_with`] is handed by [`select_items`]; a caller with a spec
410/// of its own starts here and adds to it.
411pub fn select_spec(theme: &Theme, m: &Metrics) -> NodeSpec {
412    NodeSpec::row()
413        .pad_xy(m.field_pad_x, m.field_pad_y)
414        .gap(8.0)
415        .cross_align(Align::Center)
416        .bg(theme.sunken)
417        .hover_bg(theme.hover)
418        .radius(m.radius)
419}
420
421/// [`select_items`] with its spec and text style in the caller's hands —
422/// a compact field in a dense panel — the way [`button_with`] takes the
423/// button's. The border, the click, the role and the disclosure are
424/// added here whatever `spec` said.
425///
426/// A `current` that names no option (past the end, or a separator) is
427/// none, with a `select-current-ignored` warning on the field: the field
428/// is described by nothing and no row is checked.
429pub fn select_with(
430    ui: &mut Ui<'_>,
431    label: &str,
432    items: &[MenuItem],
433    current: Option<usize>,
434    spec: NodeSpec,
435    text: TextStyle,
436) -> Key {
437    let key = ui.child_key(label);
438    let current = current.filter(|&i| {
439        let separator = items
440            .get(i)
441            .is_some_and(|it| it.role == MenuRole::Separator);
442        let names_one = i < items.len() && !separator;
443        if !names_one {
444            ui.core().warn(crate::diag::select_current_ignored(
445                key,
446                label,
447                i,
448                items.len(),
449                separator,
450            ));
451        }
452        names_one
453    });
454    let t = ui.theme();
455    let shown = current
456        .and_then(|i| items.get(i))
457        .map_or("", |i| i.text())
458        .to_string();
459    let open = ui.core().menu().is_some_and(|menu| menu.target == key);
460    let border = if open || ui.is_focused(key) {
461        t.accent
462    } else {
463        t.border
464    };
465    let menu: Vec<MenuItem> = items
466        .iter()
467        .enumerate()
468        .map(|(i, item)| item.clone().checked(current == Some(i)))
469        .collect();
470    ui.core().declare_select(key, menu);
471    ui.with_keyed(
472        label,
473        spec.border(1.0, border)
474            .cursor(CursorShape::Pointer)
475            .on_click(select_tag())
476            // A button named by the field, described by the choice: what
477            // a reader says of a pop-up button, in the two slots a button
478            // has (`value` is a slider's and an editor's).
479            .role(Role::Button)
480            .label(label)
481            .description(shown.as_str())
482            .expanded(open),
483        |ui| {
484            ui.text(&shown, text.color(t.fg).nowrap());
485            // The disclosure: a small triangle, the mark every platform's
486            // pop-up field carries.
487            ui.text("\u{25BE}", text.color(t.muted));
488        },
489    )
490}
491
492/// Default titlebar height, logical px, where the strip is the app's
493/// alone. Follows platform conventions (as measured by gpui): 32 on
494/// Windows (the native caption height), 34 elsewhere. The stock
495/// [`Metrics`] carries the same number as `titlebar_h`, and the titlebar
496/// draws from *that*, so an app that set its own metrics lays out against
497/// `ui.metrics().titlebar_h` rather than this constant — and where the OS
498/// keeps controls of its own over the strip, against [`titlebar_height`].
499pub const TITLEBAR_H: f32 = Metrics::comfortable().titlebar_h;
500
501/// The height the titlebar strip draws at — what an app laying out its
502/// own strip, or something under it, should read instead of
503/// `ui.metrics().titlebar_h`. Where the OS keeps controls of its own over
504/// the strip (`env.window.native_controls`: the macOS traffic lights under
505/// custom chrome) the strip is the OS's own titlebar, as tall as the
506/// keep-out rect says that titlebar is, so the strip's content centres on
507/// the buttons the OS centred in it. Everywhere else the strip is the app's alone and
508/// `Metrics::titlebar_h` is its height. A keep-out with no height (a host
509/// that reported a width only) falls back to the metric.
510pub fn titlebar_height(ui: &Ui<'_>) -> f32 {
511    match ui.env().window.native_controls {
512        Some(r) if r.h > 0.0 => r.h,
513        _ => ui.metrics().titlebar_h,
514    }
515}
516
517/// A cross-platform titlebar: a full-width drag strip with the window title
518/// left-aligned next to the window controls. Reads `env.window` and adapts
519/// by itself — under macOS custom chrome it insets past the native traffic
520/// lights and draws no buttons; under custom chrome elsewhere it appends
521/// minimize/maximize/close; under native decorations it is just a drag
522/// strip (no duplicate buttons).
523///
524/// Typical use, as the first child of a full-height root:
525/// `widgets::titlebar(ui, "my app")`.
526pub fn titlebar(ui: &mut Ui<'_>, title: &str) {
527    let focused = ui.env().focused;
528    let title = title.to_string();
529    titlebar_with(ui, move |ui| {
530        // A background window's title recedes; the OS does the same.
531        let t = ui.theme();
532        let size = ui.metrics().chrome_text;
533        let color = if focused { t.fg } else { t.faint };
534        ui.text_in(
535            NodeSpec::row().fill().cross_align(Align::Center),
536            &title,
537            TextStyle::new(size).color(color).ellipsis(),
538        );
539    });
540}
541
542/// Titlebar with custom content (tabs, a search box, …) between the
543/// platform inset and the window buttons. The whole strip is a drag
544/// handle; interactive children declared inside it sit on top and win
545/// hit-testing, so buttons in a titlebar just work.
546pub fn titlebar_with(ui: &mut Ui<'_>, content: impl FnOnce(&mut Ui<'_>)) {
547    let win = ui.env().window;
548    let h = titlebar_height(ui);
549    ui.with_keyed(
550        "kui:titlebar",
551        NodeSpec::row()
552            .grow_width()
553            .height(h)
554            .cross_align(Align::Center)
555            .window_drag(),
556        |ui| {
557            // Keep clear of controls the OS draws over our content (the
558            // reported rect already includes the trailing gap); without
559            // them, a plain leading margin.
560            let inset = win.native_controls.map_or(12.0, |r| r.x + r.w);
561            ui.leaf(NodeSpec::row().width(inset));
562            content(ui);
563            window_buttons(ui);
564        },
565    );
566}
567
568/// The minimize/maximize/close cluster. Renders nothing when the OS already
569/// provides controls (native decorations, or macOS traffic lights), so it
570/// is always safe to call. It grows to the height it is given — the
571/// strip's, in [`titlebar_with`] — and is a titlebar tall where nothing
572/// gives it one, since a grow child adds nothing to a fit parent's
573/// height.
574pub fn window_buttons(ui: &mut Ui<'_>) {
575    let win = ui.env().window;
576    if !win.custom_chrome || win.native_controls.is_some() {
577        return;
578    }
579    let h = titlebar_height(ui);
580    ui.with(NodeSpec::row().grow_height().min_height(h), |ui| {
581        window_button(ui, WindowButton::Minimize, win.maximized);
582        window_button(ui, WindowButton::Maximize, win.maximized);
583        window_button(ui, WindowButton::Close, win.maximized);
584    });
585}
586
587fn window_button(ui: &mut Ui<'_>, button: WindowButton, maximized: bool) {
588    let label = match button {
589        WindowButton::Minimize => "kui:win-min",
590        WindowButton::Maximize => "kui:win-max",
591        WindowButton::Close => "kui:win-close",
592    };
593    let key = ui.child_key(label);
594    let (hovered, pressed) = (ui.is_hovered(key), ui.is_pressed(key));
595    let t = ui.theme();
596    let fg = t.fg;
597    // Close is the one button that keeps a colour of its own on both
598    // bases — it is the platform's signal, not the palette's — but it is
599    // the theme's danger rather than a second red.
600    let (bg, fg) = match button {
601        WindowButton::Close if pressed => (t.danger.mix(Color::BLACK, 0.15), Color::WHITE),
602        WindowButton::Close if hovered => (t.danger, Color::WHITE),
603        _ if pressed => (t.pressed, fg),
604        _ if hovered => (t.hover, fg),
605        _ => (Color::TRANSPARENT, fg),
606    };
607    ui.with_keyed(
608        label,
609        NodeSpec::row()
610            .width(46.0)
611            .grow_height()
612            .center()
613            .bg(bg)
614            .window_button(button),
615        |ui| match button {
616            WindowButton::Minimize => {
617                ui.leaf(NodeSpec::row().size(10.0, 1.0).bg(fg));
618            }
619            WindowButton::Maximize if maximized => {
620                // Restore: two offset outlines.
621                ui.with(NodeSpec::column().size(10.0, 10.0), |ui| {
622                    for (x, y) in [(Align::End, Align::Start), (Align::Start, Align::End)] {
623                        ui.leaf(
624                            NodeSpec::column()
625                                .size(7.5, 7.5)
626                                .border(1.0, fg)
627                                .float(crate::spec::FloatConfig::parent().inside(x, y)),
628                        );
629                    }
630                });
631            }
632            WindowButton::Maximize => {
633                ui.leaf(NodeSpec::column().size(9.0, 9.0).border(1.0, fg));
634            }
635            WindowButton::Close => {
636                // The multiplication sign inks only about 0.42 em, so it
637                // needs a far larger em than the 9-10px bar and box beside
638                // it to read as the same size. It also rides the math axis,
639                // which sits a little under the middle of the line box, so
640                // the bottom padding lifts it back onto the button center.
641                const EM: f32 = 23.0;
642                ui.text_in(
643                    NodeSpec::row().padding(Edges {
644                        b: EM * 0.25,
645                        ..Edges::default()
646                    }),
647                    "\u{00d7}",
648                    TextStyle::new(EM).line_height(EM).color(fg),
649                );
650            }
651        },
652    );
653}
654
655/// The standard button's spec: hover and pressed backgrounds are declared
656/// on the node and resolved by the core, so every binding's button is this
657/// same data. Add the label as a child.
658///
659/// The three backgrounds are the theme's accent trio (`accent`,
660/// `accent_hover`, `accent_pressed`): the OS's accent where the host
661/// reports one, the app's where it set or pinned one, and kui's blue
662/// otherwise. Takes the theme and the metrics rather than reading them, so
663/// `widgets::button_spec(&ui.theme(), &ui.metrics())` is the idiom. The
664/// derivation for any other base colour is [`button_palette`].
665pub fn button_spec(theme: &Theme, m: &Metrics) -> NodeSpec {
666    NodeSpec::row()
667        .pad_xy(m.control_pad_x, m.control_pad_y)
668        .bg(theme.accent)
669        .hover_bg(theme.accent_hover)
670        .pressed_bg(theme.accent_pressed)
671        .radius(m.radius)
672        .center()
673}
674
675/// A button's three backgrounds from one base colour: the base, a hover a
676/// step toward white, a pressed a step toward black. The steps are the
677/// distances the stock button's own trio sits at, so an accent-painted
678/// button reads as the same control in a different colour.
679///
680/// Public because "a button in *this* colour" is the same question with a
681/// different answer, and the arithmetic should not be re-guessed per app.
682pub fn button_palette(base: Color) -> (Color, Color, Color) {
683    (
684        base,
685        base.mix(Color::WHITE, 0.09),
686        base.mix(Color::BLACK, 0.10),
687    )
688}
689
690/// Black or white, whichever a reader can see on `bg`.
691///
692/// The split is at `Color::luminance` 0.4 rather than at the midpoint:
693/// white text needs a darker background than black text needs a light one,
694/// and the accents that land near the line (macOS's yellow at 0.72, its
695/// orange at 0.44) come out the way the platform paints them. It is the
696/// stock button's answer, not a general contrast checker — a palette that
697/// cares should say what its label colour is.
698pub fn readable_on(bg: Color) -> Color {
699    if bg.luminance() > 0.4 {
700        Color::BLACK
701    } else {
702        Color::WHITE
703    }
704}
705
706/// The stock button's text size — [`Metrics::default`]'s `control_text`;
707/// the widget itself reads `ui.metrics()`.
708pub const BUTTON_TEXT: f32 = Metrics::comfortable().control_text;
709/// What a disabled stock button's opacity is multiplied by. The core makes
710/// it inert and drops its hover and pressed backgrounds, and nothing else
711/// would show a sighted user the state a reader is told.
712pub const BUTTON_DISABLED_OPACITY: f32 = 0.5;
713
714/// A push button showing `text`, keyed by it; a click posts `payload` as
715/// a [`UiEvent`](crate::input::UiEvent) on the button's key.
716///
717/// ```rust
718/// # use kui_core::{Core, NodeSpec, Size, widgets};
719/// # let mut core = Core::new();
720/// # let mut ui = core.frame(Size::new(200.0, 100.0), 1.0);
721/// widgets::button(&mut ui, "Save", "save");
722/// // The same button with its spec in hand: a tooltip and a stable key.
723/// let (t, m) = (ui.theme(), ui.metrics());
724/// widgets::button_with(&mut ui, "save-2", "Save", widgets::button_spec(&t, &m).on_click("save"), Some("Ctrl+S"));
725/// # ui.finish();
726/// ```
727///
728/// A label that changes re-keys the node (a new node, so it loses keyboard
729/// focus and a screen reader's cursor); declare such a button with
730/// [`button_with`] and a key of its own. The pointer over it is the hand
731/// (`CursorShape::Pointer`): the core implies no shape from an `on_click`,
732/// and the stock button is the one place the hand is declared for you.
733pub fn button(ui: &mut Ui<'_>, text: &str, payload: impl Into<Value>) {
734    let (theme, m) = (ui.theme(), ui.metrics());
735    button_with(
736        ui,
737        text,
738        text,
739        button_spec(&theme, &m).on_click(payload.into()),
740        None,
741    );
742}
743
744/// [`button`] with its spec in the caller's hands: `spec` is [`button_spec`]
745/// plus what the caller declared on it — the `on_click`, and the rows the
746/// stock button admits in every binding (`schema::BUTTON_ROWS_JSX`): a
747/// `label` when the text is not the name, a `description`, `disabled`,
748/// and the hover tracking and description a `tooltip` sets, whose float
749/// is `hint` — drawn under the button while it is hovered, as every
750/// binding's `tooltip` prop floats one. Keyed by `key`, so a label that
751/// changes need not re-key the node. A disabled button is dimmed
752/// ([`BUTTON_DISABLED_OPACITY`]) as well as inert.
753///
754/// This is what `<button>`, `button { }` and `kui_button_with` lower to,
755/// so a binding cannot end up with a button of its own.
756pub fn button_with(ui: &mut Ui<'_>, key: &str, text: &str, spec: NodeSpec, hint: Option<&str>) {
757    button_body(ui, Ident::Label(key), text, spec, hint);
758}
759
760/// [`button_with`] keyed by a data index rather than a label — a row of a
761/// virtual list (`Ui::open_indexed`), so the button keeps its focus, its
762/// hover and its tweens as the built range slides and the same text on
763/// two rows is two nodes. What `<button index>` and
764/// `button { index = }` lower to.
765pub fn button_indexed(ui: &mut Ui<'_>, index: u64, text: &str, spec: NodeSpec, hint: Option<&str>) {
766    button_body(ui, Ident::Index(index), text, spec, hint);
767}
768
769/// How a button is keyed: by the label its `key` declares, or by the
770/// data index its `index` declares.
771enum Ident<'a> {
772    Label(&'a str),
773    Index(u64),
774}
775
776/// A widget that takes a `hint` floats it itself, so a spec that also
777/// declared [`NodeSpec::tooltip`] does not float a second one.
778fn own_hint(spec: &mut NodeSpec, hint: Option<&str>) {
779    if hint.is_some() && spec.access().tooltip {
780        spec.access_mut().tooltip = false;
781    }
782}
783
784fn button_body(ui: &mut Ui<'_>, ident: Ident<'_>, text: &str, spec: NodeSpec, hint: Option<&str>) {
785    let theme = ui.theme();
786    // `accent` asks for the whole family, not just the background the
787    // core would substitute for any node: a button whose hover and pressed
788    // shades stayed put would flash under a yellow accent. On a stock
789    // spec it changes nothing — `button_spec` paints from the theme's
790    // trio already (AR41) — and on a spec whose caller set its own `bg`
791    // it is the ask to take the theme's instead. The family is the
792    // *theme's*, and the theme always has one, so there is no
793    // gate here: kui's blue is the accent nobody chose.
794    let spec = if spec.accent {
795        spec.bg(theme.accent)
796            .hover_bg(theme.accent_hover)
797            .pressed_bg(theme.accent_pressed)
798    } else {
799        spec
800    };
801    let spec = if spec.disabled {
802        let o = spec.style.opacity * theme.disabled_opacity;
803        spec.opacity(o)
804    } else {
805        spec
806    };
807    // The hand is declared, never derived from the `on_click`
808    // (`crate::cursor`), and the stock button is where it is declared: a
809    // caller's own `cursor` stands, and an inert button is the arrow — the
810    // click it refuses is not one to point at.
811    let spec = if spec.cursor.is_none() && !spec.disabled {
812        spec.cursor(CursorShape::Pointer)
813    } else {
814        spec
815    };
816    // The hint floats out of the access tree (`hover_hint`), so it is
817    // heard only as the description: a caller that passed one without
818    // `apply_tooltip` on the spec still has it said. A declared
819    // description stands, as it does over the prop.
820    let mut spec = match hint {
821        Some(hint) if spec.access().description.is_none() => spec.apply_tooltip(hint),
822        _ => spec,
823    };
824    own_hint(&mut spec, hint);
825    // Whatever the background ended up being: white on the stock blue as
826    // it has always been, black on an accent light enough to need it.
827    let label = readable_on(spec.style.bg);
828    let size = ui.metrics().control_text;
829    let node = match ident {
830        Ident::Label(key) => ui.child_key(key),
831        Ident::Index(i) => ui.child_key_indexed(i),
832    };
833    let body = |ui: &mut Ui<'_>| {
834        ui.text(text, TextStyle::new(size).color(label));
835        if let Some(hint) = hint
836            && ui.is_hovered(node)
837        {
838            hover_hint(ui, hint);
839        }
840    };
841    match ident {
842        Ident::Label(key) => {
843            ui.with_keyed(key, spec, body);
844        }
845        Ident::Index(i) => {
846            ui.with_indexed(i, spec, body);
847        }
848    }
849}
850
851// -- Stock controls ---------------------------------------------------------
852// The stock controls over the roles: checkbox, radio, switch
853// and slider, composed over the roles the core already reads. The state is
854// the app's and rides on the spec — `checked`, `mixed`, `value_now` — so a
855// control is drawn from what the view declared this frame, and a toggle's
856// press is its `on_click` like any button's. One definition per control:
857// every binding's element lowers to the `*_with` here.
858
859/// The side of a stock control's box — a checkbox, a radio's circle, a
860/// switch's height, a slider's thumb — from the metrics' control text, so
861/// `compact` and `scaled` move it with the stock button: 16 px at the
862/// comfortable density, 14 at the compact one.
863pub fn control_box(m: &Metrics) -> f32 {
864    (m.control_text + 1.0).round()
865}
866
867/// Which toggle a [`toggle_with`] draws.
868#[derive(Clone, Copy, Debug, PartialEq, Eq)]
869pub enum Toggle {
870    Checkbox,
871    Radio,
872    Switch,
873}
874
875impl Toggle {
876    /// The role it declares, whatever the spec said.
877    pub fn role(self) -> Role {
878        match self {
879            Toggle::Checkbox => Role::Checkbox,
880            Toggle::Radio => Role::Radio,
881            Toggle::Switch => Role::Switch,
882        }
883    }
884
885    /// The element's name in every binding.
886    pub fn name(self) -> &'static str {
887        match self {
888            Toggle::Checkbox => "checkbox",
889            Toggle::Radio => "radio",
890            Toggle::Switch => "switch",
891        }
892    }
893}
894
895/// A stock toggle's spec — the row its indicator and label sit in — as
896/// [`button_spec`] is the button's. A caller with a spec of its own starts
897/// here and adds the state (`checked`, `mixed`), the `on_click` and the
898/// access rows to it.
899pub fn toggle_spec(m: &Metrics) -> NodeSpec {
900    NodeSpec::row()
901        .gap((control_box(m) / 2.0).round())
902        .cross_align(Align::Center)
903}
904
905/// A checkbox labelled `text`, keyed by it, drawn from `checked`; a press
906/// — pointer, Space, Enter or assistive technology — posts `payload`,
907/// and the view flips its model and draws it again.
908pub fn checkbox(ui: &mut Ui<'_>, text: &str, checked: bool, payload: impl Into<Value>) -> Key {
909    let m = ui.metrics();
910    toggle_with(
911        ui,
912        Toggle::Checkbox,
913        text,
914        text,
915        toggle_spec(&m).checked(checked).on_click(payload.into()),
916        None,
917    )
918}
919
920/// A radio labelled `text`, keyed by it; see [`checkbox`]. Radios belong in
921/// a [`radio_group_with`], whose arrows move the choice.
922pub fn radio(ui: &mut Ui<'_>, text: &str, checked: bool, payload: impl Into<Value>) -> Key {
923    let m = ui.metrics();
924    toggle_with(
925        ui,
926        Toggle::Radio,
927        text,
928        text,
929        toggle_spec(&m).checked(checked).on_click(payload.into()),
930        None,
931    )
932}
933
934/// A switch labelled `text`, keyed by it; see [`checkbox`].
935pub fn switch(ui: &mut Ui<'_>, text: &str, on: bool, payload: impl Into<Value>) -> Key {
936    let m = ui.metrics();
937    toggle_with(
938        ui,
939        Toggle::Switch,
940        text,
941        text,
942        toggle_spec(&m).checked(on).on_click(payload.into()),
943        None,
944    )
945}
946
947/// A toggle with its spec in the caller's hands, the way [`button_with`]
948/// takes the button's: `spec` is [`toggle_spec`] plus the state and the
949/// rows the element admits — `checked`, `mixed` (a checkbox's third
950/// state), `on_click`, `label`, `description`, `disabled`, and `hint`, the
951/// tooltip drawn while it is hovered. The role is `kind`'s whatever the
952/// spec said. Keyed by `key`; an empty `text` draws the indicator alone,
953/// which then wants a `label`. A disabled toggle is dimmed as well as
954/// inert. This is what `<checkbox>`, `<radio>`, `<switch>` and their Lua
955/// and C doors lower to.
956pub fn toggle_with(
957    ui: &mut Ui<'_>,
958    kind: Toggle,
959    key: &str,
960    text: &str,
961    spec: NodeSpec,
962    hint: Option<&str>,
963) -> Key {
964    let t = ui.theme();
965    let m = ui.metrics();
966    let node = ui.child_key(key);
967    let ax = spec.access();
968    let mixed = ax.mixed && kind == Toggle::Checkbox;
969    let on = ax.checked || mixed;
970    let disabled = spec.disabled;
971    let hovered = !disabled && ui.is_hovered(node);
972    let mut spec = spec.role(kind.role());
973    own_hint(&mut spec, hint);
974    if disabled {
975        let o = spec.style.opacity * t.disabled_opacity;
976        spec = spec.opacity(o);
977    } else if spec.cursor.is_none() {
978        spec = spec.cursor(CursorShape::Pointer);
979    }
980    let b = control_box(&m);
981    ui.with_keyed(key, spec, |ui| {
982        let edge = if on || hovered {
983            t.accent
984        } else {
985            t.border_strong
986        };
987        match kind {
988            Toggle::Checkbox | Toggle::Radio => {
989                let radius = if kind == Toggle::Radio {
990                    b / 2.0
991                } else {
992                    m.radius_inner.min(b / 4.0)
993                };
994                let face = NodeSpec::row()
995                    .size(b, b)
996                    .radius(radius)
997                    .border(1.0, edge)
998                    .bg(if on { t.accent } else { t.sunken })
999                    .center();
1000                ui.with(face, |ui| {
1001                    if !on {
1002                        return;
1003                    }
1004                    if kind == Toggle::Radio {
1005                        let d = (b * 0.4).round();
1006                        ui.leaf(NodeSpec::row().size(d, d).radius(d / 2.0).bg(t.on_accent));
1007                    } else if mixed {
1008                        ui.leaf(
1009                            NodeSpec::row()
1010                                .size((b * 0.5).round(), 2.0)
1011                                .radius(1.0)
1012                                .bg(t.on_accent),
1013                        );
1014                    } else {
1015                        // Drawn, not a glyph: the same mark at every size
1016                        // and in every font.
1017                        ui.polyline(
1018                            &[
1019                                Vec2::new(b * 0.26, b * 0.52),
1020                                Vec2::new(b * 0.43, b * 0.69),
1021                                Vec2::new(b * 0.75, b * 0.33),
1022                            ],
1023                            crate::line::Stroke::new((b / 8.0).max(1.5), t.on_accent),
1024                            NodeSpec::default(),
1025                        );
1026                    }
1027                });
1028            }
1029            Toggle::Switch => {
1030                let track = NodeSpec::row()
1031                    .size((b * 1.75).round(), b)
1032                    .pad(2.0)
1033                    .radius(b / 2.0)
1034                    .bg(if on { t.accent } else { t.border_strong })
1035                    .main_align(if on { Align::End } else { Align::Start })
1036                    .cross_align(Align::Center)
1037                    .transition(120.0);
1038                ui.with_keyed("track", track, |ui| {
1039                    let k = b - 4.0;
1040                    ui.leaf_keyed(
1041                        "knob",
1042                        NodeSpec::row()
1043                            .size(k, k)
1044                            .radius(k / 2.0)
1045                            .bg(t.on_accent)
1046                            .transition(120.0)
1047                            .slide(),
1048                    );
1049                });
1050            }
1051        }
1052        if !text.is_empty() {
1053            ui.text(text, TextStyle::new(m.control_text).color(t.fg));
1054        }
1055        if let Some(hint) = hint
1056            && ui.is_hovered(node)
1057        {
1058            tooltip(ui, hint);
1059        }
1060    })
1061}
1062
1063/// The stock radio group's spec: a column of radios. What
1064/// [`radio_group_with`] is handed by [`radio_group`].
1065pub fn radio_group_spec(m: &Metrics) -> NodeSpec {
1066    NodeSpec::column().gap((control_box(m) / 2.0).round())
1067}
1068
1069/// A radio group named `label`: one Tab stop whose arrows, Home and End
1070/// move the choice among the radios `f` declares and press the one they
1071/// land on, so a group of radios whose
1072/// payloads each set the choice answers the keyboard with no more code.
1073/// The role and the name are the group's whatever `spec` said; a `row`
1074/// spec lays the radios out across, and its arrows run across with it. A
1075/// spec with no gap takes [`radio_group_spec`]'s, so a binding that built
1076/// the spec from its rows — where `dir="row"` starts one from nothing —
1077/// gets the stock spacing without restating it.
1078pub fn radio_group_with(
1079    ui: &mut Ui<'_>,
1080    label: &str,
1081    spec: NodeSpec,
1082    f: impl FnOnce(&mut Ui<'_>),
1083) -> Key {
1084    let spec = radio_group_open_spec(&ui.metrics(), label, spec);
1085    ui.with_keyed(label, spec, f)
1086}
1087
1088/// The spec a radio group named `label` opens with: `spec` with the
1089/// group's role and name, and the stock gap where it has none. What
1090/// [`radio_group_with`] opens, and what C's `kui_radio_group_open` does,
1091/// whose radios are declared between it and `kui_close`.
1092pub fn radio_group_open_spec(m: &Metrics, label: &str, spec: NodeSpec) -> NodeSpec {
1093    let mut spec = spec.role(Role::RadioGroup).label(label);
1094    if spec.layout.gap == 0.0 {
1095        spec.layout.gap = radio_group_spec(m).layout.gap;
1096    }
1097    spec
1098}
1099
1100/// A radio group over named options: `current` is the one in force, and a
1101/// choice posts `payload(i)`. Each radio is keyed by its index, so two
1102/// options with one label are two radios.
1103pub fn radio_group(
1104    ui: &mut Ui<'_>,
1105    label: &str,
1106    options: &[&str],
1107    current: Option<usize>,
1108    payload: impl Fn(usize) -> Value,
1109) -> Key {
1110    let m = ui.metrics();
1111    radio_group_with(ui, label, radio_group_spec(&m), |ui| {
1112        for (i, option) in options.iter().enumerate() {
1113            let key = format!("{i}");
1114            toggle_with(
1115                ui,
1116                Toggle::Radio,
1117                &key,
1118                option,
1119                toggle_spec(&m)
1120                    .checked(current == Some(i))
1121                    .on_click(payload(i)),
1122                None,
1123            );
1124        }
1125    })
1126}
1127
1128/// The stock slider's spec: a row as wide as a menu and as tall as its
1129/// thumb, padded by half the thumb on either side so the thumb's centre
1130/// is under the pointer at both ends — the content box is the track the
1131/// core reads a press along. A caller sizing its
1132/// own slider changes the width and keeps the padding.
1133pub fn slider_spec(m: &Metrics) -> NodeSpec {
1134    let b = control_box(m);
1135    NodeSpec::row()
1136        .size(m.menu_width, b)
1137        .pad_xy(b / 2.0, 0.0)
1138        .cross_align(Align::Center)
1139}
1140
1141/// A slider named `label` over `min..=max`, at `value`, moving by `step`.
1142/// Its changes arrive as `{kind: "change", value, phase, tag}` with `tag`
1143/// — from the pointer, the arrows, the Page keys, Home / End and
1144/// assistive technology alike — and the view stores `value` and draws the
1145/// slider again at it.
1146pub fn slider(
1147    ui: &mut Ui<'_>,
1148    label: &str,
1149    value: f32,
1150    min: f32,
1151    max: f32,
1152    step: f32,
1153    tag: impl Into<Value>,
1154) -> Key {
1155    let m = ui.metrics();
1156    slider_with(
1157        ui,
1158        label,
1159        slider_spec(&m)
1160            .value_now(value)
1161            .value_min(min)
1162            .value_max(max)
1163            .value_step(step)
1164            .on_change(tag.into()),
1165        None,
1166    )
1167}
1168
1169/// A slider with its spec in the caller's hands: [`slider_spec`] plus the
1170/// value rows (`value_now`, `value_min`, `value_max`, `value_step`,
1171/// `value_text`), `on_change`, `description`, `disabled`, a width, and
1172/// `hint`, the tooltip drawn while it is hovered. Keyed by `label`, which
1173/// is its accessible name unless the spec carries a `label` of its own.
1174/// The role is the slider's whatever the spec said. What `<slider>` and
1175/// its Lua and C doors lower to.
1176pub fn slider_with(ui: &mut Ui<'_>, label: &str, spec: NodeSpec, hint: Option<&str>) -> Key {
1177    let t = ui.theme();
1178    let m = ui.metrics();
1179    let node = ui.child_key(label);
1180    let ax = spec.access();
1181    let fraction = crate::slider::SliderRange::of(ax).map_or(0.0, |r| {
1182        let now = ax.value_now.map_or(r.min, crate::slider::exact);
1183        ((now - r.min) / (r.max - r.min)).clamp(0.0, 1.0) as f32
1184    });
1185    let named = ax.label.is_some();
1186    let disabled = spec.disabled;
1187    let mut spec = spec.role(Role::Slider);
1188    own_hint(&mut spec, hint);
1189    if !named {
1190        spec = spec.label(label);
1191    }
1192    if disabled {
1193        let o = spec.style.opacity * t.disabled_opacity;
1194        spec = spec.opacity(o);
1195    } else if spec.cursor.is_none() {
1196        spec = spec.cursor(CursorShape::Pointer);
1197    }
1198    let b = control_box(&m);
1199    ui.with_keyed(label, spec, |ui| {
1200        let track = NodeSpec::row()
1201            .grow_width()
1202            .height(4.0)
1203            .radius(2.0)
1204            .bg(t.border_strong);
1205        ui.with(track, |ui| {
1206            let fill = NodeSpec::row()
1207                .width(Sizing::Percent(fraction))
1208                .grow_height()
1209                .radius(2.0)
1210                .bg(t.accent);
1211            ui.with(fill, |ui| {
1212                // Hung off the fill's end, so it sits where the value is
1213                // with no arithmetic of the view's.
1214                ui.leaf(
1215                    NodeSpec::row()
1216                        .size(b, b)
1217                        .radius(b / 2.0)
1218                        .bg(t.on_accent)
1219                        .border(1.0, t.border_strong)
1220                        .float(
1221                            FloatConfig::parent()
1222                                .at(Align::End, Align::Center)
1223                                .self_at(Align::Center, Align::Center),
1224                        ),
1225                );
1226            });
1227        });
1228        if let Some(hint) = hint
1229            && ui.is_hovered(node)
1230        {
1231            tooltip(ui, hint);
1232        }
1233    })
1234}
1235
1236// -- Splitter ---------------------------------------------------------------
1237
1238/// A divider between two panes that the pointer drags:
1239/// `thickness` px across, growing along the rest of its parent, in the
1240/// theme's border colour and its accent while hovered or held, with the
1241/// resize arrows, and `tag` as its `on_drag`. `dir` is the parent's: in a
1242/// `Dir::Row` the panes sit side by side and the bar stands between them;
1243/// in a `Dir::Column` it lies across. A press on it leaves the keyboard
1244/// where it was (`keep_focus`), as a divider beside an editor should.
1245///
1246/// The split is the app's: `ev.drag()` on the tag's event, and
1247/// `Drag::ratio()` is the pointer's place across the parent — `.x` for a
1248/// row's split, `.y` for a column's — which is the new fraction as it is.
1249/// Returns the bar's key.
1250pub fn splitter(
1251    ui: &mut Ui<'_>,
1252    label: &str,
1253    dir: crate::spec::Dir,
1254    thickness: f32,
1255    tag: impl Into<Value>,
1256) -> Key {
1257    let t = ui.theme();
1258    let bar = match dir {
1259        crate::spec::Dir::Row => NodeSpec::column()
1260            .size(thickness, Sizing::GROW)
1261            .cursor(CursorShape::EwResize),
1262        crate::spec::Dir::Column => NodeSpec::column()
1263            .size(Sizing::GROW, thickness)
1264            .cursor(CursorShape::NsResize),
1265    };
1266    ui.leaf_keyed(
1267        label,
1268        bar.bg(t.border)
1269            .hover_bg(t.accent)
1270            .pressed_bg(t.accent)
1271            .on_drag(tag)
1272            .keep_focus(),
1273    )
1274}
1275
1276// -- Context menus ----------------------------------------------------------
1277// The menu every app was writing for itself. It is
1278// exported rather than hidden inside the core's automatic path, and the
1279// automatic path calls exactly this — so an app that answers its own
1280// `onContextMenu` to add two items of its own gets the layout, the
1281// keyboard, the dismissal and the access rows without rewriting them, and
1282// the corpus tests one menu rather than two.
1283
1284/// Menu chrome, in one place so a native renderer's absence still looks
1285/// deliberate rather than improvised.
1286pub const MENU_WIDTH: f32 = Metrics::comfortable().menu_width;
1287pub const MENU_TEXT: f32 = Metrics::comfortable().chrome_text;
1288/// The reserved label the stock menu is keyed under. A menu the core
1289/// opened is found by key, not by guessing at payloads, so an app is free
1290/// to post whatever it likes from its own items.
1291pub const MENU_KEY: &str = "kui.menu";
1292
1293/// Draws a context menu at `at` (logical viewport px) and returns the key
1294/// of its root. A float anchored to the viewport rather than to a parent,
1295/// because a context menu belongs at the pointer and not under whatever
1296/// node happens to enclose it; `fit` is what keeps it in the window, which
1297/// for a menu near the bottom edge means flipping above the point.
1298///
1299/// It declares `modal`, so a press outside it or Escape emits a `dismiss`
1300/// event on it rather than through a dismissal rule of its own; the caller
1301/// closes it when that dismissal arrives. The rows are `menuItem`s under a
1302/// `menu`, which is what makes the arrow keys work and what a screen reader
1303/// reads.
1304///
1305/// Each chosen row posts the item's `id`, or its label when it declares
1306/// none. A `Separator` posts nothing and takes no focus.
1307pub fn context_menu(ui: &mut Ui<'_>, at: Vec2, items: &[MenuItem]) -> Key {
1308    let t = ui.theme();
1309    let m = ui.metrics();
1310    // The menu's nodes are the core's, not the host's: opened under their
1311    // own origin, so the core takes their events back by it.
1312    let saved = ui.origin();
1313    ui.set_origin(OriginId::MENU);
1314    // The menu floats against the window, not the host area (a menu the
1315    // platform showed would not stop at a dock's edge either, and the
1316    // devtools' own select opens one inside the dock): the host's point
1317    // becomes the window's.
1318    let at = at.plus(ui.core().dt_shift());
1319    let root = menu_panel(
1320        ui,
1321        MENU_KEY,
1322        menu_panel_spec(&t, &m)
1323            .float(
1324                FloatConfig::viewport()
1325                    // Top-left of the menu at the top-left of the
1326                    // viewport, then offset to the point: the placement
1327                    // every context menu has, with `fit` flipping it up
1328                    // or clamping it in when the point is near an edge.
1329                    .inside(Align::Start, Align::Start)
1330                    .offset(at.x, at.y)
1331                    .fit(),
1332            )
1333            .modal(Value::str(MENU_KEY))
1334            .label("Menu"),
1335        items,
1336    );
1337    ui.set_origin(saved);
1338    root
1339}
1340
1341/// The panel every menu is: a fixed-width column of rows, in the palette
1342/// the stock menu paints. What the caller adds is where it goes and what
1343/// scope it belongs to — a context menu floats at the pointer and declares
1344/// its own `modal`; the menu bar's drops out of its title and lives inside
1345/// the bar's. Takes the palette and the metrics rather than reading them,
1346/// because a caller that has a `Ui` in one hand cannot lend it to this and
1347/// to `menu_panel` in the same expression; `let t = ui.theme();` first is
1348/// the idiom.
1349pub fn menu_panel_spec(t: &Theme, m: &Metrics) -> NodeSpec {
1350    NodeSpec::column()
1351        .role(Role::Menu)
1352        // As wide as its widest row and never narrower than the metric: a
1353        // long accelerator beside a long label widens the menu rather than
1354        // wrapping either onto a second line (backlog F127). Rows grow to
1355        // the panel, so their right edges line up.
1356        .width(Sizing::Fit)
1357        .min_width(m.menu_width)
1358        .pad(MENU_PANEL_PAD)
1359        .gap(1.0)
1360        .bg(t.raised)
1361        .border(1.0, t.border_strong)
1362        .radius(m.radius)
1363}
1364
1365/// Builds the rows of one menu into `spec`, keyed under `label`, and
1366/// reports the keys they took. The one place a menu's rows are drawn:
1367/// both menus kui has are this function with a different container.
1368///
1369/// A row with a submenu ([`MenuItem::submenu`]) is drawn with a chevron.
1370/// In the core's own menus — the context menu and the drawn bar's — its
1371/// menu opens beside it, as another of these panels, when the pointer
1372/// rests on it, it is clicked, or the keyboard opens it (Enter, the Right
1373/// arrow); an app drawing this panel itself gets the chevron and opens
1374/// nothing, since the open submenus are the core's state.
1375pub fn menu_panel(ui: &mut Ui<'_>, label: &str, spec: NodeSpec, items: &[MenuItem]) -> Key {
1376    menu_level(ui, label, label, spec, items, &[])
1377}
1378
1379/// One level of a menu: `items` built into `spec` under `label`, at `path`
1380/// — empty for the menu itself, the rows opened on the way for a submenu.
1381/// `root` is the outermost panel's label, which names the hover groups.
1382fn menu_level(
1383    ui: &mut Ui<'_>,
1384    root: &str,
1385    label: &str,
1386    spec: NodeSpec,
1387    items: &[MenuItem],
1388    path: &[usize],
1389) -> Key {
1390    let t = ui.theme();
1391    let m = ui.metrics();
1392    // A wash rather than a fill, so a row's label stays readable on both
1393    // bases without the view guessing a frame ahead of the core — see
1394    // `Theme::accent_soft`.
1395    let accent = t.accent_soft;
1396    // A gutter for the checkmarks, and only where a row has one: a menu of
1397    // plain commands is not indented for a column nothing uses, and one
1398    // with a setting in it keeps every label on the same left edge whether
1399    // the setting is on or off.
1400    let gutter = items.iter().any(|i| i.checked);
1401    // Which of the core's menus this is, by the origin its nodes open
1402    // under: only those have submenu state to open and close.
1403    let surface = crate::runtime::MenuSurface::of(ui.origin());
1404    let group = |i: usize| format!("{root}/{path:?}/{i}");
1405    // Where the pointer rests opens or closes a submenu, resolved before
1406    // anything is built — the frame that notices the hover draws what it
1407    // opened, as the bar's titles do.
1408    if let Some(s) = surface {
1409        for (i, item) in items.iter().enumerate() {
1410            if item.selectable() && ui.is_group_hovered(NodeSpec::hover_group_id(&group(i))) {
1411                let row: Vec<usize> = path.iter().copied().chain([i]).collect();
1412                ui.core().submenu_hovered(s, &row, item.has_submenu());
1413            }
1414        }
1415    }
1416    let open_here = surface.and_then(|s| ui.core().submenu_open_at(s, path));
1417    let mut first_key = None;
1418    let root_key = ui.with_keyed(label, spec, |ui| {
1419        let mut first = path.is_empty();
1420        for (i, item) in items.iter().enumerate() {
1421            if item.role == MenuRole::Separator {
1422                ui.leaf_indexed(
1423                    i as u64,
1424                    NodeSpec::row()
1425                        .grow_width()
1426                        .height(1.0)
1427                        .bg(t.border)
1428                        // Not a row anything reads out: a divider is
1429                        // paint, and a screen reader hearing "separator"
1430                        // between every pair of items is noise.
1431                        .role(Role::None),
1432                );
1433                continue;
1434            }
1435            // The row posts which item it is; the core takes the event back
1436            // by origin, performs the item — or opens its submenu — and
1437            // what the app hears is the item's own `id` on the node the
1438            // menu was about.
1439            let payload = menu_row_tag(i, path);
1440            let opens = item.has_submenu();
1441            let is_open = opens && open_here == Some(i);
1442            let mut spec = NodeSpec::row()
1443                .role(Role::MenuItem)
1444                .label(item.text())
1445                .grow_width()
1446                // Its content as a floor, which is what a fit panel is
1447                // sized from: a grow child alone contributes nothing.
1448                .min_width(crate::spec::Bound::Fit)
1449                .pad_xy(m.menu_pad_x, m.menu_pad_y)
1450                .gap(8.0)
1451                .radius(m.radius_inner)
1452                .main_align(Align::Start)
1453                .cross_align(Align::Center);
1454            if item.checked {
1455                // The gutter's checkmark is paint; this is the same fact for
1456                // a screen reader, which reads a row that carries one as
1457                // checked rather than as "✓ Wrap".
1458                spec = spec.checked(true);
1459            }
1460            if item.enabled {
1461                spec = spec.on_click(payload).hover_bg(accent).focus_bg(accent);
1462                if surface.is_some() {
1463                    spec = spec.hover_group(&group(i));
1464                }
1465                // The first row that can take focus is where the modal opens:
1466                // a menu whose keyboard starts nowhere makes the arrow keys
1467                // feel like they missed. A submenu is no modal of its own;
1468                // the keyboard that opens one puts focus in it.
1469                if first {
1470                    spec = spec.initial_focus();
1471                    first = false;
1472                }
1473            } else {
1474                spec = spec.disabled(true).opacity(t.disabled_opacity);
1475            }
1476            if opens {
1477                // A reader hears it as a row that opens something, and
1478                // whether it is open; the open one keeps the wash while the
1479                // pointer is in its submenu, so the way back is visible.
1480                spec = spec.expanded(is_open);
1481                if is_open {
1482                    spec = spec.bg(accent);
1483                }
1484            }
1485            let row = |ui: &mut Ui<'_>| {
1486                if gutter {
1487                    ui.with(NodeSpec::row().width(MENU_CHECK_W), |ui| {
1488                        if item.checked {
1489                            ui.text("\u{2713}", TextStyle::new(m.chrome_text).color(t.fg));
1490                        }
1491                    });
1492                }
1493                // One line each, whatever the panel's width: the panel is
1494                // sized to fit them, and a row that wrapped would be read as
1495                // two.
1496                ui.text(
1497                    item.text(),
1498                    TextStyle::new(m.chrome_text).color(t.fg).nowrap(),
1499                );
1500                // Pushed to the right edge by a grow spacer, so the label
1501                // stays where the eye expects it whatever follows it; at
1502                // least `MENU_ACCEL_GAP` wide, so the widest label and the
1503                // widest accelerator never touch. A submenu's row has its
1504                // chevron there and no accelerator: it binds nothing.
1505                let tail = if opens {
1506                    Some(std::borrow::Cow::Borrowed(MENU_CHEVRON))
1507                } else {
1508                    item.accel_label()
1509                };
1510                if let Some(tail) = tail {
1511                    ui.leaf(NodeSpec::row().grow_width().min_width(MENU_ACCEL_GAP));
1512                    ui.text(&tail, TextStyle::new(m.chrome_text).color(t.muted).nowrap());
1513                }
1514            };
1515            let key = if opens {
1516                // A wrapper the submenu drops out of, so the panel is the
1517                // row's *sibling* — the menu bar's reason: a `menuItem` is
1518                // named from its content, and a menu inside one would be
1519                // read as part of its name — and floats against the row's
1520                // own box.
1521                let mut key = Key::ROOT;
1522                ui.with_indexed(
1523                    i as u64,
1524                    NodeSpec::row()
1525                        .grow_width()
1526                        .min_width(crate::spec::Bound::Fit),
1527                    |ui| {
1528                        key = ui.with_keyed(MENU_ROW_KEY, spec, row);
1529                        if is_open && item.enabled {
1530                            let sub: Vec<usize> = path.iter().copied().chain([i]).collect();
1531                            menu_level(
1532                                ui,
1533                                root,
1534                                MENU_SUB_KEY,
1535                                menu_panel_spec(&t, &m)
1536                                    .label(item.text())
1537                                    // A region of its own, so a press on its
1538                                    // padding or a dead row is inside the
1539                                    // menu — as the outer panel's `modal`
1540                                    // makes it there — and not the press
1541                                    // outside that dismisses it.
1542                                    .hoverable()
1543                                    .float(
1544                                        // Beside the row, its first row level
1545                                        // with this one (the panel's padding
1546                                        // above it), flipped to the other side
1547                                        // at the window's edge.
1548                                        FloatConfig::parent()
1549                                            .at(Align::End, Align::Start)
1550                                            .self_at(Align::Start, Align::Start)
1551                                            .offset(MENU_PANEL_PAD, -MENU_PANEL_PAD)
1552                                            .fit(),
1553                                    ),
1554                                &item.submenu,
1555                                &sub,
1556                            );
1557                        }
1558                    },
1559                );
1560                key
1561            } else {
1562                ui.with_indexed(i as u64, spec, row)
1563            };
1564            if item.enabled && first_key.is_none() {
1565                first_key = Some(key);
1566            }
1567        }
1568    });
1569    // The keyboard opened this submenu (Enter, the Right arrow): the
1570    // first row it can take is where it lands, now that it exists.
1571    if let (Some(s), Some(key)) = (surface, first_key)
1572        && !path.is_empty()
1573    {
1574        ui.core().submenu_drawn(s, path, key);
1575    }
1576    root_key
1577}
1578
1579/// The padding inside a menu's panel, logical px: what a submenu is offset
1580/// by so its first row sits level with the row that opened it.
1581const MENU_PANEL_PAD: f32 = 4.0;
1582
1583/// The chevron a submenu's row draws where an accelerator would be.
1584pub const MENU_CHEVRON: &str = "\u{203a}";
1585/// The label a submenu's row is keyed under, inside its wrapper.
1586const MENU_ROW_KEY: &str = "row";
1587/// The label a submenu's panel is keyed under, beside its row.
1588const MENU_SUB_KEY: &str = "sub";
1589
1590/// What a menu row's click carries: its index in its menu's items, and —
1591/// for a row of a submenu — the rows opened on the way to it, for the core
1592/// to read back (`Core::take_surface_events`). The title of a menu-bar
1593/// menu carries its index the same way, under `title`. A top-level row
1594/// carries `row` alone, as it always did.
1595fn menu_row_tag(i: usize, path: &[usize]) -> Value {
1596    let row = ("row", Value::Int(i as i64));
1597    if path.is_empty() {
1598        Value::map([row])
1599    } else {
1600        Value::map([
1601            row,
1602            (
1603                "path",
1604                Value::List(path.iter().map(|&p| Value::Int(p as i64)).collect()),
1605            ),
1606        ])
1607    }
1608}
1609
1610fn menu_title_tag(i: usize) -> Value {
1611    Value::map([("title", Value::Int(i as i64))])
1612}
1613
1614/// The reserved label the drawn menu bar is keyed under, the way
1615/// [`MENU_KEY`] is the open menu's.
1616pub const MENU_BAR_KEY: &str = "kui.menubar";
1617/// The label its dropped menu is keyed under, beside the open title.
1618const MENU_BAR_PANEL_KEY: &str = "kui.menubar.menu";
1619/// The label each title is keyed under, inside its own wrapper.
1620const MENU_BAR_TITLE_KEY: &str = "kui.menubar.title";
1621
1622/// The hover group a title and its menu share, so the widget can ask
1623/// whether the pointer is on the `i`th title without knowing its key.
1624fn group_name(i: usize) -> String {
1625    format!("{MENU_BAR_KEY}.{i}")
1626}
1627/// The bar's height, logical px — a little under a titlebar's, which is
1628/// what every platform that draws one in the window does.
1629pub const MENU_BAR_H: f32 = Metrics::comfortable().menu_bar_h;
1630
1631/// The application menu: `bar` is what the app's menu *is*, and calling
1632/// this is where its titles go when they have to be drawn in the window.
1633///
1634/// One call and not two, because the declaration and the placement are one
1635/// decision. **It draws nothing where the platform owns the bar** — macOS,
1636/// where the driver hands this same declaration to `NSApp` — so the call
1637/// still says what the menu is and the strip simply is not there; that is
1638/// the contract [`window_buttons`] has under native decorations, and it is
1639/// what makes one view portable. An empty `bar` takes the menu away.
1640///
1641/// Declared every frame, and diffed: an unchanged menu costs a comparison
1642/// and rebuilds nothing.
1643///
1644/// Everything below a title is the stock menu: the same rows, roles,
1645/// accelerators and access tree the context menu draws, through the same
1646/// [`menu_panel`]. What is the bar's own is the scope — while a menu is
1647/// open the *bar* is the frame's modal, not the dropdown, so hovering
1648/// across the titles moves the open menu the way a menu bar does, a press
1649/// on the open title closes it, and Escape or a press in the app below
1650/// dismisses it as any modal is dismissed.
1651///
1652/// Typical use, as the first child of a full-height root, under the
1653/// titlebar if there is one:
1654/// `widgets::menu_bar(ui, self.menu());`
1655pub fn menu_bar(ui: &mut Ui<'_>, bar: MenuBar) {
1656    // Declaring it is this call's first half, and drawing it the second:
1657    // where the platform owns the bar there is no second half, and the
1658    // frame has still said what the app's menu is.
1659    ui.core().declare_menu_bar(bar);
1660    if ui.core().native_menu_bar() {
1661        return;
1662    }
1663    let Some(bar) = ui.core().menu_bar().cloned() else {
1664        return;
1665    };
1666    if bar.menus.is_empty() {
1667        return;
1668    }
1669    let t = ui.theme();
1670    let m = ui.metrics();
1671    let accent = t.accent_soft;
1672    let mut open = ui.core().menu_bar_open();
1673    // The bar's nodes are the core's, opened under their own origin (see
1674    // `OriginId::MENU_BAR`), so the core takes their events back by it.
1675    let saved = ui.origin();
1676    ui.set_origin(OriginId::MENU_BAR);
1677    let mut spec = NodeSpec::row()
1678        .grow_width()
1679        .height(m.menu_bar_h)
1680        .cross_align(Align::Center)
1681        .pad_xy(4.0, 0.0)
1682        .gap(2.0)
1683        .bg(t.bg)
1684        .role(Role::Menu)
1685        .label("Menu bar");
1686    if open.is_some() {
1687        // The bar and not the dropdown is the modal while a menu is open:
1688        // the titles have to stay live for the hover to walk them, and the
1689        // app below has to be as inert as it is under any other menu.
1690        spec = spec.modal(Value::str(MENU_BAR_KEY));
1691    }
1692    let root = ui.with_keyed(MENU_BAR_KEY, spec, |ui| {
1693        // Hovering another title while a menu is open moves the open menu
1694        // to it, which is what a menu bar does everywhere. Resolved before
1695        // anything is built, so the frame that notices the hover is the
1696        // frame that draws the new menu and not the one after it — and
1697        // asked by *group* rather than by key, since a title's key is
1698        // inside a wrapper this loop has not opened yet.
1699        if open.is_some() {
1700            for (i, menu) in bar.menus.iter().enumerate() {
1701                let hovered = ui.is_group_hovered(NodeSpec::hover_group_id(&group_name(i)));
1702                if open != Some(i) && menu.enabled && !menu.items.is_empty() && hovered {
1703                    open = Some(i);
1704                    ui.core().set_menu_bar_open(open);
1705                }
1706            }
1707        }
1708        for (i, menu) in bar.menus.iter().enumerate() {
1709            let live = menu.enabled && !menu.items.is_empty();
1710            let is_open = open == Some(i);
1711            // A wrapper the menu drops out of, so the panel is a *sibling*
1712            // of the title and not a child of it: a `menuItem` is a
1713            // name-from-content role, and a menu nested inside one would be
1714            // read as part of its name and never reached on its own.
1715            ui.with_indexed(i as u64, NodeSpec::row(), |ui| {
1716                let mut spec = NodeSpec::row()
1717                    .role(Role::MenuItem)
1718                    .label(menu.label.as_str())
1719                    // Two px shorter than a row's, so the bar's height and
1720                    // not the title's padding decides the strip.
1721                    .pad_xy(m.menu_pad_x, (m.menu_pad_y - 2.0).max(0.0))
1722                    .radius(m.radius_inner)
1723                    .cross_align(Align::Center);
1724                if live {
1725                    // Which title this is: the core takes the event back
1726                    // by origin and opens or closes the `i`th menu.
1727                    spec = spec
1728                        .on_click(menu_title_tag(i))
1729                        .hover_group(&group_name(i))
1730                        .hover_bg(accent)
1731                        .focus_bg(accent);
1732                    if is_open {
1733                        spec = spec.bg(accent);
1734                    }
1735                } else {
1736                    spec = spec.disabled(true).opacity(t.disabled_opacity);
1737                }
1738                ui.text_in_keyed(
1739                    MENU_BAR_TITLE_KEY,
1740                    spec,
1741                    menu.label.as_str(),
1742                    TextStyle::new(m.chrome_text).color(t.fg),
1743                );
1744                if is_open {
1745                    // Out of the title's bottom-left corner, and `fit` to
1746                    // slide back in at the right-hand end of the bar.
1747                    menu_panel(
1748                        ui,
1749                        MENU_BAR_PANEL_KEY,
1750                        menu_panel_spec(&t, &m).label(menu.label.as_str()).float(
1751                            FloatConfig::parent()
1752                                .at(Align::Start, Align::End)
1753                                .self_at(Align::Start, Align::Start)
1754                                .offset(0.0, 2.0)
1755                                .fit(),
1756                        ),
1757                        &menu.items,
1758                    );
1759                }
1760            });
1761        }
1762    });
1763    ui.set_origin(saved);
1764    ui.core().set_menu_bar_root(root);
1765}
1766
1767/// The checkmark gutter's width, logical px.
1768const MENU_CHECK_W: f32 = 14.0;
1769
1770/// The least room between a row's label and its accelerator, logical px,
1771/// beside the row's own gap on either side: about what AppKit leaves
1772/// before a key equivalent.
1773pub const MENU_ACCEL_GAP: f32 = 16.0;
1774
1775// -- Virtual lists ----------------------------------------------------------
1776// The core culls glyphs by viewport but builds every child a view declares,
1777// so a ten-thousand-row log costs ten thousand rows of build and layout on
1778// every frame — most of a 120 Hz budget spent on rows nobody can see. A view
1779// that knows the container's height and offset can declare a screenful and
1780// two spacers instead. `Core::scroll_geometry` is that knowledge; this is
1781// the arithmetic, for the case where every row is the same height.
1782
1783/// The half-open range of rows a container of `rows` rows, each `row_h`
1784/// logical px tall, has any reason to build — those crossing the visible
1785/// band, plus `overscan` on each side — given the geometry of the frame
1786/// before. Pure arithmetic, exposed for views that build their own
1787/// container instead of using [`uniform_list`].
1788///
1789/// `vh` is the container's height, and `pad_t` the padding above the first
1790/// row. `None` geometry means no layout has resolved the container yet:
1791/// the caller decides what the first frame builds.
1792pub fn visible_rows(
1793    offset_y: f32,
1794    vh: f32,
1795    pad_t: f32,
1796    row_h: f32,
1797    rows: usize,
1798    overscan: usize,
1799) -> std::ops::Range<usize> {
1800    if rows == 0 || row_h <= 0.0 {
1801        return 0..0;
1802    }
1803    // Flow coordinates: row i spans [i*row_h, (i+1)*row_h), and layout puts
1804    // the flow's origin at pad_t - offset_y inside the container's box, so
1805    // the visible window is [offset_y - pad_t, that + vh).
1806    let top = offset_y - pad_t;
1807    let first = (top / row_h).floor().max(0.0) as usize;
1808    let last = ((top + vh.max(0.0)) / row_h).ceil().max(0.0) as usize;
1809    let first = first.saturating_sub(overscan).min(rows);
1810    let last = last.saturating_add(overscan).min(rows);
1811    first..last.max(first)
1812}
1813
1814/// A vertically scrolling column of `rows` uniform rows that builds only the
1815/// visible ones. `row(ui, i)` declares row `i`; it must come out exactly
1816/// `row_h` logical px tall, since that is the arithmetic placing every row
1817/// above and below it.
1818///
1819/// The container is `spec` forced to a scrolling column with no gap — put
1820/// the spacing inside `row_h` (a row that pads itself) rather than in a
1821/// `gap`, so one number describes the stride. Rows are opened with
1822/// [`Ui::open_indexed`] at their *data* index, so a row keeps its key, and
1823/// with it its hover, focus, edit buffer and tweens, as the built range
1824/// slides over it. Above and below sit two empty spacers holding the space
1825/// of the rows not built, so the content height, the scrollbar and
1826/// `set_scroll` all behave as if the whole list were there.
1827///
1828/// The geometry it slices by is the previous frame's, so the first frame —
1829/// before any layout has resolved the container — slices by the viewport
1830/// height instead and asks for one more frame; a resize is one frame late
1831/// and covered by the two rows of overscan. Returns the container's key,
1832/// for `set_scroll` (`Vec2::new(0.0, i as f32 * row_h)` scrolls row `i` to
1833/// the top, which is how you reach a row that is not built — `reveal` of an
1834/// unbuilt row finds nothing).
1835pub fn uniform_list(
1836    ui: &mut Ui<'_>,
1837    label: &str,
1838    spec: NodeSpec,
1839    rows: usize,
1840    row_h: f32,
1841    row: impl FnMut(&mut Ui<'_>, usize),
1842) -> Key {
1843    uniform_list_with(ui, label, spec, rows, row_h, |_| NodeSpec::column(), row)
1844}
1845
1846/// [`uniform_list`] with each row's own node spelled by `row_spec(i)` —
1847/// the click, the zebra stripe, the hover background, the role a row
1848/// carries — where the plain form's rows are bare and the callback nests
1849/// a second node inside each to carry them. The height is
1850/// forced to `row_h`, the stride the arithmetic assumes, and a width the
1851/// spec leaves `fit` grows across the list.
1852pub fn uniform_list_with(
1853    ui: &mut Ui<'_>,
1854    label: &str,
1855    spec: NodeSpec,
1856    rows: usize,
1857    row_h: f32,
1858    mut row_spec: impl FnMut(usize) -> NodeSpec,
1859    mut row: impl FnMut(&mut Ui<'_>, usize),
1860) -> Key {
1861    const OVERSCAN: usize = 2;
1862
1863    let key = ui.child_key(label);
1864    let pad_t = spec.layout.padding.t;
1865    // Both numbers from the same frame: the geometry's offset is clamped to
1866    // that frame's travel, so a `set_scroll(key, huge)` between frames
1867    // slices the end of the list instead of a megabyte past it.
1868    let (offset_y, vh, first_frame) = match ui.scroll_geometry(key) {
1869        Some(g) => (g.offset.y, g.rect.h, false),
1870        // Nothing laid out yet: the container cannot be taller than the
1871        // window in the ordinary case, so a screenful is a safe over-build
1872        // for one frame.
1873        None => (ui.scroll_offset(key).y, ui.viewport().h, true),
1874    };
1875    let range = visible_rows(offset_y, vh, pad_t, row_h, rows, OVERSCAN);
1876
1877    ui.with_keyed(label, spec.scroll_y().gap(0.0), |ui| {
1878        // The whole list's size, built or not: what Select All inside a
1879        // `selectable` list spans.
1880        ui.row_count(rows as u64);
1881        // Keyed, not auto-keyed: an auto key is a sibling index, and the
1882        // rows already occupy that namespace at their data indices — an
1883        // auto-keyed spacer next to a built row 0 would be row 0's key.
1884        let lead = range.start as f32 * row_h;
1885        if lead > 0.0 {
1886            ui.leaf_keyed("lead", spacer_spec(lead));
1887        }
1888        for i in range.clone() {
1889            let mut spec = row_spec(i).height(row_h);
1890            if spec.layout.width == Sizing::Fit {
1891                spec = spec.grow_width();
1892            }
1893            ui.with_indexed(i as u64, spec, |ui| row(ui, i));
1894        }
1895        let tail = (rows - range.end) as f32 * row_h;
1896        if tail > 0.0 {
1897            ui.leaf_keyed("tail", spacer_spec(tail));
1898        }
1899    });
1900
1901    // Sliced by a screenful's guess, with no layout of its own yet: the
1902    // next frame slices by its geometry. kui's ask, not the app's, so a
1903    // trace names it for what it is.
1904    if first_frame {
1905        ui.owe_frame("list first frame");
1906    }
1907    key
1908}
1909
1910/// Scrolls the [`uniform_list`] labelled `label` so row `i` shows, when
1911/// it does not already: to the middle of the list, so a jump lands with
1912/// rows on both sides of it. Call it before the list is
1913/// declared, in the same parent — the frame that scrolls then slices its
1914/// rows by the offset it scrolls to, instead of a frame late. Returns
1915/// whether it scrolled. The first frame, before the list has laid out,
1916/// has no geometry and scrolls nothing; the row arithmetic assumes the
1917/// list's rows start at its content top and fill its box, as they do
1918/// without padding. A row past the list's content — an index past its
1919/// end — scrolls nothing and answers false, as does a `row_h` that is
1920/// not positive.
1921///
1922/// `Ui::reveal` cannot do this for a row that is not built, and a
1923/// virtual list builds only what shows.
1924pub fn reveal_row(ui: &mut Ui<'_>, label: &str, i: usize, row_h: f32) -> bool {
1925    let key = ui.child_key(label);
1926    let Some(g) = ui.scroll_geometry(key) else {
1927        return false;
1928    };
1929    let y = i as f32 * row_h;
1930    if row_h <= 0.0 || y + row_h > g.content.h + 0.5 {
1931        return false;
1932    }
1933    if g.offset.y <= y && y + row_h <= g.offset.y + g.rect.h {
1934        return false;
1935    }
1936    let to = (y + row_h / 2.0 - g.rect.h / 2.0).max(0.0);
1937    ui.set_scroll(key, Vec2::new(g.offset.x, to));
1938    true
1939}
1940
1941/// How many whole rows of `row_h` the [`uniform_list`] labelled `label`
1942/// shows as of the last layout — a PageDown's stride. 0 before it has
1943/// laid out, and for a `row_h` that is not positive.
1944pub fn rows_in_view(ui: &mut Ui<'_>, label: &str, row_h: f32) -> usize {
1945    let key = ui.child_key(label);
1946    if row_h <= 0.0 {
1947        return 0;
1948    }
1949    ui.scroll_geometry(key)
1950        .map_or(0, |g| (g.rect.h / row_h).floor().max(0.0) as usize)
1951}
1952
1953/// Each row's own node in the variable-height `list`: the wrapper the
1954/// callback builds inside, sized to the height the arithmetic assumes.
1955fn row_spec(h: f32) -> NodeSpec {
1956    NodeSpec::column().grow_width().height(h)
1957}
1958
1959fn spacer_spec(h: f32) -> NodeSpec {
1960    NodeSpec::column().grow_width().height(h)
1961}
1962
1963// -- Variable-height virtual lists ------------------------------------------
1964// `uniform_list` takes one stride and every row must come out that tall,
1965// which is the log viewer, the data table and the chat history whose rows are
1966// one line. A row that wraps, a card with an image, a message that is
1967// sometimes three lines: none of those have a stride, and the three things
1968// the uniform arithmetic does with `i * row_h` — the lead spacer, the search
1969// from an offset to the first visible row, and "scroll to row i" — have no
1970// closed form without one. Prefix sums are the closed form, and
1971// `RowHeights` is where they live.
1972//
1973// Heights come from the caller, measured only for the rows the frame needs:
1974// `measure_text` gives layout's own number for a text row (wrap, max_lines
1975// and the shaping cache included), so a row measured and then drawn shapes
1976// once. Everything not measured yet stands at an estimate, and the estimate
1977// is the mean of what has been measured — which means it *moves*, and moving
1978// it changes the height of every row above the window as well as below.
1979// That is what the anchor is for.
1980
1981/// The heights a [`list`] slices by: a measured number per row
1982/// where one is known, an estimate everywhere else, and the prefix sums over
1983/// both.
1984///
1985/// The app owns it and hands the same one back every frame — a widget
1986/// composed from primitives keeps no state of its own, which is what keeps
1987/// it reachable from a scripting frontend. Rebuild it (or [`Self::clear`])
1988/// when the rows themselves change.
1989#[derive(Clone, Debug)]
1990pub struct RowHeights {
1991    /// One per row; `f32::NAN` for a row nothing has measured yet.
1992    h: Vec<f32>,
1993    /// The prefix sums, split so that the estimate is applied at the query
1994    /// rather than baked in: `m[i]` is the measured height in rows `0..i`
1995    /// and `u[i]` how many of those rows have none. A moving mean then costs
1996    /// nothing to fold in — which matters, because every measurement moves
1997    /// it, and a mean baked into the sums would dirty all of them.
1998    m: Vec<f32>,
1999    u: Vec<u32>,
2000    /// How many entries of `m` / `u` are valid, counting from 0. Filled
2001    /// on demand and only as far as a query asks, so a list scrolled to row
2002    /// 30 never sums the 9,970 below it; a measurement at row `i` truncates
2003    /// this to `i + 1`, since nothing at or below `i` changed.
2004    clean: usize,
2005    /// What the caller guessed before anything was measured.
2006    seed: f32,
2007    /// Running mean of the measured rows — the estimate for the rest.
2008    sum: f32,
2009    n: usize,
2010    /// The content width the cached heights were measured at. A different
2011    /// one rewraps every row, so it drops them all.
2012    width: f32,
2013    /// Where the last search landed. Scrolling is local, so the next one
2014    /// gallops out from here instead of bisecting the whole list — which is
2015    /// what keeps the lazy `ensure` above from being filled past what is
2016    /// being looked at, and what a bisection from 0..len would defeat by
2017    /// probing the middle every time.
2018    last: usize,
2019}
2020
2021impl RowHeights {
2022    /// `rows` rows, none measured, each standing at `estimate` logical px
2023    /// until it is. The estimate only has to be the right order of
2024    /// magnitude: it decides how wrong the scrollbar is before the list has
2025    /// been scrolled through, and nothing else.
2026    pub fn new(rows: usize, estimate: f32) -> Self {
2027        RowHeights {
2028            h: vec![f32::NAN; rows],
2029            m: vec![0.0],
2030            u: vec![0],
2031            clean: 1,
2032            seed: estimate.max(1.0),
2033            sum: 0.0,
2034            n: 0,
2035            width: f32::NAN,
2036            last: 0,
2037        }
2038    }
2039
2040    pub fn len(&self) -> usize {
2041        self.h.len()
2042    }
2043
2044    pub fn is_empty(&self) -> bool {
2045        self.h.is_empty()
2046    }
2047
2048    /// Grows or shrinks to `rows`, keeping what is still in range — rows
2049    /// appended to a log keep every height already measured, and cost
2050    /// nothing until something asks about them. A list whose rows *changed*
2051    /// rather than grew wants [`Self::clear`].
2052    pub fn set_len(&mut self, rows: usize) {
2053        if rows == self.h.len() {
2054            return;
2055        }
2056        for i in rows..self.h.len() {
2057            self.forget(i);
2058        }
2059        self.h.resize(rows, f32::NAN);
2060        self.clean = self.clean.min(rows + 1);
2061    }
2062
2063    /// Forgets every measurement, keeping the length and the seed — the call
2064    /// for a list whose contents changed under the same indices.
2065    pub fn clear(&mut self) {
2066        self.h.fill(f32::NAN);
2067        self.sum = 0.0;
2068        self.n = 0;
2069        self.clean = 1;
2070    }
2071
2072    /// Records row `i`'s height. Rows measured this way are what the
2073    /// estimate for the others is the mean of.
2074    pub fn set(&mut self, i: usize, h: f32) {
2075        if i >= self.h.len() || !h.is_finite() || h < 0.0 {
2076            return;
2077        }
2078        self.forget(i);
2079        self.h[i] = h;
2080        self.sum += h;
2081        self.n += 1;
2082        // Everything up to and including row `i`'s own top is unchanged.
2083        self.clean = self.clean.min(i + 1);
2084    }
2085
2086    fn forget(&mut self, i: usize) {
2087        let old = self.h[i];
2088        if !old.is_nan() {
2089            self.sum -= old;
2090            self.n -= 1;
2091            self.h[i] = f32::NAN;
2092            self.clean = self.clean.min(i + 1);
2093        }
2094    }
2095
2096    /// Row `i`'s height as it was measured, or `None` for one standing at
2097    /// the estimate.
2098    pub fn measured(&self, i: usize) -> Option<f32> {
2099        self.h.get(i).copied().filter(|h| !h.is_nan())
2100    }
2101
2102    /// Row `i`'s height: measured, or the estimate.
2103    pub fn get(&self, i: usize) -> f32 {
2104        self.measured(i).unwrap_or_else(|| self.estimate())
2105    }
2106
2107    /// What an unmeasured row stands at: the mean of the measured ones, or
2108    /// the caller's seed before there are any.
2109    pub fn estimate(&self) -> f32 {
2110        if self.n == 0 {
2111            self.seed
2112        } else {
2113            self.sum / self.n as f32
2114        }
2115    }
2116
2117    /// The width the measurements were taken at, or `NaN` before any.
2118    pub fn width(&self) -> f32 {
2119        self.width
2120    }
2121
2122    /// Declares the content width the next measurements are for. A width
2123    /// that differs from the cached one drops every height — the rows wrap
2124    /// differently now — and returns true. [`list`] calls this from
2125    /// the container's own laid-out box.
2126    pub fn set_width(&mut self, w: f32) -> bool {
2127        if !w.is_finite() || w <= 0.0 || (self.width - w).abs() < 0.5 {
2128            return false;
2129        }
2130        let had = self.n > 0;
2131        self.width = w;
2132        if had {
2133            self.clear();
2134        }
2135        true
2136    }
2137
2138    /// Fills the prefix sums up to `i` if they do not reach it yet.
2139    fn ensure(&mut self, i: usize) {
2140        let want = i.min(self.h.len()) + 1;
2141        if self.clean >= want {
2142            return;
2143        }
2144        self.m.truncate(self.clean);
2145        self.u.truncate(self.clean);
2146        self.m.reserve(want - self.clean);
2147        self.u.reserve(want - self.clean);
2148        let (mut acc, mut est) = (self.m[self.clean - 1], self.u[self.clean - 1]);
2149        for &h in &self.h[self.clean - 1..want - 1] {
2150            if h.is_nan() {
2151                est += 1;
2152            } else {
2153                acc += h;
2154            }
2155            self.m.push(acc);
2156            self.u.push(est);
2157        }
2158        self.clean = want;
2159    }
2160
2161    /// The top of row `i` in content coordinates — the height of everything
2162    /// above it. `offset_of(len())` is the whole list's height.
2163    pub fn offset_of(&mut self, i: usize) -> f32 {
2164        let i = i.min(self.h.len());
2165        self.ensure(i);
2166        self.m[i] + self.u[i] as f32 * self.estimate()
2167    }
2168
2169    /// The list's total height, measured and estimated together — what the
2170    /// two spacers and the scrollbar are made of. Kept as it goes, so the
2171    /// tail spacer costs nothing however long the list is.
2172    pub fn total(&self) -> f32 {
2173        self.sum + (self.h.len() - self.n) as f32 * self.estimate()
2174    }
2175
2176    /// The row `y` (content coordinates) lands in: the last row whose top is
2177    /// at or above it, clamped to the list. The binary search that replaces
2178    /// `y / row_h`.
2179    pub fn row_at(&mut self, y: f32) -> usize {
2180        let rows = self.h.len();
2181        if rows == 0 || y <= 0.0 {
2182            self.last = 0;
2183            return 0;
2184        }
2185        // `offset_of` is non-decreasing, so what is wanted is the last row
2186        // whose top is at or below `y`. Row 0's top is 0, so it always
2187        // qualifies and the bracket below always closes.
2188        let mut lo = self.last.min(rows - 1);
2189        let mut hi;
2190        if self.offset_of(lo) > y {
2191            hi = lo;
2192            let mut step = 1usize;
2193            while lo > 0 {
2194                lo = lo.saturating_sub(step);
2195                if self.offset_of(lo) <= y {
2196                    break;
2197                }
2198                hi = lo;
2199                step *= 2;
2200            }
2201        } else {
2202            hi = (lo + 1).min(rows);
2203            let mut step = 1usize;
2204            while hi < rows && self.offset_of(hi) <= y {
2205                lo = hi;
2206                hi = (hi + step).min(rows);
2207                step *= 2;
2208            }
2209        }
2210        while lo + 1 < hi {
2211            let mid = lo + (hi - lo) / 2;
2212            if self.offset_of(mid) <= y {
2213                lo = mid;
2214            } else {
2215                hi = mid;
2216            }
2217        }
2218        self.last = lo;
2219        lo
2220    }
2221}
2222
2223/// A vertically scrolling column of rows of *different* heights that builds
2224/// only the visible ones — [`uniform_list`] where no single stride
2225/// describes the list.
2226///
2227/// `measure(ui, i, width)` returns row `i`'s height at that content width,
2228/// and is called only for rows the frame is about to build that `heights`
2229/// has no number for; `ui.measure_text(.., Some(width))` is layout's own
2230/// answer for a text row, wrap and all, and shapes through the same cache
2231/// the row's draw will hit. What it returns is the height the row *gets*:
2232/// each row's node is fixed to it, so the arithmetic above and below can
2233/// never disagree with the layout, the way `uniform_list`'s stride cannot.
2234/// A row that would rather size itself has to say what that size is here.
2235///
2236/// `row(ui, i)` declares row `i` inside that node, exactly as
2237/// `uniform_list`'s does, and rows are opened with [`Ui::open_indexed`] at
2238/// their data index, so a row keeps its hover, focus, edit buffer and tweens
2239/// as the built range slides over it.
2240///
2241/// **What it does that the uniform one never has to:** every row not yet
2242/// measured stands at the mean of the ones that are, so measuring the rows
2243/// this frame builds changes the height of every row it does not — the ones
2244/// above the window included. Left alone that slides the content out from
2245/// under the pointer on the frame it learns anything. So the widget takes
2246/// the row the window starts in and how far into it, measures, and then puts
2247/// that pair back: `Core::set_scroll` from inside a view lands on the frame
2248/// being built (the positions pass reads the store after the view has run),
2249/// so the corrected frame is the only one ever seen. What does move is the
2250/// scrollbar, which is the honest thing to move — the list really did just
2251/// learn it is a different length.
2252///
2253/// Returns the container's key, for `set_scroll` — and "scroll to row `i`"
2254/// is `set_scroll(key, Vec2::new(0.0, heights.offset_of(i)))`, exact for a
2255/// measured row and converging over a frame or two for one that is not.
2256pub fn list(
2257    ui: &mut Ui<'_>,
2258    label: &str,
2259    spec: NodeSpec,
2260    heights: &mut RowHeights,
2261    mut measure: impl FnMut(&mut Ui<'_>, usize, f32) -> f32,
2262    mut row: impl FnMut(&mut Ui<'_>, usize),
2263) -> Key {
2264    let key = ui.child_key(label);
2265    let mut slice = heights.slice(ListReading::of(ui, key, spec.layout.padding));
2266    loop {
2267        let pending = slice.unmeasured(heights);
2268        if pending.is_empty() {
2269            break;
2270        }
2271        for i in pending {
2272            let h = measure(ui, i, slice.width());
2273            heights.set(i, h);
2274        }
2275        if !slice.reslice(heights) {
2276            break;
2277        }
2278    }
2279    let plan = slice.finish(heights);
2280    // A write from inside a view lands on the frame being built: the
2281    // positions pass reads the store after the view has run. So the frame
2282    // that learned the rows are a different size is drawn already
2283    // corrected, and the uncorrected one is never seen. A shift, not a
2284    // `set_scroll`: the correction moves the coordinates under the
2285    // content, so it is never eased on a container with a `transition`,
2286    // and mid-glide it moves the leg with it rather than ending the leg
2287    // where the content stands (RG18).
2288    if let Some((drawn, target)) = plan.shift {
2289        ui.shift_scroll(key, Vec2::new(0.0, drawn), Vec2::new(0.0, target));
2290    }
2291
2292    ui.with_keyed(label, spec.scroll_y().gap(0.0), |ui| {
2293        ui.row_count(heights.len() as u64);
2294        if plan.lead > 0.0 {
2295            ui.leaf_keyed("lead", spacer_spec(plan.lead));
2296        }
2297        for i in plan.range.clone() {
2298            ui.with_indexed(i as u64, row_spec(heights.get(i)), |ui| row(ui, i));
2299        }
2300        if plan.tail > 0.0 {
2301            ui.leaf_keyed("tail", spacer_spec(plan.tail));
2302        }
2303    });
2304
2305    // As `uniform_list`'s first frame.
2306    if plan.first_frame {
2307        ui.owe_frame("list first frame");
2308    }
2309    key
2310}
2311
2312/// What a variable-height list reads before it slices: the container's
2313/// last layout, where its scroll is going, the window, and the padding
2314/// its rows sit inside. [`list`] takes it from the frame
2315/// ([`Self::of`]); a binding builds it from the same readings its view
2316/// already has (`scrollGeometry`, `scrollOffset`, the viewport), so the
2317/// arithmetic after it is this module's in every language.
2318#[derive(Clone, Copy, Debug, Default)]
2319pub struct ListReading {
2320    /// `scroll_geometry` of the container, `None` before a layout has
2321    /// resolved it — the first frame.
2322    pub geometry: Option<crate::scroll::ScrollGeometry>,
2323    /// `scroll_offset(key).y`: the retained offset, which is where an
2324    /// eased leg is going when it differs from `geometry.offset`.
2325    pub scroll_y: f32,
2326    /// The window, logical px: what the first frame slices by.
2327    pub viewport: crate::geom::Size,
2328    /// The container's top padding and its horizontal padding together.
2329    pub pad_t: f32,
2330    pub pad_x: f32,
2331    /// Rows built past each end of the window; 2 unless a view says.
2332    pub overscan: usize,
2333}
2334
2335impl ListReading {
2336    /// The reading for the container `key`, with `pad` its padding, from
2337    /// the frame being built.
2338    pub fn of(ui: &Ui<'_>, key: Key, pad: crate::geom::Edges) -> Self {
2339        ListReading {
2340            geometry: ui.scroll_geometry(key),
2341            scroll_y: ui.scroll_offset(key).y,
2342            viewport: ui.viewport(),
2343            pad_t: pad.t,
2344            pad_x: pad.x(),
2345            overscan: 2,
2346        }
2347    }
2348}
2349
2350/// One frame's slicing of a variable-height list, between the reading and
2351/// the rows: which rows to measure, and — once they are — where the window
2352/// lands and what to build. Made by [`RowHeights::slice`]; see [`list`]
2353/// for the loop that drives it, which every binding's port repeats.
2354#[derive(Clone, Debug)]
2355pub struct ListSlice {
2356    range: std::ops::Range<usize>,
2357    /// The row the window starts in, and how far into it: the pair the
2358    /// correction puts back where it was.
2359    anchor: usize,
2360    into: f32,
2361    top: f32,
2362    /// What the passes move `top` away from. The correction is for a
2363    /// *measurement* moving the numbers — not for the clamp to zero, which
2364    /// on a list shorter than its box (offset 0, padding 6) makes `top +
2365    /// pad_t` differ from the offset every frame, and a correction every
2366    /// frame is a frame requested every frame.
2367    top_before: f32,
2368    /// Where an eased leg (F80) is going, when that is somewhere other
2369    /// than where the content is drawn: a second anchor, so the row under
2370    /// the target stays the target however the measurements move the rows
2371    /// between the two (RG18). The target, its row, and how far into it.
2372    target: Option<(f32, usize, f32)>,
2373    vh: f32,
2374    width: f32,
2375    overscan: usize,
2376    passes: usize,
2377    first_frame: bool,
2378}
2379
2380/// What a [`ListSlice`] comes to: the rows to build, the two spacers'
2381/// heights, and the scroll correction the frame needs (see [`list`]).
2382#[derive(Clone, Debug, PartialEq)]
2383pub struct ListPlan {
2384    pub range: std::ops::Range<usize>,
2385    pub lead: f32,
2386    pub tail: f32,
2387    /// `(drawn, target)` on y, for `Ui::shift_scroll`, when measuring moved
2388    /// the rows; `None` when nothing needs correcting.
2389    pub shift: Option<(f32, f32)>,
2390    /// Sliced by the window, not a layout: the frame after it has to run.
2391    pub first_frame: bool,
2392}
2393
2394/// Measuring changes the heights the range was sliced from, which can widen
2395/// it; four passes is far more than a screenful ever needs and bounds the
2396/// work whatever the measurements do.
2397const LIST_PASSES: usize = 4;
2398
2399impl RowHeights {
2400    /// Starts a frame's slicing from `reading`: the content width (a new one
2401    /// drops every height, since the rows rewrap), the anchors, and the
2402    /// first range.
2403    pub fn slice(&mut self, reading: ListReading) -> ListSlice {
2404        let mut target_y = None;
2405        let (offset_y, vh, cw, first_frame) = match reading.geometry {
2406            Some(g) => {
2407                let t = reading.scroll_y.clamp(0.0, g.max_offset.y);
2408                if (t - g.offset.y).abs() > 0.5 {
2409                    target_y = Some(t);
2410                }
2411                (g.offset.y, g.rect.h, g.rect.w - reading.pad_x, false)
2412            }
2413            // Nothing laid out yet: a screenful of the viewport is a safe
2414            // over-build for one frame, and the width is its width.
2415            None => (
2416                reading.scroll_y,
2417                reading.viewport.h,
2418                reading.viewport.w - reading.pad_x,
2419                true,
2420            ),
2421        };
2422        // A resize rewraps every row, so the cache is void; the frame after
2423        // it measures a screenful again.
2424        self.set_width(cw);
2425        let top = (offset_y - reading.pad_t).max(0.0);
2426        let anchor = self.row_at(top);
2427        let into = top - self.offset_of(anchor);
2428        let target = target_y.map(|t| {
2429            let t = (t - reading.pad_t).max(0.0);
2430            let row = self.row_at(t);
2431            (t, row, t - self.offset_of(row))
2432        });
2433        let range = visible_range(self, top, vh, reading.overscan);
2434        ListSlice {
2435            range,
2436            anchor,
2437            into,
2438            top,
2439            top_before: top,
2440            target,
2441            vh,
2442            width: cw,
2443            overscan: reading.overscan,
2444            passes: 0,
2445            first_frame,
2446        }
2447    }
2448}
2449
2450impl ListSlice {
2451    /// The content width the rows are measured at.
2452    pub fn width(&self) -> f32 {
2453        self.width
2454    }
2455
2456    /// The rows of the current range nothing has measured: measure each,
2457    /// [`RowHeights::set`] it, then [`Self::reslice`]. Empty is done.
2458    pub fn unmeasured(&self, heights: &RowHeights) -> Vec<usize> {
2459        self.range
2460            .clone()
2461            .filter(|&i| heights.measured(i).is_none())
2462            .collect()
2463    }
2464
2465    /// After measuring: puts the anchor row back where it was and slices
2466    /// again. Measuring moved the numbers the slice was taken from — this
2467    /// row's own, the rows above it, and (through the mean) every row
2468    /// nobody has measured at all — so what is under the pointer would
2469    /// otherwise slide out from under it. True when the range moved and
2470    /// its new rows want measuring, within the pass budget.
2471    pub fn reslice(&mut self, heights: &mut RowHeights) -> bool {
2472        self.passes += 1;
2473        self.top = heights.offset_of(self.anchor) + self.into;
2474        let next = visible_range(heights, self.top, self.vh, self.overscan);
2475        if next == self.range {
2476            return false;
2477        }
2478        self.range = next;
2479        self.passes < LIST_PASSES
2480    }
2481
2482    /// The rows to build, the spacers, and the correction.
2483    pub fn finish(self, heights: &mut RowHeights) -> ListPlan {
2484        let drawn = self.top - self.top_before;
2485        let target = match self.target {
2486            Some((t, row, into)) => heights.offset_of(row) + into - t,
2487            None => drawn,
2488        };
2489        let lead = heights.offset_of(self.range.start);
2490        let tail = heights.total() - heights.offset_of(self.range.end);
2491        ListPlan {
2492            range: self.range,
2493            lead,
2494            tail,
2495            shift: (drawn.abs() > 0.01 || target.abs() > 0.01).then_some((drawn, target)),
2496            first_frame: self.first_frame,
2497        }
2498    }
2499}
2500
2501/// The rows crossing `[top, top + vh)` plus `overscan` on each side, by
2502/// prefix-sum search. The variable-height [`visible_rows`].
2503fn visible_range(
2504    heights: &mut RowHeights,
2505    top: f32,
2506    vh: f32,
2507    overscan: usize,
2508) -> std::ops::Range<usize> {
2509    let rows = heights.len();
2510    if rows == 0 {
2511        return 0..0;
2512    }
2513    let first = heights.row_at(top).saturating_sub(overscan);
2514    let last = (heights.row_at(top + vh.max(0.0)) + 1 + overscan).min(rows);
2515    first..last.max(first)
2516}