Skip to main content

kui_core/
widgets.rs

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