//! Nodes to egui.
//!
//! Every function here takes a piece of [`quasi_router`]'s screen tree and draws
//! it into a `Ui`. Nothing returns a `Result`: a description that exists is
//! renderable by construction, which is the property the owned mirror in
//! `quasi-router` was built to have.
//!
//! # Where the drawing goes
//!
//! Down, into `makeover-immediate`. A meter, a token, a control, a figure, a
//! field and a table are all its, and this module is the walk that decides which
//! one a node is and where it sits. The split is the same one `quasi-tui` keeps
//! against `makeover-tui`, and it is what stops a second copy of the vocabulary's
//! drawing existing per host.
//!
//! What is left here is what a `Screen` adds over a node: the address a control
//! carries, the values a form gathers, and the selection a row's tick joins.
//! None of that is `makeover-layout`'s, so none of it can be down there.
//!
//! # The walk is split in two, and the table is why
//!
//! [`draw`] is exhaustive over `Node` and dispatches to [`leaf`] for the nodes
//! that carry no address, and to [`container`] for the ones that hold others or
//! reach a screen's own facts.
//!
//! The split is forced rather than tidy: `makeover_immediate::table` draws a cell
//! through a closure that already holds the `Ui` and the renderer, and a closure
//! cannot also hold the pass mutably. So a cell draws its ordinary nodes through
//! `leaf`, and collects the two that carry an address to fire after the table
//! has finished with the borrow.
use std::collections::BTreeMap;
use egui::{RichText, Ui};
use makeover_immediate::widget;
use makeover_immediate::{Filling, field, frame};
use quasi_router::layout;
use quasi_router::{
Act, Action, Bar, Clock, Image, Node, Params, Placed, RegionKind, Rest, Row, Slot,
};
use crate::view::Asking;
use crate::{Immediate, Pass};
/// What a column is assumed to need when nothing measured it.
///
/// `Sizing::lengths` is how an app says a column's longest value is wider than
/// its name, and a described table carries no such measurement: the description
/// says what a column *is*, not how long its contents turned out. So every
/// column falls back to this, and `egui_extras` sizes the remainder.
const CELL_WIDTH: f32 = 120.0;
/// Draw one node.
///
/// Every member is named, and the containers are listed one by one rather than
/// swept up.
///
/// # Why there is a catch-all anyway
///
/// [`Node`] is `#[non_exhaustive]`, so one is compulsory: a node added upstream
/// does not stop this crate compiling, and the member after this one is not a
/// lockstep release across three renderers.
///
/// The catch-all goes to [`undrawn`], which draws a line saying the renderer
/// does not know this node yet, so a screen quietly drawing less than it
/// describes is the one outcome that cannot happen.
///
/// The discipline the paragraph above asks for still applies, it is not the
/// compiler's job: name every member, and treat a member that reaches the
/// catch-all as one still owed an arm. Neither half of the split may end in a
/// wildcard that *does* something, routing to `container` or to `leaf`, since
/// that is what turns an unknown node into a panic.
pub(crate) fn draw(pass: &mut Pass<'_>, ui: &mut Ui, node: &Node) {
match node {
// The nodes that carry no address. Split out so a table cell can
// draw them: the cell closure holds the `Ui` and cannot also hold
// the pass mutably, and these need nothing but the palette.
Node::Heading { .. }
| Node::Text { .. }
| Node::Rich { .. }
| Node::Code { .. }
| Node::Figure(_)
| Node::Image(_)
| Node::Meter(_)
| Node::Since { .. }
| Node::Until { .. }
| Node::Age { .. } => leaf(pass.immediate, ui, node),
// A notice with something to do about it is addressed, so it cannot go
// to the leaf walk, which has no `Pass` to fire through. The text half
// is still `leaf`'s, so there is one notice drawing and not two.
Node::Notice { act, .. } => {
leaf(pass.immediate, ui, node);
if let Some(act) = act {
act_node(pass, ui, act);
}
}
Node::Act(act) => {
act_node(pass, ui, act);
}
Node::Link { text, action, .. } => {
// A link is a link and not a button: egui has `Link`, and a control
// that navigates should not look like one that writes.
if ui
.link(RichText::new(text).color(pass.immediate.palette.action))
.clicked()
{
pass.fire(action, Params::new(), None);
}
}
Node::Token(tag) => {
let mut pressed = widget::token(
ui,
&tag.label,
tag.kind,
tag.tone,
tag.latched,
&pass.immediate.palette,
&pass.immediate.widget,
);
// `436bc223`: the detail behind the label. egui has hover, so this
// renderer draws it -- `on_hover_text` is the same affordance the
// webview spends `title` on, and a token is already a `Response`,
// so nothing in makeover-immediate had to grow a parameter for it.
if let Some(hint) = &tag.hint {
pressed = pressed.on_hover_text(hint);
}
if let Some(action) = &tag.action
&& pressed.clicked()
{
pass.fire(action, Params::new(), None);
}
}
Node::StandIn { message, act, .. } => {
ui.label(RichText::new(message).color(pass.immediate.palette.content_muted));
if let Some(act) = act {
act_node(pass, ui, act);
}
}
// The containers, named rather than swept up, so that a member added
// upstream stops this match compiling instead of reaching `container`
// and panicking there.
Node::Field(_)
| Node::Region(_)
| Node::Form { .. }
| Node::Table { .. }
| Node::Timeline { .. }
| Node::Stats { .. } => container(pass, ui, node),
// A member added since this renderer last learned the vocabulary. It
// says so and draws nothing else; see this function's header for what
// `#[non_exhaustive]` cost here and what it bought.
_ => undrawn(pass.immediate, ui),
}
}
/// The nodes that carry no address.
///
/// Everything here needs the palette and nothing else, which is what makes a
/// table cell able to draw one: the cell closure already holds the `Ui` and the
/// renderer, and cannot also hold the pass mutably.
///
/// The catch-all is unreachable through [`draw`], which is exhaustive and sends
/// only these here. It is a private split of one walk rather than a second walk,
/// so the guarantee that a new `Node` member stops the build lives up there.
fn leaf(immediate: &Immediate, ui: &mut Ui, node: &Node) {
match node {
Node::Heading { level, text } => {
let size = ui.text_style_height(&egui::TextStyle::Body)
* match level {
layout::Heading::Page => 1.6,
layout::Heading::Section => 1.3,
layout::Heading::Subsection => 1.1,
};
ui.label(
RichText::new(text)
.size(size)
.strong()
.color(immediate.palette.content),
);
}
Node::Text { text, .. } => {
ui.label(RichText::new(text).color(immediate.palette.content));
}
// Markdown arrives as source, so that every renderer answers it its own
// way. This one has no rich text of its own worth the name, so it takes
// the plain rendering: `**bold**` reads as `bold` rather than as four
// characters of syntax, which is the outcome `Node::Text` would have
// given anyway and is the honest floor until egui grows a markdown
// widget worth adopting.
Node::Rich { source, .. } => {
ui.label(
RichText::new(docengine::render_plain(source)).color(immediate.palette.content),
);
}
// `19d7602d`. The runs arrived classified, so all this owes is a
// colour each and a monospace face. egui lays out a horizontal run of
// coloured labels, which is what a code line is.
//
// The palette has no syntax colours and will not grow any: a
// highlighting palette is held fixed while a theme changes, which is
// the opposite of what a palette is for. So this spends the status
// colours, on the terminal renderer's reasoning and with the same
// base16 Tomorrow pairing.
Node::Code { runs, inline, .. } => {
let colour = |syntax: layout::Syntax| match syntax {
layout::Syntax::Comment => immediate.palette.content_muted,
layout::Syntax::String => immediate.palette.success,
layout::Syntax::Keyword => immediate.palette.action,
layout::Syntax::Constant => immediate.palette.warning,
layout::Syntax::Entity => immediate.palette.info,
layout::Syntax::Variable => immediate.palette.danger,
layout::Syntax::Support => immediate.palette.content_secondary,
// Plain, and any class added since this renderer last learned
// the vocabulary: ordinary code, drawn and uncoloured.
_ => immediate.palette.content,
};
let draw_runs = |ui: &mut Ui| {
ui.spacing_mut().item_spacing.x = 0.0;
for run in runs {
ui.label(
RichText::new(run.text.clone())
.monospace()
.color(colour(run.syntax)),
);
}
};
// A block owns its lines and an inline literal sits in one, which
// is the same distinction the node's `inline` flag makes for
// containment. egui has no wrapping run of styled text, so a block
// is a vertical of horizontals per source line and an inline is one
// horizontal.
if *inline {
ui.horizontal(draw_runs);
} else {
ui.vertical(draw_runs);
}
}
Node::Figure(figure) => {
widget::figure(
ui,
&figure.as_layout(),
&immediate.palette,
&immediate.widget,
);
}
Node::Image(picture) => {
image(immediate, ui, picture);
}
Node::Notice { tone, text, .. } => {
// The tone carries it, and the surface says it is a thing set on
// the page rather than part of the flow. Where a toast lands
// against a banner is renderer policy and this renderer has one
// place to put either, which is where the caller drew it.
frame(
ui,
layout::Depth::Raised,
&immediate.palette,
immediate.frame,
|ui| {
ui.label(RichText::new(text).color(immediate.palette.tone(*tone)));
},
);
}
Node::Meter(meter) => {
widget::meter(
ui,
&meter.as_layout(),
&immediate.palette,
&immediate.widget,
);
}
// The bars are borrowed here for the reason every compound member is:
// the description owns them and `makeover-immediate` draws the borrowed
// ones.
Node::Chart { axis, bars, .. } => {
let borrowed: Vec<_> = bars.iter().map(Bar::as_layout).collect();
widget::chart(
ui,
&axis.as_layout(),
&borrowed,
&immediate.palette,
&immediate.widget,
);
}
// The readouts derived from the current time. The instant is the
// description's and the words are this crate's, made fresh here because
// an immediate host redraws from the description every frame -- which
// is the half of this ruling egui gets for free. The half it does not
// is asking for the next frame, and that is `Runtime::show`.
Node::Since { at } => clock_label(immediate, ui, Clock::Since, *at),
Node::Until { at } => clock_label(immediate, ui, Clock::Until, *at),
Node::Age { at } => clock_label(immediate, ui, Clock::Age, *at),
// An addressed node reaching here is this crate's own routing bug, so
// it stays a panic and the members are named to keep it one. Sweeping
// them into the arm below would turn a wrong branch in `draw` into a
// screen that quietly drew a placeholder where a button was.
Node::Act(_)
| Node::Link { .. }
| Node::Token(_)
| Node::StandIn { .. }
| Node::Field(_)
| Node::Form { .. }
| Node::Table { .. }
| Node::Timeline { .. }
| Node::Stats { .. }
| Node::Region(_) => unreachable!("an addressed node reached the leaf walk"),
// A member added since this renderer last learned the vocabulary.
//
// This arm is why the one above lists its members. A table cell walks
// its own node match and ends `other => leaf(..)`, so an unknown member
// inside a cell arrives here rather than at [`draw`] -- and until this
// existed it arrived at the `unreachable!` and panicked the frame,
// which is the same failure the note on [`draw`] records against
// `Node::Image` and `Node::Timeline`.
_ => undrawn(immediate, ui),
}
}
/// A time-derived readout, as one label.
///
/// The clock is read here rather than passed in, which is the shape the ruling
/// asks for: the renderer owns it. A test that needs a fixed answer calls
/// [`crate::clock::text`] with a `now` of its own.
fn clock_label(immediate: &Immediate, ui: &mut Ui, clock: Clock, at: std::time::SystemTime) {
let words = crate::clock::text(clock, at, std::time::SystemTime::now());
ui.label(RichText::new(words).color(immediate.palette.content));
}
/// What a node this renderer has not learned yet draws instead of itself.
///
/// One muted line, the same the description's own [`Node::StandIn`] gets,
/// because it is the same situation said by the renderer instead of by the
/// handler: something is here and you are not seeing it.
///
/// Saying so rather than drawing nothing is the whole point. The failure this
/// crate has already had twice is a screen that silently drew less than it
/// described, and a blank where a node was is indistinguishable from a screen
/// that never carried one.
fn undrawn(immediate: &Immediate, ui: &mut Ui) {
ui.label(
RichText::new("(not drawn: this renderer does not know this yet)")
.italics()
.color(immediate.palette.content_muted),
);
}
/// What a [`RegionKind::Handover`] with no fill says.
///
/// [`undrawn`]'s sibling, for the other way a region ends up empty. A handover
/// is a fill the app owes every host, so this renderer having none is a hole
/// rather than a finished region.
///
/// [`RegionKind::Ceded`] gets no equivalent on purpose: nothing is owed there,
/// so drawing nothing is correct and a notice would invent a gap the app has
/// already ruled on.
fn unfilled(immediate: &Immediate, ui: &mut Ui) {
ui.label(
RichText::new("(not drawn: this host has no fill for this)")
.italics()
.color(immediate.palette.content_muted),
);
}
/// The controls for a set that arrived in parts.
///
/// Back, position, forward, in a row, which is the order the other two renderers
/// put them in. A direction the description gave no address for is drawn
/// disabled rather than left out: an immediate-mode pass rebuilds this row every
/// frame, so a button that appeared on page two would move the position label
/// under the pointer mid-session.
///
/// Where the description offered jumps the numbered pages take the position's
/// place, as the row of controls this host draws everything else as.
fn rest_controls(pass: &mut Pass<'_>, ui: &mut Ui, rest: &Rest) {
let paging = rest.as_layout();
ui.horizontal(|ui| {
if ui
.add_enabled(rest.back.is_some(), egui::Button::new("Prev"))
.clicked()
&& let Some(action) = &rest.back
{
pass.fire(action, Params::new(), None);
}
if rest.jumps.is_empty() {
ui.label(match (paging.page(), paging.pages_total()) {
(Some(page), Some(total)) => format!("{page} / {total}"),
_ => match paging.total() {
Some(total) => format!("{} of {total}", paging.shown()),
None => "Showing what arrived".to_owned(),
},
});
} else {
// The offered pages in place of the readout, for the other two
// renderers' reason: the strip says which page and how many
// already, and printing the position beside it is a control
// arguing with itself.
for jump in &rest.jumps {
if jump.here {
// A label and not a disabled button. A control that
// reloads the page it is on is an affordance that does
// nothing, and this is the one page in the strip that is a
// readout rather than somewhere to go.
ui.label(jump.page.to_string());
continue;
}
if ui.button(jump.page.to_string()).clicked() {
pass.fire(&jump.action, Params::new(), None);
}
}
}
if ui
.add_enabled(rest.forward.is_some(), egui::Button::new("Next"))
.clicked()
&& let Some(action) = &rest.forward
{
pass.fire(action, Params::new(), None);
}
});
}
/// A picture, at a source the description does not carry.
///
/// Drawn here rather than handed down to `makeover-immediate`, and that is the
/// same call `quasi-webview` makes for the same reason: the source is the
/// member the drawing turns on, `makeover-layout` deliberately has no notion of
/// an address, and what is left below the source is a box and a line of text.
/// `makeover-webview` contributes `picture_rules` and writes no `
` either.
fn image(immediate: &Immediate, ui: &mut Ui, picture: &Image) {
let available = ui.available_width();
let mut shown = egui::Image::new(&picture.src)
// A picture the screen needs now advertises that it is coming; one the
// description says can wait does not put a spinner in the flow for it.
.show_loading_spinner(matches!(picture.loading, layout::Loading::Eager));
// What egui paints where the bytes do not arrive. An empty `alt` is a claim
// that the picture adds nothing to the text beside it, so standing in for it
// with anything at all would be worse than the gap -- the argument
// `layout::Image::speaks` carries, and the call `quasi-tui` makes too.
if picture.speaks() {
shown = shown.alt_text(picture.alt.clone());
}
// `Fit::Natural` and `Fit::Contain` are both what happens below: the whole
// picture, at its own proportions, inside the width on offer.
//
// `Fit::Cover` is not, and cannot be here. Cropping needs a box to crop to,
// and a node in an egui vertical flow is given a width and an unbounded
// height -- there is no shape to fill. The webview gets that shape from the
// stylesheet and this host has no stylesheet. Drawing it whole is wrong by
// one member and drawing it cropped to a rectangle nobody described is
// wrong by more, so it draws whole. Filed.
shown = match picture.intrinsic {
// The dimensions doing the one job they exist for: the box is the right
// shape before a byte arrives, so nothing below it moves when the
// texture lands. Never scaled up -- a 5120-wide screenshot is not
// asking for a 5120-wide window.
Some(extent) if extent.width > 0 && extent.height > 0 => {
let width = available.min(f64_ish(extent.width));
let height = width * f64_ish(extent.height) / f64_ish(extent.width);
shown.fit_to_exact_size(egui::vec2(width, height))
}
// Not known, so nothing can be held. egui sizes the texture once it has
// it and whatever is below moves once, which is the honest outcome and
// the one `layout::Image::intrinsic` exists to let an app avoid.
_ => shown.max_width(available).maintain_aspect_ratio(true),
};
ui.add(shown);
// Content that happens to sit under a picture, so it reads the same whether
// or not the picture arrived. Not muted, unlike the alt text, which is
// standing in for something rather than being it.
if let Some(caption) = &picture.caption {
ui.label(RichText::new(caption).color(immediate.palette.content_muted));
}
}
/// A picture's pixel count as a drawing measure.
///
/// `as` on a `u32` is a lossy cast a pedantic lint is right to want justified,
/// and the justification is that it is a texture dimension: `f32` is exact to
/// 16.7 million and no picture is that wide.
#[expect(
clippy::cast_precision_loss,
reason = "a texture dimension is exact in f32 far past any real picture"
)]
fn f64_ish(pixels: u32) -> f32 {
pixels as f32
}
/// How tall one tick of a track is, in lines of body text.
///
/// The least that lets the ruler label itself without the labels touching, and
/// every other measure on the axis falls out of it. A pixel height is
/// presentation and `layout::Track` deliberately carries none, so this is the
/// renderer choosing, the way the webview's stylesheet chooses.
const TICK_LINES: f32 = 1.5;
/// The gutter a ruler writes its labels in, in labels' widths.
const RULER_PADDING: f32 = 8.0;
/// Rows placed by when they happen.
///
/// The lane packing, the gridlines and how tall a slot is are all this
/// renderer's, and `layout::Track` says so: it carries the window, the
/// granularity and the unit, and nothing measured in pixels. What travels is
/// the two integers per entry that order alone cannot say.
fn timeline(
pass: &mut Pass<'_>,
ui: &mut Ui,
track: layout::Track,
entries: &[Placed],
focus: Option,
) {
let text_height = ui.text_style_height(&egui::TextStyle::Body);
let slots = track.slots();
let per_tick = if track.slot == 0 || track.tick == 0 {
0
} else {
track.tick / track.slot
};
let slot_height = if per_tick == 0 {
text_height
} else {
text_height * TICK_LINES / f32::from(per_tick)
};
let height = f32::from(slots) * slot_height;
// The gutter is as wide as the labels going in it, measured rather than
// guessed: a day strip writes `31` and a clock writes `00:00`, and a
// constant wide enough for one wastes half of itself on the other.
let sample = tick_label(track.unit, track.span.from());
let ruler = ui
.painter()
.layout_no_wrap(
sample,
egui::TextStyle::Body.resolve(ui.style()),
pass.immediate.palette.content_muted,
)
.rect
.width()
+ RULER_PADDING;
let (rect, _) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), height),
egui::Sense::hover(),
);
let painter = ui.painter().clone();
let palette = pass.immediate.palette;
// The ruler. A rule per slot and a label per tick, both the axis describing
// itself, so neither is an entry and neither is reachable.
for slot in 0..slots {
let y = rect.top() + f32::from(slot) * slot_height;
let ticked = per_tick > 0 && slot % per_tick == 0;
painter.hline(
rect.x_range(),
y,
egui::Stroke::new(
1.0,
if ticked {
palette.bevel_dark
} else {
palette.sunken
},
),
);
if ticked {
painter.text(
egui::pos2(rect.left(), y),
egui::Align2::LEFT_TOP,
tick_label(track.unit, track.span.from() + slot * track.slot),
egui::TextStyle::Body.resolve(ui.style()),
palette.content_muted,
);
}
}
// Lanes. Which lane an entry takes is a fact about how wide the box is and
// not about the day, so it is worked out here rather than described: the
// description said when things happen and `Placed::overlaps` turns that into
// who collides. Greedy first-fit, the standard day-view packing -- an entry
// takes the lowest lane no occupant of which it overlaps. O(n^2) worst case
// over a day's worth of appointments, so the interval graph is not worth its
// own bugs.
let mut lanes: Vec = Vec::with_capacity(entries.len());
for (i, entry) in entries.iter().enumerate() {
let mut lane = 0;
while entries[..i]
.iter()
.zip(&lanes)
.any(|(other, &taken)| taken == lane && entry.overlaps(other))
{
lane += 1;
}
lanes.push(lane);
}
// One width for the whole track rather than per collision cluster, which is
// the same call the webview makes and for the same reason: per-cluster is
// denser and is a layout decision either renderer can revisit without the
// description changing.
let across = lanes.iter().copied().max().map_or(1, |most| most + 1);
let body = rect.with_min_x(rect.left() + ruler);
let lane_width = body.width() / f32::from(u16::try_from(across).unwrap_or(u16::MAX).max(1));
for (entry, &lane) in entries.iter().zip(&lanes) {
let top = rect.top() + track.fraction(entry.placement.at()) * height;
let bottom = rect.top() + track.fraction(entry.placement.end()) * height;
let at = egui::Rect::from_min_size(
egui::pos2(
body.left() + f32::from(u16::try_from(lane).unwrap_or(u16::MAX)) * lane_width,
top,
),
// A placement is never zero-length, but a short one against a long
// span still rounds to nothing, and a thing that happened is worth
// a line whatever its duration.
egui::vec2(lane_width, (bottom - top).max(text_height)),
);
painter.rect_filled(at, 2.0, palette.well);
ui.scope_builder(egui::UiBuilder::new().max_rect(at.shrink(2.0)), |ui| {
// Each entry gets its own scoped `Ui`, so the position is only ever
// 0 here and the id is distinct regardless.
list_row(pass, ui, &entry.row, 0, false);
});
}
// "Show me 09:00" rather than a scroll offset, which is the whole of what
// `focus` is: the app knows the interesting moment and the renderer knows
// how to get there. Once per moment and not once per frame -- egui redraws
// continuously, and a scroll request every frame is a track the user cannot
// scroll away from.
if let Some(minute) = focus {
let id = ui.id().with("track-focus");
if ui.data(|data| data.get_temp::(id)) != Some(minute) {
ui.data_mut(|data| data.insert_temp(id, minute));
let y = rect.top() + track.fraction(minute) * height;
ui.scroll_to_rect(
egui::Rect::from_min_size(
egui::pos2(rect.left(), y),
egui::vec2(rect.width(), text_height),
),
Some(egui::Align::Center),
);
}
}
}
/// What a tick says about where it sits.
///
/// The one thing on an axis that cannot be derived from the numbers, which is
/// why `layout::Unit` exists: the geometry above is unit-agnostic and was
/// correct while the ruler printed `00:00` over a month strip.
fn tick_label(unit: layout::Unit, offset: u16) -> String {
match unit {
// Wall clock, wrapped, so a span running past midnight labels 02:00
// rather than 26:00. `Span` counts past 1440 deliberately so that it
// needs no date, and how that reads to a person is the renderer's.
layout::Unit::Minutes => format!("{:02}:{:02}", (offset / 60) % 24, offset % 60),
// Day one, not day zero. A strip's offsets are zero-based like every
// other axis here and nobody calls the first of the month the zeroth.
layout::Unit::Days => format!("{}", offset + 1),
// A unit added later lands here rather than silently taking the clock,
// which is exactly how the clock-over-a-month defect shipped.
_ => String::new(),
}
}
/// The nodes that hold other nodes, or that a screen's own facts reach into.
///
/// Split from [`draw`] where the line fell naturally rather than to satisfy a
/// lint: everything above is a leaf that needs the palette and nothing else,
/// and everything here needs the view, the selection or a nested walk.
fn container(pass: &mut Pass<'_>, ui: &mut Ui, node: &Node) {
match node {
Node::Field(described) => {
field_node(pass, ui, described);
}
Node::Region(slot) => {
region(pass, ui, slot);
}
Node::Form {
fields,
submit,
action,
..
} => {
form(pass, ui, fields, submit, action);
}
// **A table that declared no columns is a list, and draws as one.** One
// node since the 2026-09-06 collapse; the guard is where the two
// arrangements part company in this renderer, and the arm below is the
// grid. Both hold the same `Row`.
Node::Table {
columns,
rows,
more,
..
} if columns.is_empty() => {
// A row a shut branch covers is not drawn at all, which is the
// whole of what folding is. `quasi_router::folded` reads it, so a
// window and a terminal fold the same rows.
//
// The chevron column is spent on every row of a list that holds a
// branch, leaf or not, so the labels line up under each other.
let branches = rows.iter().any(|row| row.open.is_some());
let shown = unfolded(rows, pass.view);
for (within, row) in rows.iter().enumerate() {
// The row's own place among its siblings, kept even when a
// branch above it is shut: it names this row's hit target, and
// renumbering on a fold would hand one row the target of
// another.
if !shown.iter().any(|kept| std::ptr::eq(*kept, row)) {
continue;
}
list_row(pass, ui, row, within, branches);
}
if let Some(rest) = more {
rest_controls(pass, ui, rest);
}
}
Node::Table {
columns,
rows,
more,
..
} => {
table(pass, ui, columns, rows);
// Under the table and belonging to it, the same row of controls a
// list gets.
if let Some(rest) = more {
rest_controls(pass, ui, rest);
}
}
Node::Timeline {
track,
entries,
focus,
..
} => {
timeline(pass, ui, *track, entries, *focus);
}
Node::Stats { figures, .. } => {
// Across rather than down, which is the one thing a strip says: a
// terminal stacks them because it has no width to spare, and a
// window does.
ui.horizontal(|ui| {
for (figure, address) in figures {
let shown = widget::figure(
ui,
&figure.as_layout(),
&pass.immediate.palette,
&pass.immediate.widget,
);
// The one of goingson's five figure sites that renders its
// value as a button: the description's half is the
// vocabulary's and the optional address is quasi's.
if let Some(action) = address
&& shown.interact(egui::Sense::click()).clicked()
{
pass.fire(action, Params::new(), None);
}
}
});
}
// Every leaf is answered by `draw`, which is exhaustive, so this
// reaches nothing. It is here because the split is this crate's and
// not the vocabulary's: `Node` still has no wildcard anywhere, and a
// member added upstream still stops `draw` compiling.
_ => unreachable!("a leaf reached the container walk"),
}
}
/// A control, with whatever the screen wants gathered behind it.
fn act_node(pass: &mut Pass<'_>, ui: &mut Ui, act: &Act) {
// The set it acts on, read off the control. This was a parameter until
// 2026-08-20 and both callers passed `None`, so `Act::over` reached this
// renderer and did nothing: the count was never drawn, the control over an
// empty set stayed live, and the ticks never left with the call. The two
// other renderers read the member directly, which is why neither drifted.
let over = act.over.as_deref();
// What the press asks for before it fires, drawn above the control. A
// webview hides these behind the verb in a `details`; there is no such
// affordance here that would not be a popup this renderer opened and closed
// on its own, and an immediate-mode screen redraws every frame anyway, so
// the boxes stand in the open. `as_asked` drops the write members: the value
// is answered by the control below, not by a route of the box's own.
for field in &act.asks {
field_node(pass, ui, &field.as_asked());
}
// A control over the screen's selection says how many it would act on, and
// is disabled while that is none. Neither is sayable in the description: the
// ticks are the host's until something submits them, so a screen built from
// the store cannot know the number, and a commit control over an empty set
// is otherwise offered, pressed, and answers "0 tasks completed" -- a screen
// letting the user find out by trying.
//
// Disabled rather than hidden. `bulk-actions.js` hides its bar and can
// afford to, because its rows keep their checkboxes; a bar that vanishes
// takes with it the only evidence that bulk actions exist.
let chosen = over.map(|_| pass.view.ticks().count());
let label = match chosen {
Some(0) | None => act.label.clone(),
Some(chosen) => format!("{} ({chosen})", act.label),
};
// A control that has been pressed and not answered yet is drawn as what it
// is: working, and not taking another press. The label is untouched, so the
// control does not change width the moment it is pressed, and disabled is
// the guard as well as the saying here -- a disabled widget reports no
// click, so the second press does not exist rather than being discarded.
let busy = pass.view.busy(&act.action);
let described = layout::Act {
label: &label,
key: act.key.as_deref(),
tone: act.tone,
state: if chosen == Some(0) || busy {
Some(layout::State::Disabled)
} else {
act.state
},
// Carried across since makeover-layout 0.40.0, so `widget::act` draws
// the hover this function used to apply to its answer.
hint: act.hint.as_deref(),
};
// `db998898`. What the control shows, above what it says, which is the tile
// shape both measured sites have. Drawn here rather than handed down for
// `Act::hint`'s reason: `makeover_layout::Act` carries no picture, that
// crate declares `links`, and a member there is one version bump across 25
// manifests in 12 repos.
//
// Grouped so the picture and the button are one item in the flow and the
// press stays the button's: a picture that answered a click would be this
// renderer inventing an affordance the description did not ask for, since
// `shows` says what a control looks like and never that the picture is a
// second control.
let pressed = match &act.shows {
Some(picture) => {
ui.vertical(|ui| {
image(pass.immediate, ui, picture);
widget::act(
ui,
&described,
&pass.immediate.palette,
&pass.immediate.widget,
)
})
.inner
}
None => widget::act(
ui,
&described,
&pass.immediate.palette,
&pass.immediate.widget,
),
};
// `ca7b5200`. Standing help, as a hover -- honest on this host in a way it
// is not on a terminal, because egui has a pointer.
//
// `widget::act` does it since makeover-layout 0.40.0 moved `hint` down, so
// nothing is applied here any more. It was applied outside the widget for
// three releases because `makeover_layout::Act` carried no hint, and the
// consequence was that a makeover host that was not quasi could not say it.
//
// `ae8e8836`. Where this control was drawn, for an `Outcome::Anchored` that
// names it. Only a named control is noted -- `Act::id` is `None` on nearly
// all of them -- so a screen anchoring nothing pays one `Option` check per
// control and no allocation.
if let Some(id) = &act.id {
crate::geometry::note_act(ui, id, pressed.rect);
}
if pressed.clicked() && chosen != Some(0) && !busy {
let mut payload = gathered(pass, over);
payload.absorb(asked(pass, &act.asks));
deposit(pass, act);
// `c3e145e0`. This host is the one of the three that needs no help: egui
// owns the clipboard already, so the copy happens here rather than
// reaching the host as a webview script or a `quasi_tui::Step`.
//
// Beside `deposit` and before the call, matching the other two: the
// local half of a press happens whether or not anything is asked, and a
// copying act asks nothing -- its action is local, so `fire` below has
// no route to call.
if let Some(value) = &act.copies {
ui.ctx().copy_text(value.clone());
}
pass.fire(&act.action, payload, act.confirm.as_deref());
}
}
/// Put the act's value into the box it named.
///
/// [`Act::fills`](quasi_router::Act::fills) names a field on the same screen
/// and the renderer decides where in it the value lands; here that is the end
/// of what is already there. egui holds a text cursor per widget and this
/// deliberately does not reach for it: the description names a destination and
/// never a position, and a value that arrives at the end is what the
/// vocabulary calls correct rather than a fallback.
///
/// After the payload is gathered, matching the other two renderers: a webview's
/// htmx listener sits on the control and its fill script on the document, so
/// there the press sends what the boxes held before it. One description sending
/// two different things on two hosts is the drift this stack exists to end.
///
/// From the box's own buffer, which is what it is showing: [`View::buffer`]
/// seeds one from the description the first time the field is drawn, and a
/// control cannot be pressed on a frame before the screen holding it was drawn.
/// So a deposit into a box somebody has typed in goes after their typing, and
/// one into an untouched box goes after whatever the description offered rather
/// than over it.
///
/// [`View::buffer`]: crate::View::buffer
fn deposit(pass: &mut Pass<'_>, act: &Act) {
let Some(fill) = &act.fills else {
return;
};
let mut value = pass.view.edit(&fill.field).unwrap_or_default().to_owned();
value.push_str(&fill.value);
pass.view.set(&fill.field, value);
}
/// A checkbox, which is the one field held as a bool rather than as text.
///
/// Split out of [`field_node`] because it shares none of the rest: no buffer to
/// outlive the frame, no consults, no suggestion list. `Node::SELECTED` is
/// quasi's submission convention, so a tick travels as a string on the way out
/// and is a bool only while it is on screen.
fn checkbox_node(
pass: &mut Pass<'_>,
ui: &mut Ui,
described: &quasi_router::Field,
name: &str,
offered: bool,
) {
let mut on = pass
.view
.edit(name)
.map_or(offered, |value| !value.is_empty());
let before = on;
described.with_layout(|borrowed| {
field(
ui,
&borrowed,
Filling::On(&mut on),
None,
&pass.immediate.palette,
&pass.immediate.field,
);
});
if on == before {
return;
}
pass.view.set(name, if on { "on" } else { "" });
// A tick is a value that moved, so a region recomputing from a set of dials
// hears it the same way it hears a typed one.
pass.stirred.insert(name.to_owned());
if let Some(action) = &described.writes {
let mut payload = Params::new();
payload.insert(name.to_owned(), if on { "on" } else { "" }.to_owned());
pass.fire(action, payload, None);
}
}
/// The upper end of an interval moved.
///
/// Its own function rather than a branch inside [`field_node`], which is long
/// enough already, and the split falls where the two ends genuinely differ:
/// only this one has to be remembered under a second name.
///
/// It fires the same [`quasi_router::Field::writes`] the lower end does. A
/// filter that moved is a filter that moved, whichever box the user dragged,
/// and both ends travel in the payload because an interval is one answer -- a
/// handler reading only the end that moved would narrow the filter to a single
/// bound every time either box was touched.
fn upper_moved(
pass: &mut Pass<'_>,
described: &quasi_router::Field,
lower: (&str, &str),
upper: (&str, &str),
) {
let (lower_name, lower_value) = lower;
let (upper_name, upper_value) = upper;
pass.view.set(upper_name, upper_value.to_owned());
let Some(action) = &described.writes else {
return;
};
let mut payload = Params::new();
payload.insert(lower_name.to_owned(), lower_value.to_owned());
payload.insert(upper_name.to_owned(), upper_value.to_owned());
pass.fire(action, payload, None);
}
/// A question answered zero or more times: the slots, and the controls the
/// reader adds and removes them with.
///
/// Each slot is [`quasi_router::Field::instance`] put back through
/// [`field_node`], so a slot is drawn by everything this renderer already does
/// to a field and there is no second field emitter.
///
/// How many slots stand is the view's, for the reason the buffers are: the
/// description says how many answers it was given, and how many boxes there are
/// now is a fact about what the reader has done since. Adding one asks nothing
/// and neither does taking one away.
fn repeat_node(pass: &mut Pass<'_>, ui: &mut Ui, described: &quasi_router::Field) {
let Some(repeat) = described.repeats.clone() else {
return;
};
ui.label(RichText::new(described.label.as_str()).color(pass.immediate.palette.content));
// What is wrong with the *set*, which no slot's own message can carry. A
// slot's error rides on the slot, through the field it is drawn as.
if let Some(error) = &described.error {
ui.label(
RichText::new(error.as_str()).color(pass.immediate.palette.tone(layout::Tone::Danger)),
);
}
let standing = pass.view.standing(described);
for at in 0..standing {
// One box for an ordinary slot, one per part for a grouped one, and
// the branch lives in the vocabulary rather than here so the three
// renderers cannot disagree about what a slot is.
let slots = described.instance_fields(at);
let held = repeat.instances.get(at);
let busy = held.is_some_and(|slot| slot.progress.busy());
ui.horizontal(|ui| {
for slot in &slots {
field_node(pass, ui, slot);
}
// A slot the work has not finished with is one the reader may not
// pull out from under it.
if repeat.fewer(standing)
&& !busy
&& ui.button(RichText::new(repeat.remove.as_str())).clicked()
{
pass.view.remove_slot(described, at);
}
});
// What is wrong with the slot as a whole, and how far the work on it
// has got. A part's own message rides on the part, as a field's does.
if let Some(held) = held {
if let Some(error) = &held.error {
ui.label(
RichText::new(error.as_str())
.color(pass.immediate.palette.tone(layout::Tone::Danger)),
);
}
if let quasi_router::Progress::Working(Some(meter)) = &held.progress {
widget::meter(
ui,
&meter.as_layout(),
&pass.immediate.palette,
&pass.immediate.widget,
);
}
}
}
// Nothing for a question whose slots come from another control, for the
// reason the other two renderers draw nothing: there is no blank a reader
// could fill.
if let Some(label) = repeat.add.label().filter(|_| repeat.more(standing))
&& ui.button(RichText::new(label)).clicked()
{
pass.view.add_slot(described);
}
}
/// One field, filled from the view rather than from the description.
/// Move each of a field's consult deadlines on, or cancel it.
///
/// Split out of [`field_node`] while that was over clippy's line bound, and it
/// is a whole idea on its own: a keystroke pushes every question this box raises
/// further out, which is what makes it a debounce.
fn push_deadlines(pass: &mut Pass<'_>, described: &quasi_router::Field, name: &str, buffer: &str) {
for (at, consult) in questions(described) {
if consult.asks_about(buffer) {
pass.view.wait_to_consult(
Asking::Field(name.to_owned()),
at,
std::time::Instant::now() + consult.after,
);
} else {
// Deleting back under the floor cancels a question that was already
// waiting, rather than letting it fire against a value the
// description says is too short to ask about.
pass.view.consulted(Asking::Field(name.to_owned()), at);
}
}
}
fn field_node(pass: &mut Pass<'_>, ui: &mut Ui, described: &quasi_router::Field) {
// A question that does not apply is left out, which is what this renderer
// already does with a region that does not. What was typed into it is kept
// in the view and still submitted, exactly as a browser sends a hidden
// input. `8fdb814c`.
if pass.hidden.field_out(&described.name) {
return;
}
let name = described.name.clone();
let kind = described.kind;
let offered = described.value.clone();
// A question answered N times is N slots, each an ordinary field of this
// same function. `60d1753c`.
if described.repeats.is_some() {
repeat_node(pass, ui, described);
return;
}
if kind == layout::FieldKind::Checkbox {
checkbox_node(pass, ui, described, &name, offered.is_some());
return;
}
// The buffer has to outlive the frame, so it is the view's. Taken out and
// put back rather than borrowed across the closure, because the closure
// also needs the palette off `pass`.
let mut buffer = pass.view.buffer(&name, offered.as_deref()).clone();
let before = buffer.clone();
// An interval edits two buffers under the two names it submits under. Its
// upper end is a second buffer of the view's for the same reason the first
// one is: what is being typed outlives the frame.
let upper_name = described
.upper_name
.clone()
.filter(|_| kind == layout::FieldKind::Interval);
let mut upper = upper_name.as_ref().map(|upper_name| {
pass.view
.buffer(upper_name, described.upper_value.as_deref())
.clone()
});
let upper_before = upper.clone();
let width = described.width;
let response = sized(ui, width, |ui| {
described.with_layout(|borrowed| {
let filling = match upper.as_mut() {
Some(upper) => Filling::Between {
lower: &mut buffer,
upper,
},
None => Filling::Text(&mut buffer),
};
field(
ui,
&borrowed,
filling,
None,
&pass.immediate.palette,
&pass.immediate.field,
)
})
});
// Where the caret starts, on the arrival frame and no other. Focus is
// egui's here, so this is a request rather than a placement, and the claim
// is taken rather than read: a flag left standing would ask again every
// frame and the reader could never move the caret off the box. See
// `View::claims_caret`.
if let Some(response) = response.as_ref()
&& pass.view.claims_caret(&name)
{
response.request_focus();
}
if upper_before != upper
&& let Some((upper_name, upper)) = upper_name.as_ref().zip(upper.as_ref())
{
// An interval's upper end is a value of its own, under its own name,
// and moving it moves the interval.
pass.stirred.insert(upper_name.clone());
upper_moved(pass, described, (&name, &buffer), (upper_name, upper));
}
if buffer != before {
pass.view.set(&name, buffer.clone());
// Said once here, read by every region that contains this box. A field
// has no way to know which regions those are and a region has no way to
// know its body moved, so the two meet on the frame.
pass.stirred.insert(name.clone());
// Every keystroke pushes the deadline out, which is what makes this a
// debounce: a slug typed in one go is asked about once, when it stops
// moving. Per keystroke and NOT on settle, unlike `changes` below: a
// question asked while typing is the whole point of a consult, and
// `Consult::after` is the description's own wait.
//
// Each question keeps its own deadline. A box asking two routes at two
// rates is MNW's discover search, and one deadline per field would have
// the faster of the two cancel the slower.
push_deadlines(pass, described, &name, &buffer);
}
// `8032fe61`, `de2376bd`. **When the value is complete, not on the way to
// it.** This fired on `buffer != before`, so typing 30 into audiofiles'
// bounded `row_height` posted a row height of 3 on the way -- outside the
// 20-32 the field's own hint states -- and dragging a classifier threshold
// wrote once per frame instead of once per drag.
//
// `quasi-webview` has always meant this: `Field::writes` is emitted as
// `hx-trigger="change"`, and a browser raises `change` on blur or Enter for
// a text control and on release for a range. Two shipped renderers
// disagreeing about one member is the drift this stack exists to end, and
// the host that was already right says what right is. Ruling on
// quasicoherent `8032fe61`.
//
// What is NOT settled here is whether these controls should write with no
// submit at all, which is Max's rule in wiki `explicit-commit-affordance`
// and is that task's remaining half.
let settled = response
.as_ref()
.is_some_and(|response| response.lost_focus() || response.drag_stopped());
// A control whose value is chosen in one go was already complete when it
// changed, and a browser fires `change` for it immediately. Only what the
// reader builds up -- typed into or dragged across -- has an "on the way".
// A theme is picked in one gesture, the way an option is.
let built_up = !(kind.offers_options() || kind.takes_files() || kind.offers_themes());
let complete = if built_up { settled } else { buffer != before };
if complete
&& described.writes.is_some()
&& pass.view.unwritten(&name, &buffer, offered.as_deref())
{
pass.view.wrote(&name, &buffer);
if let Some(action) = &described.writes {
let mut payload = Params::new();
payload.insert(name.clone(), buffer.clone());
// Both ends, because an interval is one answer. A handler reading
// only the end that moved would narrow the filter to a single
// bound every time either box was touched.
if let Some((upper_name, upper)) = upper_name.as_ref().zip(upper.as_ref()) {
payload.insert(upper_name.clone(), upper.clone());
}
pass.fire(action, payload, None);
}
}
// Candidates for a value that no longer clears the floor are an answer to a
// question the field would not ask now, so a delete takes them with it.
if let Some(owned) = described.suggests.as_ref()
&& !owned.asks_about(&buffer)
{
pass.view.unsuggest();
}
// Checked every frame rather than only on a keystroke, because the whole
// point is what happens when the keystrokes stop.
for (at, consult) in questions(described) {
let Some(due) = pass.view.consult_due(Asking::Field(name.clone()), at) else {
continue;
};
let now = std::time::Instant::now();
if now >= due {
pass.view.consulted(Asking::Field(name.clone()), at);
// This box's value, plus whatever else the question said it
// carries. No ticks: a consult asks about this box, never about a
// set of rows.
let mut payload = pass.view.contributed(&consult.sends);
payload.insert(name.clone(), buffer.clone());
pass.fire(&consult.action, payload, None);
} else {
// An idle app stops repainting, and a deadline nobody wakes up for
// is a question never asked. This is the one place the renderer
// needs a clock, and egui already owns one.
ui.ctx().request_repaint_after(due - now);
}
}
suggestions(pass, &name, &mut buffer, ui);
}
/// Every question this field asks, the one it owns first.
///
/// The owned question is keyed just past the end of
/// [`Field::consults`](quasi_router::Field::consults), so it gets a deadline of
/// its own for the reason each consult does — a box asking two routes at two
/// rates would otherwise have the faster question cancel the slower — and no
/// index can collide with a consult's.
fn questions(field: &quasi_router::Field) -> impl Iterator- {
field.consults.iter().enumerate().chain(
field
.suggests
.iter()
.map(|owned| (field.consults.len(), owned)),
)
}
/// The candidates under the box, and what picking one does.
///
/// Drawn where the list is described to be — under the field that owns it, in
/// flow — because an immediate-mode frame has no z-order to float in and a
/// popup here would be a window this renderer opened on its own. The webview
/// floats and the terminal paints over; each host's answer is its own, and
/// what they share is the description.
///
/// Picking writes [`Candidate::value`](quasi_router::Candidate::value) into the
/// box and closes the list. It costs exactly what a settled value costs, which
/// is why a [`Field::writes`](quasi_router::Field::writes) route fires: the
/// buffer this writes into is compared against its previous contents on the
/// next frame, the same way a keystroke is, so no second write path exists here
/// to keep in step.
///
/// That is the default and not the definition. A candidate carrying
/// [`picks`](quasi_router::Candidate::picks) has that action fired instead,
/// and nothing is written -- which is the whole of MNW's search box, where a
/// pick navigates and the typed value is discarded.
///
/// [`detail`](quasi_router::Candidate::detail) is drawn after the label and
/// dimmed, which is this host's answer to the same question the terminal
/// answers with the rest of the row and a webview with a second line.
fn suggestions(pass: &mut Pass<'_>, name: &str, buffer: &mut String, ui: &mut egui::Ui) {
let Some(options) = pass.view.suggesting(name) else {
return;
};
// Cloned rather than borrowed across the loop: the list is read off the
// view and picking writes back to it, and a candidate is two short strings.
let options = options.to_vec();
let mut picked = None;
for candidate in options {
let mut text = egui::text::LayoutJob::default();
text.append(
&candidate.label,
0.0,
egui::TextFormat::simple(egui::FontId::default(), ui.visuals().text_color()),
);
if let Some(detail) = &candidate.detail {
text.append(
detail,
8.0,
egui::TextFormat::simple(egui::FontId::default(), ui.visuals().weak_text_color()),
);
}
if ui.add(egui::Button::new(text).frame(false)).clicked() {
picked = Some(candidate.clone());
}
}
let Some(candidate) = picked else {
return;
};
pass.view.unsuggest();
// Performed as written, with no payload rule invented here: the action
// carries its own params and the view it was offered under, exactly as a
// control's does.
if let Some(action) = candidate.picks {
pass.fire(&action, Params::new(), None);
return;
}
buffer.clone_from(&candidate.value);
pass.view.set(name, candidate.value);
}
/// How wide a control asks to be, in the one unit egui takes.
///
/// `Fill` is what an egui text field does already, and is the default, so a
/// described field that says nothing draws exactly as it did before the member
/// existed: `TextEdit`'s desired width is infinite, so in a row it takes what is
/// left.
///
/// The other two need a number the description does not carry, which is the same
/// place `Column` leaves this renderer and the terminal one -- see `quasi-tui`'s
/// table sizing, whose `fallback` is a guess "until the vocabulary carries a
/// measure". This is that guess for a field: a guess rather than nothing,
/// because the alternative is `Content` and `Fill` drawing identically, which
/// would make the member unfalsifiable here.
const ASKED: f32 = 220.0;
/// Draw within whatever the control asked for.
fn sized(ui: &mut Ui, width: layout::Width, draw: impl FnOnce(&mut Ui) -> R) -> R {
match width {
layout::Width::Fill => draw(ui),
_ => {
ui.scope(|ui| {
ui.set_max_width(ASKED.min(ui.available_width()));
draw(ui)
})
.inner
}
}
}
/// The values a control asked for before it fired, read as a submit reads them.
///
/// `Act::asks`. Empty for a control that asked for nothing, which is nearly all
/// of them.
fn asked(pass: &Pass<'_>, asks: &[quasi_router::Field]) -> Params {
if asks.is_empty() {
return Params::new();
}
let (names, described) = submitted(pass, asks);
pass.view.submission(&names, &described)
}
/// The names a set of fields submits under, and what the description offered
/// for each.
///
/// One name per field, except for a question answered N times, which is N
/// names: `60d1753c`. How many those are is the view's, because the slots the
/// reader added are not in the description, which is why this is not a
/// function of the fields alone.
fn submitted(
pass: &Pass<'_>,
fields: &[quasi_router::Field],
) -> (Vec, BTreeMap) {
let mut names = Vec::new();
let mut described = BTreeMap::new();
for field in fields {
for at in 0..pass.view.standing(field) {
let slots = if field.repeats.is_some() {
field.instance_fields(at)
} else {
vec![field.clone()]
};
for slot in slots {
if let Some(value) = slot.value {
described.insert(slot.name.clone(), value);
}
names.push(slot.name);
}
}
}
(names, described)
}
/// The ticks a control writing over a selection sends, and nothing for one that
/// does not.
///
/// Under `Node::TICKED`, which is the name every renderer sends them under and
/// what the vocabulary documents.
fn gathered(pass: &Pass<'_>, over: Option<&str>) -> Params {
over.map_or_else(Params::new, |_| {
pass.view.gathering(quasi_router::Node::TICKED)
})
}
/// A form: its fields, then the one control that answers all of them.
fn form(
pass: &mut Pass<'_>,
ui: &mut Ui,
fields: &[quasi_router::Field],
submit: &str,
action: &Action,
) {
for described in fields {
field_node(pass, ui, described);
}
if ui.button(RichText::new(submit)).clicked() {
// A question answered N times sends N values under N indexed names, in
// one submission with the rest of the form. That is the whole of what
// the member is for. `60d1753c`.
let (names, described) = submitted(pass, fields);
let payload = pass.view.submission(&names, &described);
pass.fire(action, payload, None);
}
}
/// The rows a shut branch is not covering, as the reader has left the outline.
///
/// [`quasi_router::folded_by`] does the reading, so a window folds what a
/// terminal and a browser fold. The reader's own answer comes first and the
/// description's stands until there is one -- `Row::open` says where an outline
/// starts, not where it stays.
fn unfolded<'a, T: quasi_router::Outline>(rows: &'a [T], view: &crate::View) -> Vec<&'a T> {
let hidden = quasi_router::folded_by(rows.iter().map(|row| {
let open = row.open().map(|described| view.open(&row.key(), described));
(row.depth(), open)
}));
rows.iter()
.zip(hidden)
.filter(|(_, hidden)| !*hidden)
.map(|(row, _)| row)
.collect()
}
/// A row's place in an outline, drawn before its words: the indent, then the
/// chevron when the row is a branch.
///
/// Answers the key and the state a press would flip, so a caller can collect
/// the fold and apply it once the pass is free. The chevron's rect comes back
/// with it, because a caller claiming the row's own hit target has to start
/// after it -- egui gives an overlapping rect to whichever widget claimed it
/// last, and a chevron the row swallows is the one thing `Row::open` says must
/// not happen.
///
/// The room is spent on a leaf too, or its label sits left of its own parent's.
fn outline_lead(
ui: &mut Ui,
view: &crate::View,
row: &impl quasi_router::Outline,
) -> (Option<(String, bool)>, Option) {
let step = ui.spacing().indent;
let depth = f32::from(row.depth().level);
if row.depth().is_nested() {
ui.add_space(step * depth);
}
let Some(described) = row.open() else {
ui.add_space(step);
return (None, None);
};
let key = row.key();
let open = view.open(&key, described);
let pressed = ui.small_button(if open { "\u{25bc}" } else { "\u{25b6}" });
let folded = pressed.clicked().then_some((key, open));
(folded, Some(pressed.rect))
}
/// One row of a list.
///
/// `within` is the row's place among its siblings, and it is there only to name
/// the row's interact rect. See the id below for why the value cannot do it
/// alone.
fn list_row(pass: &mut Pass<'_>, ui: &mut Ui, row: &Row, within: usize, branches: bool) {
// Whether the press that just happened was on the chevron rather than on
// the row. Collected here because the row claims the whole strip below,
// and a fold that also opened the row would be one press doing two things.
let mut folded = None;
// Where the chevron landed, so the row's own hit target can start after it.
// egui gives an overlapping rect to whichever widget claimed it last, and
// the row claims the whole strip below -- so without this the row swallows
// every press meant for the chevron, which is the one thing `Row::open`
// says must not happen.
let mut chevron = None;
let strip = ui.horizontal(|ui| {
// The row's own indent, then its disclosure. `ccaa7e4b`, and the
// chevron is **a separate hit target from the label** on purpose: this
// is the shipped egui sidebar's own behaviour rather than an
// improvement on it, since pressing a tag filters by it and pressing
// its chevron does not.
if branches {
(folded, chevron) = outline_lead(ui, pass.view, row);
}
// The tick, where the row can carry one. `toggle` first, because a row
// carrying one has said the tick *is* the write and that beats the
// screen's staged set.
if let Some(ticked) = row.selected {
let mut on = row
.value
.as_ref()
.map_or(ticked, |value| pass.view.is_ticked(value));
if ui.checkbox(&mut on, "").changed() {
if let Some(action) = &row.toggle {
pass.fire(action, Params::new(), None);
} else if let Some(value) = &row.value {
pass.view.tick(value);
}
}
}
// `Part::worth` is not read here, and unlike `Flow` that is a statement
// about this renderer rather than a deferral. A terminal wraps one
// shared flow, so a trailing fact costs the row a whole extra line and
// dropping it by worth buys back that line. Here the parts are a
// horizontal strip and each elides itself, so nothing is lost to *the
// run* being too tall -- what is lost is whatever sits past the right
// edge once an earlier part has taken the width. That is a budget
// problem rather than a worth problem, and `budget` below is the
// answer to it.
let spacing = ui.spacing().item_spacing.x;
for (index, part) in row.cells.iter().enumerate() {
let after = tail_width(ui, pass.immediate, &row.cells[index + 1..], spacing);
row_part(pass, ui, part, after);
}
});
// Where this row landed, for a host reading a gesture the description does
// not carry. Noted for every row rather than only pressable ones: "is the
// pointer over a row" has to be answerable for the row that answers
// nothing, which is the case a drag guard needs. See `crate::geometry`.
crate::geometry::note_row(
ui,
row.value.as_deref(),
within,
strip.response.rect,
row.chosen == Some(true),
);
// Opening the row, and asking what else it offers, both land on the strip
// the row just drew.
//
// **The rect is the `horizontal`'s own response, not `ui.min_rect()`.** That
// was the rect until 2026-08-17, and every row of a list is drawn into one
// shared `Ui` -- a column-less table loops `list_row` over it -- so `min_rect` grew
// with each row and the fourth row's target covered the first four. Four
// overlapping rects, one pointer, and the row that answered was whichever
// egui hit last rather than the one under the cursor. The `horizontal`
// answers this row's strip and nothing above it.
//
// One `interact` for both gestures, claimed whenever the row has either: a
// row that only offers a menu still needs somewhere to right-click.
// `Sense::click()` covers the secondary button -- egui reads a context click
// off the same sense -- so nothing about the opening gesture changes.
// The fold, applied after the strip: the view is behind the pass, which the
// closure above cannot hold. `folded` also stands in for "the press was the
// chevron's", so the row below is not opened by it.
if let Some((key, open)) = folded {
pass.view.fold(&key, open);
return;
}
if row.activate.is_some() || !row.menu.is_empty() {
// **The id falls back to the row's position, and has to.** It was
// `("row", value.unwrap_or(""))` until 2026-08-17, and `Row::value` is
// only set by `ticking`, so every row of a list without checkboxes was
// `("row", "")` -- one id for all of them. egui answers a repeated id
// with a "First use / Second use" error label and one shared
// interaction, so the second row onwards could not be clicked at all.
// Sibling of the `min_rect` defect fixed above and invisible for the
// same reason: nothing here could press a row until this file could
// measure text.
//
// The value still wins where there is one, because it survives a
// reorder and an index does not.
let id = match row.value.as_deref() {
Some(value) => ui.id().with(("row", value)),
None => ui.id().with(("row-at", within)),
};
// Everything the row drew except its chevron. See `chevron` above.
let mut target = strip.response.rect;
if let Some(chevron) = chevron {
target.min.x = target.min.x.max(chevron.max.x);
}
let response = ui.interact(target, id, egui::Sense::click());
// Say what was claimed, so the row exists for something other than a
// pointer.
//
// A bare `ui.interact` registers no `WidgetInfo`, and egui builds its
// accessibility tree from `WidgetInfo` alone, so until 2026-08-22 a row
// that opens or carries a menu contributed **no node at all**. Not
// mislabelled: absent. It worked under a mouse and did not exist for a
// keyboard or a screen reader, which on audiofiles' tag queue meant
// there was no way but the mouse to open one of the tag groups the
// screen navigates by. Found by a harness reading what the renderer
// drew.
//
// Named by the row's first text, which is the same thing a reader would
// call the row and the same thing `Row::new` takes.
let named = row_name(row);
response.widget_info(|| {
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &named)
});
// The menu first. A right-click lands on the same rect as a left-click,
// and firing `activate` off it would open the row the user was asking
// what they could do to.
if let Some((action, confirm)) = row_menu(pass.immediate, &response, &row.menu) {
pass.fire(&action, Params::new(), confirm.as_deref());
} else if let Some(action) = &row.activate
&& response.clicked()
{
pass.fire(action, choosing(ui, row.chosen), None);
}
}
}
/// Say that a table row can be opened, once, on its first column.
///
/// The press is claimed per cell, and saying so per cell would put a button in
/// the tree for every column of every row -- five buttons all called "kick.wav"
/// on a five-column table, which is noise rather than access. The first column
/// is where the row's name already is, so that is where it is announced;
/// pressing any cell still opens it, exactly as before.
///
/// Second half of `461582b5`. The list half is [`row`], where a row has one
/// rect and one place to say this.
fn announce_row(ui: &Ui, response: &egui::Response, index: usize, row: &Row) {
if index != 0 || (row.activate.is_none() && row.menu.is_empty()) {
return;
}
let named = row_name(row);
response.widget_info(|| {
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &named)
});
}
/// What a table row is called: the first thing in its first cell.
/// The first thing a run of leaves says.
fn row_leaf_text(parts: &[Node]) -> String {
parts
.iter()
.find_map(|part| match part {
Node::Text { text, .. } | Node::Heading { text, .. } | Node::Link { text, .. } => {
Some(text.clone())
}
_ => None,
})
.unwrap_or_default()
}
/// What a row is called: the first thing it says.
///
/// For [`egui::WidgetInfo`], which needs one string where a row is a run of
/// leaves. The first textual part is what `Row::new` takes and what a reader
/// would call the row, so there is nothing to invent here.
///
/// One function since the 2026-09-05 collapse. `cells_name` was its counterpart
/// for the other container and asked the same question of the same type, cell
/// by cell rather than over a flattened copy. This keeps the cell-by-cell
/// walk: it is the one that does not clone every node in the row, and this
/// runs per frame.
fn row_name(row: &quasi_router::Row) -> String {
row.cells
.iter()
.find_map(|cell| {
let said = row_leaf_text(&cell.content);
(!said.is_empty()).then_some(said)
})
.unwrap_or_default()
}
/// One part of a row's run.
///
/// A part is a role and a node, so the drawing is `draw` again: the role says
/// where it sits in the run and the node says what it is. That is the property
/// the containment migration bought every renderer, and it is why a row does not
/// need a second switch over member types here.
fn row_part(pass: &mut Pass<'_>, ui: &mut Ui, part: &quasi_router::Cell, after: Option) {
// The cap, settled by `7bfb554a`: a run is a line, and a part takes the
// lines its flow allows and no more. Only the text leaves need it -- a
// token, a control and a meter are single-line widgets already, and putting
// them through a galley would be saying something about them that the
// description did not.
// A cell holds a run since the 2026-09-05 collapse, so this walks it. The
// flow is the cell's, which is the column's unless the description narrowed
// it, and it applies to every text leaf in the run.
let flow = part.room();
for node in &part.content {
match node {
Node::Text { text, .. } => capped(pass.immediate, ui, text, flow, after),
Node::Rich { source, .. } => {
capped(
pass.immediate,
ui,
&docengine::render_plain(source),
flow,
after,
);
}
other => draw(pass, ui, other),
}
}
}
/// What the rest of the run needs, if this crate can say.
///
/// `None` the moment one part cannot be measured. A budget built on a guess
/// about an unknown widget is worse than no budget: too small and the flexible
/// part is elided for room nobody used, too large and the guess bought nothing.
/// The parts that can be measured are the ones whose width is their text --
/// which is every part a row has ever held in practice.
fn tail_width(
ui: &Ui,
immediate: &Immediate,
rest: &[quasi_router::Cell],
spacing: f32,
) -> Option {
if rest.is_empty() {
return None;
}
let mut total = 0.0;
for part in rest {
total += intrinsic_width(ui, immediate, part)? + spacing;
}
Some(total)
}
/// The width a cell wants when nothing is squeezing it.
///
/// A cell holds a run since the 2026-09-05 collapse, so this is the sum of its
/// leaves. `None` if any leaf cannot say, which is the same answer the single
/// node gave before: a run containing something unmeasurable is unmeasurable.
fn intrinsic_width(ui: &Ui, immediate: &Immediate, part: &quasi_router::Cell) -> Option {
let mut total = 0.0;
for node in &part.content {
total += leaf_width(ui, immediate, node)?;
}
Some(total)
}
/// The width one leaf wants when nothing is squeezing it.
fn leaf_width(ui: &Ui, immediate: &Immediate, node: &Node) -> Option {
match node {
Node::Text { text, .. } => Some(text_width(ui, text)),
Node::Rich { source, .. } => Some(text_width(ui, &docengine::render_plain(source))),
// A token is its words plus `token_padding` on both sides, which is
// `widget::token`'s own arithmetic rather than a guess at it. Taking
// the button padding here instead would be close enough to look right
// and wrong enough to lose the last part in the run.
Node::Token(tag) => {
Some(text_width(ui, &tag.label) + immediate.widget.token_padding.x * 2.0)
}
Node::Act(act) => {
let drawn = act.as_layout();
let label = match drawn.key {
Some(key) => format!("{} ({key})", drawn.label),
None => drawn.label.to_owned(),
};
// A control is an `egui::Button`, so this padding really is the
// style's button padding.
Some(text_width(ui, &label) + ui.spacing().button_padding.x * 2.0)
}
_ => None,
}
}
/// How wide this text is laid out with nothing in its way.
fn text_width(ui: &Ui, text: &str) -> f32 {
let job = egui::text::LayoutJob::single_section(
text.to_owned(),
egui::TextFormat {
font_id: egui::TextStyle::Body.resolve(ui.style()),
..Default::default()
},
);
// Measured in the frame that draws, never remembered between frames. The
// renderer's standing promise is that the same description at the same
// width is the same picture, and a measurement carried over from a
// narrower frame is exactly how that would stop being true.
ui.painter().layout_job(job).rect.width()
}
/// The width a flexible part may take, once the rest of the run is accounted.
///
/// Reserving only when the reservation leaves something behind, which is the
/// rule quasi-tui arrived at from the other end: a budget that cannot fit the
/// tail anyway would elide the primary to make room for parts that are still
/// past the edge, spending the one thing on screen for nothing.
fn budget(available: f32, after: Option) -> f32 {
match after {
Some(reserved) if reserved < available => available - reserved,
_ => available,
}
}
/// A text leaf drawn under a line budget.
///
/// `ui.label(RichText)` grows to as many rows as the words need, which is what
/// `leaf` does everywhere outside a run and is right there: a block may be as
/// tall as it is. Inside a run it is the unbounded behaviour `Flow` exists to
/// end, so this is the same text through a `LayoutJob`, whose wrapping carries
/// both the budget and the ellipsis.
///
/// `max_width` has to be set from the `Ui`: a job defaults to infinite width,
/// so a budget without it would never wrap and never elide, and the cap would
/// silently do nothing.
fn capped(immediate: &Immediate, ui: &mut Ui, text: &str, flow: layout::Flow, after: Option) {
let mut job = egui::text::LayoutJob::single_section(
text.to_owned(),
egui::TextFormat {
font_id: egui::TextStyle::Body.resolve(ui.style()),
color: immediate.palette.content,
..Default::default()
},
);
job.wrap = egui::text::TextWrapping {
max_width: budget(ui.available_width(), after),
max_rows: flow.lines() as usize,
// Words, unless one line is all there is: a single row elided at a word
// boundary can lose most of the row, which is the case egui's own docs
// name for breaking anywhere.
break_anywhere: flow.lines() == 1,
overflow_character: Some('\u{2026}'),
};
// Laid out here rather than handed to `ui.label`, and this is the whole
// difference between a budget that works and one that reads as though it
// does. A `Label` re-lays a job it is given, overwriting `max_width` with
// the `Ui`'s available width: the row's first part then elided at the full
// pane, took all of it, and the parts after it were left with nothing --
// which is the defect this budget was written to fix, surviving the fix.
// A galley is already laid out, so a `Label` built from one draws exactly
// what was measured.
let galley = ui.painter().layout_job(job);
ui.add(egui::Label::new(galley));
}
/// The cell just drawn, sensed for the two gestures a table row answers.
///
/// **`ui.response()` cannot answer either of them**, and was what stood here
/// The response a `Ui` gives for itself carries the sense it was built with,
/// and `UiBuilder`'s default is `Sense::hover`, so `clicked` and the secondary
/// click `Response::context_menu` reads are both permanently false. A table
/// row could not be opened and its menu could not be raised, in the one
/// consumer whose file list is a `Node::Table`.
///
/// Interacting for the sense we need is the move `list_row` already makes. The
/// id needs no disambiguator the way a list row's does: `makeover_immediate`
/// gives each cell its own `Ui`, so `ui.id()` differs per cell already.
fn cell_row_response(ui: &mut Ui) -> egui::Response {
ui.interact(
ui.min_rect(),
ui.id().with("cell-row"),
egui::Sense::click(),
)
}
/// What a press on a chosen-able row meant, as a payload.
///
/// The renderer's half of `Row::chosen`: the description says which rows are
/// chosen and the app owns the set, and this is the part only this side can
/// answer -- what the press *meant*, read off the modifiers this host has.
///
/// The mapping is the desktop's, unchanged since the Macintosh Finder: a plain
/// click chooses the row alone, ctrl (command on macOS) toggles it into the set,
/// and shift takes everything between the app's pointer and here. egui's
/// `Modifiers::command` is already the platform's own answer to which of ctrl
/// and command means "the modifier key", so nothing here is per-OS.
///
/// **Shift wins over command** when both are held, which `Choosing` rules on
/// rather than leaving to a renderer. audiofiles' own shipped list read them
/// the other way round (`ui/file_list.rs::handle_click`, before `49b7429`), so
/// this is a change to that app of the rarest press it has.
///
/// That is what makes this additive rather than a change to every row
/// activation in the tree.
fn choosing(ui: &Ui, chosen: Option) -> Params {
if chosen.is_none() {
return Params::new();
}
let modifiers = ui.ctx().input(|input| input.modifiers);
let meant = if modifiers.shift {
quasi_router::Choosing::Through
} else if modifiers.command {
quasi_router::Choosing::Also
} else {
quasi_router::Choosing::Only
};
Params::new().with(
quasi_router::Node::CHOOSING.to_owned(),
meant.as_str().to_owned(),
)
}
/// What a row offers, on the gesture this host means by asking.
///
/// A list row and a table row are one type, so a menu draws through one
/// function. The description's own instruction is that a menu
/// is "reached by right-click on a pointer host, long-press on a touch one, and
/// a key in a terminal"; egui is a pointer host, and `Response::context_menu` is
/// its right-click, so there is nothing for this renderer to invent.
///
/// A menu dropped silently, with no arm and no comment, is the failure mode the
/// wildcard arms in this file are written to avoid: the description says what
/// the row offers, and a renderer that quietly offers nothing is the one
/// outcome that must not happen.
///
/// Returns what was pressed rather than firing it. Every collecting site in this
/// file has the same reason -- the closure holds `&mut Ui` and firing wants the
/// pass mutably -- and a menu inside a table cell is the case where it is forced,
/// so both callers do it the one way.
///
/// A disabled act draws and does not answer, which is `widget::act`'s own
/// contract; a menu that omitted it would be a menu whose length changed with
/// state, and the reader loses the place they had learned.
fn row_menu(
immediate: &crate::Immediate,
response: &egui::Response,
menu: &[Act],
) -> Option<(Action, Option)> {
if menu.is_empty() {
return None;
}
let mut picked = None;
response.context_menu(|ui| {
for act in menu {
let pressed = widget::act(ui, &act.as_layout(), &immediate.palette, &immediate.widget);
if pressed.clicked() && act.state != Some(layout::State::Disabled) {
picked = Some((act.action.clone(), act.confirm.clone()));
// The menu closes on the press, not on the answer. A described
// act may raise a confirmation before anything happens, and a
// menu still standing behind that dialog is two surfaces asking
// at once.
ui.close();
}
}
});
picked
}
/// What the tick column calls itself.
///
/// `makeover_immediate::table` addresses a cell by its column's name, so the
/// one this renderer adds needs one. Never shown as a heading.
const TICK_COLUMN: &str = "select";
/// A described table.
///
/// The narrowing, the header carets and the tracks are all
/// `makeover_immediate::table`'s; what is here is the walk that turns a
/// described cell into the nodes inside it, and the two facts a `Screen` adds
/// over a `Column`: the address a row opens, and the address a heading reorders
/// by.
///
/// **A cell is a run of nodes, so drawing one is [`draw`] again.** That is the
/// property the containment migration bought every renderer, and it is why a
/// button in a cell needs no special case here: it is a `Node::Act` like any
/// other, and it fires through the same `Pass`.
/// The columns as `makeover-immediate` wants them, with the tick column in
/// front when the table has ticks.
///
/// Split out of [`table`] rather than inlined, which is where it was: a tick
/// takes no column in the description, so this renderer adds one, and that is a
/// self-contained fact about the two vocabularies meeting.
fn table_columns<'a>(columns: &'a [quasi_router::Column], ticks: bool) -> Vec> {
let mut borrowed: Vec> =
Vec::with_capacity(columns.len() + usize::from(ticks));
if ticks {
borrowed.push(layout::Column {
width: layout::Width::Fixed,
priority: layout::Priority::Essential,
..layout::Column::new(TICK_COLUMN)
});
}
borrowed.extend(columns.iter().map(|c| c.as_layout()));
borrowed
}
/// One table row's tick, and the value to toggle when it was pressed.
///
/// Drawn from the set the view holds rather than from the description, which is
/// the rule every renderer follows for a tick: the description says what
/// arrived and the view says what the user has done since. The view always,
/// because a table row's tick is a member of the screen's set by construction --
/// unlike a list row, there is no case where the description's flag is the
/// answer.
///
/// Split out of [`table`] to keep that function under the line cap, and it is
/// the right piece to split: a tick is the one cell in a table that is not a
/// value in the grid.
fn tick_cell(ui: &mut Ui, view: &crate::View, row: &Row) -> Option {
let (Some(_), Some(value)) = (row.selected, row.value.as_ref()) else {
return None;
};
let mut on = view.is_ticked(value);
ui.checkbox(&mut on, "").changed().then(|| value.clone())
}
fn table(pass: &mut Pass<'_>, ui: &mut Ui, columns: &[quasi_router::Column], rows: &[Row]) {
// A tick takes no column in the description, so this renderer adds one:
// `makeover_immediate::table` addresses cells by column, and a checkbox
// drawn outside that has no track to sit in. Essential, so narrowing never
// takes the affordance away, and it is named rather than blank because two
// unnamed columns would be one column twice.
// The rows a shut branch is not covering, which is what the table has this
// frame. Taken before anything is counted, so an index into the body is an
// index into what was drawn. `quasi_router::folded_by` is the reading, so a
// window and a terminal fold the same rows.
let branches = rows.iter().any(|row| row.open.is_some());
let shown = unfolded(rows, pass.view);
let rows = shown.as_slice();
let ticks = rows.iter().any(|row| row.selected.is_some());
let borrowed = table_columns(columns, ticks);
// What is drawn as a selected row: the app's pointer, and every row of a
// live selection. `Body::selected` takes a predicate rather than a set,
// which is what lets both facts answer through one call -- an app whose
// selection is a range does not have to build a collection to be asked.
//
// `1894e95d`. Before this, `Row::current` was the only thing that lit a
// row, so a five-hundred-row Cmd+A in audiofiles drew the same as a one-row
// click. A chosen row and the current row look alike here on purpose: the
// pointer sits inside the selection nearly always, and a second mark for
// "and this is the one the detail pane is showing" is a distinction the
// detail pane is already making.
let current = |at: usize| {
rows.get(at)
.is_some_and(|row| row.current || row.chosen == Some(true))
};
let body = makeover_immediate::table::Body {
rows: rows.len(),
selected: Some(¤t),
scroll_to: None,
};
let sizing = makeover_immediate::table::Sizing {
lengths: &[],
fallback: CELL_WIDTH,
};
// What the user pressed, collected rather than fired inside the closure:
// the closure holds `&mut Ui` and the pass at once, and firing needs the
// pass mutably.
let mut fired: Option<(Action, Params)> = None;
// Same reason as `fired`: ticking is a write to the view, and the view is
// behind the pass the closure cannot hold.
let mut toggled: Option = None;
// A menu press, kept apart from `fired` because it carries a confirmation
// and because it wins: a right-click lands on the same cell a left-click
// does, so collecting both in one slot would let opening the row beat the
// menu the user actually asked for.
let mut menued: Option<(Action, Option)> = None;
// A press on a branch's chevron, collected for `toggled`'s reason: folding
// writes to the view and the view is behind the pass.
let mut folded: Option<(String, bool)> = None;
let reordered = makeover_immediate::table::table(
ui,
&borrowed,
&body,
&sizing,
&pass.immediate.palette,
&pass.immediate.table,
|ui, column, at| {
let Some(row) = rows.get(at) else { return };
// Labelled, so the rect below is measured whichever way this
// cell left. `600c9e42`.
'cell: {
if ticks && column.name == TICK_COLUMN {
if let Some(value) = tick_cell(ui, pass.view, row) {
toggled = Some(value);
}
break 'cell;
}
let Some(index) = columns.iter().position(|c| c.name == column.name) else {
break 'cell;
};
let Some(cell) = row.cells.get(index) else {
break 'cell;
};
// The outline, in the first column and nowhere else. A table
// has no gutter to indent in and the indent is not a value in
// the grid, so it rides in front of the row's leading cell --
// which is the one the eye reads the hierarchy from anyway.
if index == 0 && branches {
let (pressed, _) = outline_lead(ui, pass.view, *row);
folded = folded.take().or(pressed);
}
// Which side of a change this line is on, when the table is a
// diff. `19d7602d`. A sign in front of the leading cell, on the
// terminal renderer's reasoning: this renderer has no per-row
// background to tint either, and the sign is what a reader of
// diffs already reads.
if index == 0
&& let Some(change) = row.change
{
let (sign, colour) = match change {
layout::Change::Added => ("+", pass.immediate.palette.success),
layout::Change::Removed => ("-", pass.immediate.palette.danger),
// Including a kind this renderer has not learned: an
// unchanged line, which draws and loses only the sign.
_ => (" ", pass.immediate.palette.content_muted),
};
ui.label(RichText::new(sign).monospace().color(colour));
}
for node in &cell.content {
// A cell's contents are ordinary nodes, but a press inside one
// cannot reach the pass from here. Only the two that carry an
// address are collected; everything else draws.
match node {
Node::Act(act) if act.state != Some(layout::State::Disabled) => {
if widget::act(
ui,
&act.as_layout(),
&pass.immediate.palette,
&pass.immediate.widget,
)
.clicked()
{
fired = Some((act.action.clone(), Params::new()));
}
}
Node::Link { text, action } => {
if ui
.link(RichText::new(text).color(pass.immediate.palette.action))
.clicked()
{
fired = Some((action.clone(), Params::new()));
}
}
other => leaf(pass.immediate, ui, other),
}
}
// Opening the row itself, from whichever cell was clicked. A table
// row has no single element to hang it on the way a list row hangs
// it on its primary text.
//
// The menu hangs off the same response, and therefore off every cell:
// `makeover_immediate::table` calls this per cell and answers no
// row-wide rect back, so "right-click the row" is "right-click any of
// its cells". That is the honest reading of what this renderer can
// see, and it is also what a user expects of a table row.
let response = cell_row_response(ui);
announce_row(ui, &response, index, row);
if let Some((action, confirm)) = row_menu(pass.immediate, &response, &row.menu) {
menued = Some((action, confirm));
} else if let Some(action) = &row.activate
&& response.clicked()
{
fired = Some((action.clone(), choosing(ui, row.chosen)));
}
}
// After it has drawn: a `Ui`'s `min_rect` is empty until
// something is in it, so measuring first recorded a sliver at the
// cell's left edge. Per cell, so the row answers under any of them.
crate::geometry::note_row(
ui,
row.value.as_deref(),
at,
ui.min_rect(),
row.chosen == Some(true),
);
},
);
if let Some(value) = toggled {
pass.view.tick(&value);
}
// A heading that was pressed reorders by that column, and the column says
// what that calls. `sortable` is `reorder.is_some()`, so a column with no
// address answers no press.
let reorder = reordered
.and_then(|pressed| columns.iter().find(|column| column.name == pressed.name))
.and_then(|column| column.reorder.clone());
table_pressed(pass, folded, menued, fired, reorder);
}
/// What a press on a table did, once the closure that saw it has let go of the
/// `Ui` and the pass is free again.
fn table_pressed(
pass: &mut Pass<'_>,
folded: Option<(String, bool)>,
menued: Option<(Action, Option)>,
fired: Option<(Action, Params)>,
reorder: Option,
) {
// The fold, instead of either press below rather than beside them: a
// chevron is a separate hit target from the row, so folding a branch never
// also opens it. `ccaa7e4b`. The reorder below still stands, because a
// heading is not a row.
if let Some((key, open)) = folded {
pass.view.fold(&key, open);
} else {
// The menu before the row, matching the order inside the closure:
// `fire` takes the first press of the frame, so whichever of the two is
// offered first wins, and the menu is the one the user asked for by
// name.
if let Some((action, confirm)) = menued {
pass.fire(&action, Params::new(), confirm.as_deref());
}
if let Some((action, payload)) = fired {
pass.fire(&action, payload, None);
}
}
if let Some(action) = reorder {
pass.fire(&action, Params::new(), None);
}
}
/// A region, drawn as the surface its kind names.
pub(crate) fn region(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot) {
// `ae8e8836`. Scoped so there is a rect to note, and noted whether or not
// anything is ever anchored here: nothing in a description says which
// regions an app anchors to. A scope adds no spacing and no frame -- it is
// a child `Ui` over the same available space -- so the drawing is what it
// was before this wrapper existed.
//
// Outside the early returns in `region_body`, deliberately. A region that is
// pending or failed still occupies space and can still be anchored to, and a
// menu that could not open over a region that had not loaded yet would be a
// rule nobody stated.
let drawn = ui.scope(|ui| region_body(pass, ui, slot));
crate::geometry::note_region(ui, &slot.id, drawn.response.rect);
}
/// What a region asks when the questions inside it move.
///
/// The browser puts one trigger on the region and lets the document gather
/// what it contains; here the containment is walked instead, through
/// [`Slot::questions`](quasi_router::Slot::questions), so the two hosts gather
/// the same set from the same walk rather than from two readings of the word
/// "inside".
///
/// Two halves, and both are needed every frame. A dial that moved pushes the
/// deadline out, which is what makes it a debounce; and a deadline that has
/// come due fires, which has to be checked whether or not anything moved this
/// frame -- the whole point is what happens once the moving stops.
fn region_consults(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot) {
if slot.consults.is_empty() {
return;
}
let inside = slot.questions();
let moved = inside
.iter()
.find(|field| pass.stirred.contains(&field.name))
.map(|field| {
pass.view
.buffer(&field.name, field.value.as_deref())
.clone()
});
for (at, consult) in slot.consults.iter().enumerate() {
// The floor is read against the value that moved, never against the
// gathered set: `Consult::asks_about` says so, and a set has no length
// a reader could predict.
if let Some(moved) = &moved {
if consult.asks_about(moved) {
pass.view.wait_to_consult(
Asking::Region(slot.id.clone()),
at,
std::time::Instant::now() + consult.after,
);
} else {
// Deleting back under the floor cancels a question already
// waiting, exactly as it does for a box's own.
pass.view.consulted(Asking::Region(slot.id.clone()), at);
}
}
let Some(due) = pass.view.consult_due(Asking::Region(slot.id.clone()), at) else {
continue;
};
let now = std::time::Instant::now();
if now < due {
// An idle app stops repainting, and a deadline nobody wakes up for
// is a question never asked.
ui.ctx().request_repaint_after(due - now);
continue;
}
pass.view.consulted(Asking::Region(slot.id.clone()), at);
// What rides along from outside the region goes in first, so a dial
// inside it wins where a name sits on both sides.
let mut payload = pass.view.contributed(&consult.sends);
for field in &inside {
// What the reader has typed, falling back to what the description
// offered, which is the order a submit reads a form in: an
// untouched dial still sends what it is showing.
payload.insert(
field.name.clone(),
pass.view
.buffer(&field.name, field.value.as_deref())
.clone(),
);
}
pass.fire(&consult.action, payload, None);
}
}
/// The region itself, inside the scope that measures it.
fn region_body(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot) {
// A region that does not apply right now is not drawn at all. `079a011e`:
// the region names the control and the value that bring it out, and this
// renderer answers it from the view it is already holding -- no request,
// and nothing re-rendered on a form the reader is midway through.
if pass.hidden.out(&slot.id) {
return;
}
// Readiness first: a region that is not ready has nothing to draw and says
// so, which is the whole of what the axis is for.
match slot.readiness {
layout::Readiness::Pending => {
// Wiki `loading-and-progress-standard`, rule 2. This was
// `ui.spinner()` until `5eccb6aa`, drawn identically whether the
// region was waiting on a measured payload or on nothing anyone
// could count -- and a turning arc reads as progress, which is the
// claim an unmeasured wait has not earned.
//
// The amount, when there is one, is what the region's own feeding
// action described. The numerator is the host's and is usually
// absent, which is what keeps a bar from being drawn out of a total
// alone.
widget::awaiting(
ui,
slot.awaiting().unwrap_or_default(),
pass.view.progress_at(std::time::Instant::now()),
pass.immediate.reduced_motion(),
pass.immediate.palette(),
&pass.immediate.widget,
);
return;
}
layout::Readiness::Failed => {
ui.label(
RichText::new("This did not load.")
.color(pass.immediate.palette.tone(layout::Tone::Danger)),
);
return;
}
// `Empty` is drawn: what says a region is empty is a `Node::StandIn`
// inside it, per `703f4cd2`, because a column with a heading and no
// rows still has content.
_ => {}
}
let cutoff = cutoff(ui.available_width());
run(pass, ui, slot, cutoff);
if slot.showing().selective() {
showing_body(pass, ui, slot, cutoff);
} else if let Some(repeating) = slot.repeating.as_deref() {
repeating_body(pass, ui, slot, repeating);
} else {
for placed in slot.body.iter().filter(|placed| placed.kept_at(cutoff)) {
draw(pass, ui, &placed.node);
}
}
// After the body, because what the region asks about is what the body is
// holding and half of it would not have been drawn yet from anywhere else.
region_consults(pass, ui, slot);
// The host's drawing under the described blocks, and only for a bespoke
// region: a fill named against a pane is a host reaching into a region the
// description already owns. The ordering is the arrangement
// `Containment::Opaque` describes -- a heading the description owns above a
// canvas it does not.
if let RegionKind::Handover { .. } | RegionKind::Ceded { .. } = slot.kind {
if let Some(fill) = pass.immediate.fill(&slot.id) {
fill(pass.immediate, ui);
} else if slot.kind.as_layout().owed() {
// The half the split exists for. Before it, a region the app had
// ruled undescribable and one nobody had filled yet were the same
// value here, and both drew as nothing at all.
unfilled(pass.immediate, ui);
}
}
}
/// A region whose children are answers to one question.
///
/// Each slot under its number, with the control that takes it away, and the
/// control that adds one under the lot. audiofiles' rule editor is the
/// consumer and this is its host, so this is the arm the ruling was measured
/// against: a condition is three questions that only mean anything together,
/// which is the shape `Repeat` could not say.
///
/// Everything is derived as nodes and drawn through [`draw`], so nothing here
/// invents styling: a slot's number is the same heading a described one gets,
/// and both controls take everything [`act_node`] knows about tone, waiting and
/// confirmation.
///
/// Nothing is narrowed by [`cutoff`]. A slot of a repeating question is not an
/// optional member of a run -- dropping the third condition at a narrow width
/// would hide an answer the reader gave, which is a different thing from
/// dropping a toolbar button they can still reach from a menu.
fn repeating_body(
pass: &mut Pass<'_>,
ui: &mut Ui,
slot: &Slot,
repeating: &quasi_router::Repeating,
) {
let standing = slot.body.len();
for (at, placed) in slot.body.iter().enumerate() {
// One-based, because it is read by a person.
draw(
pass,
ui,
&Node::section(format!("{} {}", repeating.one, at + 1)),
);
draw(pass, ui, &placed.node);
// The child's own, because only the child knows which slot it is. The
// boundary is drawn rather than hidden, which is the call audiofiles'
// editor already made for its last condition -- what changed is that
// `Repeating::least` says it once instead of the app disabling its own
// button.
if let Node::Region(child) = &placed.node
&& let Some(removes) = &child.removes
{
draw(
pass,
ui,
&Node::Act(bounded(removes, repeating.may_remove(standing))),
);
}
}
draw(
pass,
ui,
&Node::Act(bounded(&repeating.add, repeating.may_add(standing))),
);
}
/// One control, at whatever the floor or the ceiling says.
fn bounded(act: &quasi_router::Act, allowed: bool) -> quasi_router::Act {
if allowed {
act.clone()
} else {
act.clone().disabled()
}
}
/// A region showing one child at a time, and the chrome that moves between them.
///
/// The derivation quasi-webview and quasi-tui both make, arriving here third.
/// Nothing reads [`RegionKind::Widget`]'s name: a carousel, a tab group and a
/// disclosure are one region that shows some of its children, and which idiom
/// comes out falls out of what the children carry.
///
/// **This renderer drew none of it until now.** `region_body` walked the whole
/// body whatever [`layout::Showing`] said, so a described tab group came out as
/// every panel stacked with no strip -- the same shape of defect as `run`'s,
/// where a description said something and this renderer silently drew something
/// else. The two findings this module's header recorded as gaps in the
/// *description* ("a tabbed arrangement does not say which tab is showing", "a
/// tab has no label") were both answered by `Showing` and [`Slot::label`]
/// before this; what was left was nobody here reading them.
///
/// # The three shapes, and what picks between them
///
/// quasi-webview's, unchanged, because the picking is the vocabulary's and not
/// the host's:
///
/// - One dismissible child with a label is a summary line that opens.
/// - Children carrying labels get a strip of them.
/// - Anything else gets previous, position, next.
///
/// # Where the bytes come from
///
/// A child carrying [`Slot::fed_by`] is a panel behind a route, and pressing its
/// tab calls it; the answer lands as a fragment naming that child. A child
/// holding its content already is moved to locally with no request, which is
/// what a carousel is. Presence is the only thing that picks between the two,
/// exactly as in the browser.
fn showing_body(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot, cutoff: layout::Priority) {
let at = pass.view.shown(slot);
let labels = slot.labels();
let total = slot.body.len();
// A named single child that can close is a disclosure, and the check comes
// first because such a child is also a labelled one: a strip of one tab is
// not what a summary line is.
let disclosure = slot.showing().dismissible() && total == 1 && labels.len() == 1;
if disclosure {
if ui.selectable_label(at.is_some(), labels[0]).clicked() {
pass.view.disclose(slot);
}
} else if !labels.is_empty() {
// A strip sits above the panes it opens. The folder semantic, and the
// same placement the other two renderers derive.
ui.horizontal(|ui| {
for (index, label) in labels.iter().enumerate() {
if ui.selectable_label(at == Some(index), *label).clicked() {
pass.view.show(&slot.id, index);
// The panel's address, when the panel is a route rather
// than content already here. No target rides with it: the
// router answers with a fragment naming the slot it
// changed, so the party that knows stays the party that
// says.
if let Some(action) = fed_child(slot, index) {
pass.fire(action, Params::new(), None);
}
}
}
});
}
// One child, or none at all: `Showing::AtMostOne` closed is the only way to
// reach `None` here, and drawing nothing is what closed means.
if let Some(index) = at
&& let Some(placed) = slot.body.get(index)
&& placed.kept_at(cutoff)
{
draw(pass, ui, &placed.node);
}
// A counter row sits under the content it counts, and only where there was
// no strip to put above it. The position reads back one step: it says where
// you are among the children and it is not one of them.
if labels.is_empty() && !disclosure {
ui.horizontal(|ui| {
if ui.button("Prev").clicked() {
pass.view.show_by(slot, -1);
}
ui.label(format!("{} / {total}", at.map_or(0, |index| index + 1)));
if ui.button("Next").clicked() {
pass.view.show_by(slot, 1);
}
});
}
}
/// The route a child of a showing region is fetched from, if it is fetched.
///
/// quasi-webview's function of the same name, verbatim. Only a region can carry
/// [`Slot::fed_by`], so a child that is a bare node is content already here by
/// construction.
fn fed_child(slot: &Slot, at: usize) -> Option<&Action> {
match &slot.body.get(at)?.node {
Node::Region(child) => child.fed_by.as_deref(),
_ => None,
}
}
/// The region's leading row: the members the description said share it.
///
/// Ruling: wiki `layout-room-and-fallback`. A member nobody can see is worse
/// than one drawn in the wrong direction, so the drop is what made this a
/// defect rather than a shortfall.
///
/// # What each fallback gets, and what it costs
///
/// The room is measured here, per frame, from the width egui is offering, the
/// same reading `cutoff` already takes for a region's body. Nothing is
/// authored and nothing is remembered between frames.
///
/// [`Fallback::Wrap`](layout::Fallback::Wrap) and
/// [`Fallback::Stack`](layout::Fallback::Stack) are both `horizontal_wrapped`.
/// egui breaks the row when the next member does not fit and measures each
/// member from its own galley, which is the derived minimum the ruling asks
/// for. The two differ in the webview by whether a wrapped member fills its
/// line; egui has no equivalent knob on a wrapped layout, so this renderer
/// answers both the same way and says so rather than authoring a width.
///
/// # A member that asks to fill
///
/// [`layout::Width::Fill`] members share what the content-sized ones did not
/// take, equally, which is that type's own rule and the same answer the
/// webview's `flex: 1 1 0` gives. Each is allocated an equal share of what is
/// left where it stands, so the division is exact when the fills follow the
/// members that take what they need. Both shapes the description reaches for
/// are that: a pane beside a pane, and a row of peers.
///
/// [`layout::Width::Fixed`] takes no allocation. A run carries no size, so
/// there is nothing to fix a member at.
///
/// [`Shed`](layout::Fallback::Shed) and [`Menu`](layout::Fallback::Menu) drop
/// by [`layout::Priority`] through [`Run::kept_at`], which the webview cannot
/// do at all -- there is no `@container (inline-size < min-content)` -- and
/// this renderer can, because it is holding the width. `Menu` then puts what
/// it shed behind one control, so every member stays reachable; `Shed` does
/// not, which is what the description asked for when it chose the word.
fn run(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot, cutoff: layout::Priority) {
let Some(run) = slot.run.as_ref() else {
return;
};
let kept = run.kept_at(cutoff);
ui.horizontal_wrapped(|ui| {
let mut fills = kept
.iter()
.filter(|placed| matches!(placed.width, layout::Width::Fill))
.count();
for placed in &kept {
if !matches!(placed.width, layout::Width::Fill) {
draw(pass, ui, &placed.node);
continue;
}
// An equal share of what is left where it stands. Exact when the
// fills come after the members that take what they need, which is
// both shapes the description reaches for -- a pane beside a pane,
// and a row of peers -- and an approximation when a fill is written
// before a content member, because an immediate-mode library has
// not measured that member yet and a sizing pass to find out would
// be the remembered measurement the ruling forbids.
let share = ui.available_width() / fills as f32;
fills = fills.saturating_sub(1);
let height = ui.available_height();
ui.allocate_ui(egui::vec2(share, height), |ui| {
draw(pass, ui, &placed.node);
});
}
if matches!(run.fallback, layout::Fallback::Menu) {
let shed: Vec<&quasi_router::Ranked> = run
.members
.iter()
.filter(|member| !member.kept_at(cutoff))
.collect();
if !shed.is_empty() {
// The label is the count rather than a name, because the
// description did not give the row one and inventing "More
// actions" here would be this renderer writing copy.
ui.menu_button(format!("{} more", shed.len()), |ui| {
for placed in shed {
draw(pass, ui, &placed.node);
}
});
}
}
});
}
/// The cutoff a region narrows to at this width, in points.
///
/// egui measures in the same unit a browser's media query does, so the
/// boundaries here are `makeover-geometry`'s size classes verbatim rather than
/// a second set of numbers: a described screen narrows at the same width in a
/// window and in a webview, which is the point of the classes being quoted in
/// one place. The terminal renderer has to convert, because a cell is not a
/// point, and says so where it does.
///
/// Read off the width egui is offering right now and nothing else. That is
/// what "Any width, one answer" costs in an immediate-mode library, and it is
/// almost nothing -- the temptation the rule guards against is the memory
/// store beside it, where a cutoff worked out once would be cheap to keep and
/// would make the layout a function of the frame that put it there.
pub(crate) fn cutoff(width: f32) -> layout::Priority {
if width < 600.0 {
layout::Priority::Essential
} else if width < 840.0 {
layout::Priority::Secondary
} else {
layout::Priority::Optional
}
}