Skip to main content

max / quasi

116.4 KB · 2714 lines History Blame Raw
1 //! Nodes to egui.
2 //!
3 //! Every function here takes a piece of [`quasi_router`]'s screen tree and draws
4 //! it into a `Ui`. Nothing returns a `Result`: a description that exists is
5 //! renderable by construction, which is the property the owned mirror in
6 //! `quasi-router` was built to have.
7 //!
8 //! # Where the drawing goes
9 //!
10 //! Down, into `makeover-immediate`. A meter, a token, a control, a figure, a
11 //! field and a table are all its, and this module is the walk that decides which
12 //! one a node is and where it sits. The split is the same one `quasi-tui` keeps
13 //! against `makeover-tui`, and it is what stops a second copy of the vocabulary's
14 //! drawing existing per host.
15 //!
16 //! What is left here is what a `Screen` adds over a node: the address a control
17 //! carries, the values a form gathers, and the selection a row's tick joins.
18 //! None of that is `makeover-layout`'s, so none of it can be down there.
19 //!
20 //! # The walk is split in two, and the table is why
21 //!
22 //! [`draw`] is exhaustive over `Node` and dispatches to [`leaf`] for the nodes
23 //! that carry no address, and to [`container`] for the ones that hold others or
24 //! reach a screen's own facts.
25 //!
26 //! The split is forced rather than tidy: `makeover_immediate::table` draws a cell
27 //! through a closure that already holds the `Ui` and the renderer, and a closure
28 //! cannot also hold the pass mutably. So a cell draws its ordinary nodes through
29 //! `leaf`, and collects the two that carry an address to fire after the table
30 //! has finished with the borrow.
31
32 use std::collections::BTreeMap;
33
34 use egui::{RichText, Ui};
35 use makeover_immediate::widget;
36 use makeover_immediate::{Filling, field, frame};
37 use quasi_router::layout;
38 use quasi_router::{
39 Act, Action, Bar, Clock, Image, Node, Params, Placed, RegionKind, Rest, Row, Slot,
40 };
41
42 use crate::view::Asking;
43 use crate::{Immediate, Pass};
44
45 /// What a column is assumed to need when nothing measured it.
46 ///
47 /// `Sizing::lengths` is how an app says a column's longest value is wider than
48 /// its name, and a described table carries no such measurement: the description
49 /// says what a column *is*, not how long its contents turned out. So every
50 /// column falls back to this, and `egui_extras` sizes the remainder.
51 const CELL_WIDTH: f32 = 120.0;
52
53 /// Draw one node.
54 ///
55 /// Every member is named, and the containers are listed one by one rather than
56 /// swept up.
57 ///
58 /// # Why there is a catch-all anyway
59 ///
60 /// [`Node`] is `#[non_exhaustive]`, so one is compulsory: a node added upstream
61 /// does not stop this crate compiling, and the member after this one is not a
62 /// lockstep release across three renderers.
63 ///
64 /// The catch-all goes to [`undrawn`], which draws a line saying the renderer
65 /// does not know this node yet, so a screen quietly drawing less than it
66 /// describes is the one outcome that cannot happen.
67 ///
68 /// The discipline the paragraph above asks for still applies, it is not the
69 /// compiler's job: name every member, and treat a member that reaches the
70 /// catch-all as one still owed an arm. Neither half of the split may end in a
71 /// wildcard that *does* something, routing to `container` or to `leaf`, since
72 /// that is what turns an unknown node into a panic.
73 pub(crate) fn draw(pass: &mut Pass<'_>, ui: &mut Ui, node: &Node) {
74 match node {
75 // The nodes that carry no address. Split out so a table cell can
76 // draw them: the cell closure holds the `Ui` and cannot also hold
77 // the pass mutably, and these need nothing but the palette.
78 Node::Heading { .. }
79 | Node::Text { .. }
80 | Node::Rich { .. }
81 | Node::Code { .. }
82 | Node::Figure(_)
83 | Node::Image(_)
84 | Node::Meter(_)
85 | Node::Since { .. }
86 | Node::Until { .. }
87 | Node::Age { .. } => leaf(pass.immediate, ui, node),
88
89 // A notice with something to do about it is addressed, so it cannot go
90 // to the leaf walk, which has no `Pass` to fire through. The text half
91 // is still `leaf`'s, so there is one notice drawing and not two.
92 Node::Notice { act, .. } => {
93 leaf(pass.immediate, ui, node);
94 if let Some(act) = act {
95 act_node(pass, ui, act);
96 }
97 }
98
99 Node::Act(act) => {
100 act_node(pass, ui, act);
101 }
102
103 Node::Link { text, action, .. } => {
104 // A link is a link and not a button: egui has `Link`, and a control
105 // that navigates should not look like one that writes.
106 if ui
107 .link(RichText::new(text).color(pass.immediate.palette.action))
108 .clicked()
109 {
110 pass.fire(action, Params::new(), None);
111 }
112 }
113
114 Node::Token(tag) => {
115 let mut pressed = widget::token(
116 ui,
117 &tag.label,
118 tag.kind,
119 tag.tone,
120 tag.latched,
121 &pass.immediate.palette,
122 &pass.immediate.widget,
123 );
124 // `436bc223`: the detail behind the label. egui has hover, so this
125 // renderer draws it -- `on_hover_text` is the same affordance the
126 // webview spends `title` on, and a token is already a `Response`,
127 // so nothing in makeover-immediate had to grow a parameter for it.
128 if let Some(hint) = &tag.hint {
129 pressed = pressed.on_hover_text(hint);
130 }
131 if let Some(action) = &tag.action
132 && pressed.clicked()
133 {
134 pass.fire(action, Params::new(), None);
135 }
136 }
137
138 Node::StandIn { message, act, .. } => {
139 ui.label(RichText::new(message).color(pass.immediate.palette.content_muted));
140 if let Some(act) = act {
141 act_node(pass, ui, act);
142 }
143 }
144
145 // The containers, named rather than swept up, so that a member added
146 // upstream stops this match compiling instead of reaching `container`
147 // and panicking there.
148 Node::Field(_)
149 | Node::Region(_)
150 | Node::Form { .. }
151 | Node::Table { .. }
152 | Node::Timeline { .. }
153 | Node::Stats { .. } => container(pass, ui, node),
154
155 // A member added since this renderer last learned the vocabulary. It
156 // says so and draws nothing else; see this function's header for what
157 // `#[non_exhaustive]` cost here and what it bought.
158 _ => undrawn(pass.immediate, ui),
159 }
160 }
161
162 /// The nodes that carry no address.
163 ///
164 /// Everything here needs the palette and nothing else, which is what makes a
165 /// table cell able to draw one: the cell closure already holds the `Ui` and the
166 /// renderer, and cannot also hold the pass mutably.
167 ///
168 /// The catch-all is unreachable through [`draw`], which is exhaustive and sends
169 /// only these here. It is a private split of one walk rather than a second walk,
170 /// so the guarantee that a new `Node` member stops the build lives up there.
171 fn leaf(immediate: &Immediate, ui: &mut Ui, node: &Node) {
172 match node {
173 Node::Heading { level, text } => {
174 let size = ui.text_style_height(&egui::TextStyle::Body)
175 * match level {
176 layout::Heading::Page => 1.6,
177 layout::Heading::Section => 1.3,
178 layout::Heading::Subsection => 1.1,
179 };
180 ui.label(
181 RichText::new(text)
182 .size(size)
183 .strong()
184 .color(immediate.palette.content),
185 );
186 }
187
188 Node::Text { text, .. } => {
189 ui.label(RichText::new(text).color(immediate.palette.content));
190 }
191
192 // Markdown arrives as source, so that every renderer answers it its own
193 // way. This one has no rich text of its own worth the name, so it takes
194 // the plain rendering: `**bold**` reads as `bold` rather than as four
195 // characters of syntax, which is the outcome `Node::Text` would have
196 // given anyway and is the honest floor until egui grows a markdown
197 // widget worth adopting.
198 Node::Rich { source, .. } => {
199 ui.label(
200 RichText::new(docengine::render_plain(source)).color(immediate.palette.content),
201 );
202 }
203
204 // `19d7602d`. The runs arrived classified, so all this owes is a
205 // colour each and a monospace face. egui lays out a horizontal run of
206 // coloured labels, which is what a code line is.
207 //
208 // The palette has no syntax colours and will not grow any: a
209 // highlighting palette is held fixed while a theme changes, which is
210 // the opposite of what a palette is for. So this spends the status
211 // colours, on the terminal renderer's reasoning and with the same
212 // base16 Tomorrow pairing.
213 Node::Code { runs, inline, .. } => {
214 let colour = |syntax: layout::Syntax| match syntax {
215 layout::Syntax::Comment => immediate.palette.content_muted,
216 layout::Syntax::String => immediate.palette.success,
217 layout::Syntax::Keyword => immediate.palette.action,
218 layout::Syntax::Constant => immediate.palette.warning,
219 layout::Syntax::Entity => immediate.palette.info,
220 layout::Syntax::Variable => immediate.palette.danger,
221 layout::Syntax::Support => immediate.palette.content_secondary,
222 // Plain, and any class added since this renderer last learned
223 // the vocabulary: ordinary code, drawn and uncoloured.
224 _ => immediate.palette.content,
225 };
226 let draw_runs = |ui: &mut Ui| {
227 ui.spacing_mut().item_spacing.x = 0.0;
228 for run in runs {
229 ui.label(
230 RichText::new(run.text.clone())
231 .monospace()
232 .color(colour(run.syntax)),
233 );
234 }
235 };
236 // A block owns its lines and an inline literal sits in one, which
237 // is the same distinction the node's `inline` flag makes for
238 // containment. egui has no wrapping run of styled text, so a block
239 // is a vertical of horizontals per source line and an inline is one
240 // horizontal.
241 if *inline {
242 ui.horizontal(draw_runs);
243 } else {
244 ui.vertical(draw_runs);
245 }
246 }
247
248 Node::Figure(figure) => {
249 widget::figure(
250 ui,
251 &figure.as_layout(),
252 &immediate.palette,
253 &immediate.widget,
254 );
255 }
256
257 Node::Image(picture) => {
258 image(immediate, ui, picture);
259 }
260
261 Node::Notice { tone, text, .. } => {
262 // The tone carries it, and the surface says it is a thing set on
263 // the page rather than part of the flow. Where a toast lands
264 // against a banner is renderer policy and this renderer has one
265 // place to put either, which is where the caller drew it.
266 frame(
267 ui,
268 layout::Depth::Raised,
269 &immediate.palette,
270 immediate.frame,
271 |ui| {
272 ui.label(RichText::new(text).color(immediate.palette.tone(*tone)));
273 },
274 );
275 }
276
277 Node::Meter(meter) => {
278 widget::meter(
279 ui,
280 &meter.as_layout(),
281 &immediate.palette,
282 &immediate.widget,
283 );
284 }
285
286 // The bars are borrowed here for the reason every compound member is:
287 // the description owns them and `makeover-immediate` draws the borrowed
288 // ones.
289 Node::Chart { axis, bars, .. } => {
290 let borrowed: Vec<_> = bars.iter().map(Bar::as_layout).collect();
291 widget::chart(
292 ui,
293 &axis.as_layout(),
294 &borrowed,
295 &immediate.palette,
296 &immediate.widget,
297 );
298 }
299
300 // The readouts derived from the current time. The instant is the
301 // description's and the words are this crate's, made fresh here because
302 // an immediate host redraws from the description every frame -- which
303 // is the half of this ruling egui gets for free. The half it does not
304 // is asking for the next frame, and that is `Runtime::show`.
305 Node::Since { at } => clock_label(immediate, ui, Clock::Since, *at),
306 Node::Until { at } => clock_label(immediate, ui, Clock::Until, *at),
307 Node::Age { at } => clock_label(immediate, ui, Clock::Age, *at),
308
309 // An addressed node reaching here is this crate's own routing bug, so
310 // it stays a panic and the members are named to keep it one. Sweeping
311 // them into the arm below would turn a wrong branch in `draw` into a
312 // screen that quietly drew a placeholder where a button was.
313 Node::Act(_)
314 | Node::Link { .. }
315 | Node::Token(_)
316 | Node::StandIn { .. }
317 | Node::Field(_)
318 | Node::Form { .. }
319 | Node::Table { .. }
320 | Node::Timeline { .. }
321 | Node::Stats { .. }
322 | Node::Region(_) => unreachable!("an addressed node reached the leaf walk"),
323
324 // A member added since this renderer last learned the vocabulary.
325 //
326 // This arm is why the one above lists its members. A table cell walks
327 // its own node match and ends `other => leaf(..)`, so an unknown member
328 // inside a cell arrives here rather than at [`draw`] -- and until this
329 // existed it arrived at the `unreachable!` and panicked the frame,
330 // which is the same failure the note on [`draw`] records against
331 // `Node::Image` and `Node::Timeline`.
332 _ => undrawn(immediate, ui),
333 }
334 }
335
336 /// A time-derived readout, as one label.
337 ///
338 /// The clock is read here rather than passed in, which is the shape the ruling
339 /// asks for: the renderer owns it. A test that needs a fixed answer calls
340 /// [`crate::clock::text`] with a `now` of its own.
341 fn clock_label(immediate: &Immediate, ui: &mut Ui, clock: Clock, at: std::time::SystemTime) {
342 let words = crate::clock::text(clock, at, std::time::SystemTime::now());
343 ui.label(RichText::new(words).color(immediate.palette.content));
344 }
345
346 /// What a node this renderer has not learned yet draws instead of itself.
347 ///
348 /// One muted line, the same the description's own [`Node::StandIn`] gets,
349 /// because it is the same situation said by the renderer instead of by the
350 /// handler: something is here and you are not seeing it.
351 ///
352 /// Saying so rather than drawing nothing is the whole point. The failure this
353 /// crate has already had twice is a screen that silently drew less than it
354 /// described, and a blank where a node was is indistinguishable from a screen
355 /// that never carried one.
356 fn undrawn(immediate: &Immediate, ui: &mut Ui) {
357 ui.label(
358 RichText::new("(not drawn: this renderer does not know this yet)")
359 .italics()
360 .color(immediate.palette.content_muted),
361 );
362 }
363
364 /// What a [`RegionKind::Handover`] with no fill says.
365 ///
366 /// [`undrawn`]'s sibling, for the other way a region ends up empty. A handover
367 /// is a fill the app owes every host, so this renderer having none is a hole
368 /// rather than a finished region.
369 ///
370 /// [`RegionKind::Ceded`] gets no equivalent on purpose: nothing is owed there,
371 /// so drawing nothing is correct and a notice would invent a gap the app has
372 /// already ruled on.
373 fn unfilled(immediate: &Immediate, ui: &mut Ui) {
374 ui.label(
375 RichText::new("(not drawn: this host has no fill for this)")
376 .italics()
377 .color(immediate.palette.content_muted),
378 );
379 }
380
381 /// The controls for a set that arrived in parts.
382 ///
383 /// Back, position, forward, in a row, which is the order the other two renderers
384 /// put them in. A direction the description gave no address for is drawn
385 /// disabled rather than left out: an immediate-mode pass rebuilds this row every
386 /// frame, so a button that appeared on page two would move the position label
387 /// under the pointer mid-session.
388 ///
389 /// Where the description offered jumps the numbered pages take the position's
390 /// place, as the row of controls this host draws everything else as.
391 fn rest_controls(pass: &mut Pass<'_>, ui: &mut Ui, rest: &Rest) {
392 let paging = rest.as_layout();
393 ui.horizontal(|ui| {
394 if ui
395 .add_enabled(rest.back.is_some(), egui::Button::new("Prev"))
396 .clicked()
397 && let Some(action) = &rest.back
398 {
399 pass.fire(action, Params::new(), None);
400 }
401
402 if rest.jumps.is_empty() {
403 ui.label(match (paging.page(), paging.pages_total()) {
404 (Some(page), Some(total)) => format!("{page} / {total}"),
405 _ => match paging.total() {
406 Some(total) => format!("{} of {total}", paging.shown()),
407 None => "Showing what arrived".to_owned(),
408 },
409 });
410 } else {
411 // The offered pages in place of the readout, for the other two
412 // renderers' reason: the strip says which page and how many
413 // already, and printing the position beside it is a control
414 // arguing with itself.
415 for jump in &rest.jumps {
416 if jump.here {
417 // A label and not a disabled button. A control that
418 // reloads the page it is on is an affordance that does
419 // nothing, and this is the one page in the strip that is a
420 // readout rather than somewhere to go.
421 ui.label(jump.page.to_string());
422 continue;
423 }
424 if ui.button(jump.page.to_string()).clicked() {
425 pass.fire(&jump.action, Params::new(), None);
426 }
427 }
428 }
429
430 if ui
431 .add_enabled(rest.forward.is_some(), egui::Button::new("Next"))
432 .clicked()
433 && let Some(action) = &rest.forward
434 {
435 pass.fire(action, Params::new(), None);
436 }
437 });
438 }
439
440 /// A picture, at a source the description does not carry.
441 ///
442 /// Drawn here rather than handed down to `makeover-immediate`, and that is the
443 /// same call `quasi-webview` makes for the same reason: the source is the
444 /// member the drawing turns on, `makeover-layout` deliberately has no notion of
445 /// an address, and what is left below the source is a box and a line of text.
446 /// `makeover-webview` contributes `picture_rules` and writes no `<img>` either.
447 fn image(immediate: &Immediate, ui: &mut Ui, picture: &Image) {
448 let available = ui.available_width();
449
450 let mut shown = egui::Image::new(&picture.src)
451 // A picture the screen needs now advertises that it is coming; one the
452 // description says can wait does not put a spinner in the flow for it.
453 .show_loading_spinner(matches!(picture.loading, layout::Loading::Eager));
454
455 // What egui paints where the bytes do not arrive. An empty `alt` is a claim
456 // that the picture adds nothing to the text beside it, so standing in for it
457 // with anything at all would be worse than the gap -- the argument
458 // `layout::Image::speaks` carries, and the call `quasi-tui` makes too.
459 if picture.speaks() {
460 shown = shown.alt_text(picture.alt.clone());
461 }
462
463 // `Fit::Natural` and `Fit::Contain` are both what happens below: the whole
464 // picture, at its own proportions, inside the width on offer.
465 //
466 // `Fit::Cover` is not, and cannot be here. Cropping needs a box to crop to,
467 // and a node in an egui vertical flow is given a width and an unbounded
468 // height -- there is no shape to fill. The webview gets that shape from the
469 // stylesheet and this host has no stylesheet. Drawing it whole is wrong by
470 // one member and drawing it cropped to a rectangle nobody described is
471 // wrong by more, so it draws whole. Filed.
472 shown = match picture.intrinsic {
473 // The dimensions doing the one job they exist for: the box is the right
474 // shape before a byte arrives, so nothing below it moves when the
475 // texture lands. Never scaled up -- a 5120-wide screenshot is not
476 // asking for a 5120-wide window.
477 Some(extent) if extent.width > 0 && extent.height > 0 => {
478 let width = available.min(f64_ish(extent.width));
479 let height = width * f64_ish(extent.height) / f64_ish(extent.width);
480 shown.fit_to_exact_size(egui::vec2(width, height))
481 }
482 // Not known, so nothing can be held. egui sizes the texture once it has
483 // it and whatever is below moves once, which is the honest outcome and
484 // the one `layout::Image::intrinsic` exists to let an app avoid.
485 _ => shown.max_width(available).maintain_aspect_ratio(true),
486 };
487
488 ui.add(shown);
489
490 // Content that happens to sit under a picture, so it reads the same whether
491 // or not the picture arrived. Not muted, unlike the alt text, which is
492 // standing in for something rather than being it.
493 if let Some(caption) = &picture.caption {
494 ui.label(RichText::new(caption).color(immediate.palette.content_muted));
495 }
496 }
497
498 /// A picture's pixel count as a drawing measure.
499 ///
500 /// `as` on a `u32` is a lossy cast a pedantic lint is right to want justified,
501 /// and the justification is that it is a texture dimension: `f32` is exact to
502 /// 16.7 million and no picture is that wide.
503 #[expect(
504 clippy::cast_precision_loss,
505 reason = "a texture dimension is exact in f32 far past any real picture"
506 )]
507 fn f64_ish(pixels: u32) -> f32 {
508 pixels as f32
509 }
510
511 /// How tall one tick of a track is, in lines of body text.
512 ///
513 /// The least that lets the ruler label itself without the labels touching, and
514 /// every other measure on the axis falls out of it. A pixel height is
515 /// presentation and `layout::Track` deliberately carries none, so this is the
516 /// renderer choosing, the way the webview's stylesheet chooses.
517 const TICK_LINES: f32 = 1.5;
518
519 /// The gutter a ruler writes its labels in, in labels' widths.
520 const RULER_PADDING: f32 = 8.0;
521
522 /// Rows placed by when they happen.
523 ///
524 /// The lane packing, the gridlines and how tall a slot is are all this
525 /// renderer's, and `layout::Track` says so: it carries the window, the
526 /// granularity and the unit, and nothing measured in pixels. What travels is
527 /// the two integers per entry that order alone cannot say.
528 fn timeline(
529 pass: &mut Pass<'_>,
530 ui: &mut Ui,
531 track: layout::Track,
532 entries: &[Placed],
533 focus: Option<u16>,
534 ) {
535 let text_height = ui.text_style_height(&egui::TextStyle::Body);
536 let slots = track.slots();
537 let per_tick = if track.slot == 0 || track.tick == 0 {
538 0
539 } else {
540 track.tick / track.slot
541 };
542 let slot_height = if per_tick == 0 {
543 text_height
544 } else {
545 text_height * TICK_LINES / f32::from(per_tick)
546 };
547 let height = f32::from(slots) * slot_height;
548
549 // The gutter is as wide as the labels going in it, measured rather than
550 // guessed: a day strip writes `31` and a clock writes `00:00`, and a
551 // constant wide enough for one wastes half of itself on the other.
552 let sample = tick_label(track.unit, track.span.from());
553 let ruler = ui
554 .painter()
555 .layout_no_wrap(
556 sample,
557 egui::TextStyle::Body.resolve(ui.style()),
558 pass.immediate.palette.content_muted,
559 )
560 .rect
561 .width()
562 + RULER_PADDING;
563
564 let (rect, _) = ui.allocate_exact_size(
565 egui::vec2(ui.available_width(), height),
566 egui::Sense::hover(),
567 );
568 let painter = ui.painter().clone();
569 let palette = pass.immediate.palette;
570
571 // The ruler. A rule per slot and a label per tick, both the axis describing
572 // itself, so neither is an entry and neither is reachable.
573 for slot in 0..slots {
574 let y = rect.top() + f32::from(slot) * slot_height;
575 let ticked = per_tick > 0 && slot % per_tick == 0;
576 painter.hline(
577 rect.x_range(),
578 y,
579 egui::Stroke::new(
580 1.0,
581 if ticked {
582 palette.bevel_dark
583 } else {
584 palette.sunken
585 },
586 ),
587 );
588 if ticked {
589 painter.text(
590 egui::pos2(rect.left(), y),
591 egui::Align2::LEFT_TOP,
592 tick_label(track.unit, track.span.from() + slot * track.slot),
593 egui::TextStyle::Body.resolve(ui.style()),
594 palette.content_muted,
595 );
596 }
597 }
598
599 // Lanes. Which lane an entry takes is a fact about how wide the box is and
600 // not about the day, so it is worked out here rather than described: the
601 // description said when things happen and `Placed::overlaps` turns that into
602 // who collides. Greedy first-fit, the standard day-view packing -- an entry
603 // takes the lowest lane no occupant of which it overlaps. O(n^2) worst case
604 // over a day's worth of appointments, so the interval graph is not worth its
605 // own bugs.
606 let mut lanes: Vec<usize> = Vec::with_capacity(entries.len());
607 for (i, entry) in entries.iter().enumerate() {
608 let mut lane = 0;
609 while entries[..i]
610 .iter()
611 .zip(&lanes)
612 .any(|(other, &taken)| taken == lane && entry.overlaps(other))
613 {
614 lane += 1;
615 }
616 lanes.push(lane);
617 }
618 // One width for the whole track rather than per collision cluster, which is
619 // the same call the webview makes and for the same reason: per-cluster is
620 // denser and is a layout decision either renderer can revisit without the
621 // description changing.
622 let across = lanes.iter().copied().max().map_or(1, |most| most + 1);
623 let body = rect.with_min_x(rect.left() + ruler);
624 let lane_width = body.width() / f32::from(u16::try_from(across).unwrap_or(u16::MAX).max(1));
625
626 for (entry, &lane) in entries.iter().zip(&lanes) {
627 let top = rect.top() + track.fraction(entry.placement.at()) * height;
628 let bottom = rect.top() + track.fraction(entry.placement.end()) * height;
629 let at = egui::Rect::from_min_size(
630 egui::pos2(
631 body.left() + f32::from(u16::try_from(lane).unwrap_or(u16::MAX)) * lane_width,
632 top,
633 ),
634 // A placement is never zero-length, but a short one against a long
635 // span still rounds to nothing, and a thing that happened is worth
636 // a line whatever its duration.
637 egui::vec2(lane_width, (bottom - top).max(text_height)),
638 );
639 painter.rect_filled(at, 2.0, palette.well);
640 ui.scope_builder(egui::UiBuilder::new().max_rect(at.shrink(2.0)), |ui| {
641 // Each entry gets its own scoped `Ui`, so the position is only ever
642 // 0 here and the id is distinct regardless.
643 list_row(pass, ui, &entry.row, 0, false);
644 });
645 }
646
647 // "Show me 09:00" rather than a scroll offset, which is the whole of what
648 // `focus` is: the app knows the interesting moment and the renderer knows
649 // how to get there. Once per moment and not once per frame -- egui redraws
650 // continuously, and a scroll request every frame is a track the user cannot
651 // scroll away from.
652 if let Some(minute) = focus {
653 let id = ui.id().with("track-focus");
654 if ui.data(|data| data.get_temp::<u16>(id)) != Some(minute) {
655 ui.data_mut(|data| data.insert_temp(id, minute));
656 let y = rect.top() + track.fraction(minute) * height;
657 ui.scroll_to_rect(
658 egui::Rect::from_min_size(
659 egui::pos2(rect.left(), y),
660 egui::vec2(rect.width(), text_height),
661 ),
662 Some(egui::Align::Center),
663 );
664 }
665 }
666 }
667
668 /// What a tick says about where it sits.
669 ///
670 /// The one thing on an axis that cannot be derived from the numbers, which is
671 /// why `layout::Unit` exists: the geometry above is unit-agnostic and was
672 /// correct while the ruler printed `00:00` over a month strip.
673 fn tick_label(unit: layout::Unit, offset: u16) -> String {
674 match unit {
675 // Wall clock, wrapped, so a span running past midnight labels 02:00
676 // rather than 26:00. `Span` counts past 1440 deliberately so that it
677 // needs no date, and how that reads to a person is the renderer's.
678 layout::Unit::Minutes => format!("{:02}:{:02}", (offset / 60) % 24, offset % 60),
679 // Day one, not day zero. A strip's offsets are zero-based like every
680 // other axis here and nobody calls the first of the month the zeroth.
681 layout::Unit::Days => format!("{}", offset + 1),
682 // A unit added later lands here rather than silently taking the clock,
683 // which is exactly how the clock-over-a-month defect shipped.
684 _ => String::new(),
685 }
686 }
687
688 /// The nodes that hold other nodes, or that a screen's own facts reach into.
689 ///
690 /// Split from [`draw`] where the line fell naturally rather than to satisfy a
691 /// lint: everything above is a leaf that needs the palette and nothing else,
692 /// and everything here needs the view, the selection or a nested walk.
693 fn container(pass: &mut Pass<'_>, ui: &mut Ui, node: &Node) {
694 match node {
695 Node::Field(described) => {
696 field_node(pass, ui, described);
697 }
698
699 Node::Region(slot) => {
700 region(pass, ui, slot);
701 }
702
703 Node::Form {
704 fields,
705 submit,
706 action,
707 ..
708 } => {
709 form(pass, ui, fields, submit, action);
710 }
711
712 // **A table that declared no columns is a list, and draws as one.** One
713 // node since the 2026-09-06 collapse; the guard is where the two
714 // arrangements part company in this renderer, and the arm below is the
715 // grid. Both hold the same `Row`.
716 Node::Table {
717 columns,
718 rows,
719 more,
720 ..
721 } if columns.is_empty() => {
722 // A row a shut branch covers is not drawn at all, which is the
723 // whole of what folding is. `quasi_router::folded` reads it, so a
724 // window and a terminal fold the same rows.
725 //
726 // The chevron column is spent on every row of a list that holds a
727 // branch, leaf or not, so the labels line up under each other.
728 let branches = rows.iter().any(|row| row.open.is_some());
729 let shown = unfolded(rows, pass.view);
730 for (within, row) in rows.iter().enumerate() {
731 // The row's own place among its siblings, kept even when a
732 // branch above it is shut: it names this row's hit target, and
733 // renumbering on a fold would hand one row the target of
734 // another.
735 if !shown.iter().any(|kept| std::ptr::eq(*kept, row)) {
736 continue;
737 }
738 list_row(pass, ui, row, within, branches);
739 }
740 if let Some(rest) = more {
741 rest_controls(pass, ui, rest);
742 }
743 }
744
745 Node::Table {
746 columns,
747 rows,
748 more,
749 ..
750 } => {
751 table(pass, ui, columns, rows);
752 // Under the table and belonging to it, the same row of controls a
753 // list gets.
754 if let Some(rest) = more {
755 rest_controls(pass, ui, rest);
756 }
757 }
758
759 Node::Timeline {
760 track,
761 entries,
762 focus,
763 ..
764 } => {
765 timeline(pass, ui, *track, entries, *focus);
766 }
767
768 Node::Stats { figures, .. } => {
769 // Across rather than down, which is the one thing a strip says: a
770 // terminal stacks them because it has no width to spare, and a
771 // window does.
772 ui.horizontal(|ui| {
773 for (figure, address) in figures {
774 let shown = widget::figure(
775 ui,
776 &figure.as_layout(),
777 &pass.immediate.palette,
778 &pass.immediate.widget,
779 );
780 // The one of goingson's five figure sites that renders its
781 // value as a button: the description's half is the
782 // vocabulary's and the optional address is quasi's.
783 if let Some(action) = address
784 && shown.interact(egui::Sense::click()).clicked()
785 {
786 pass.fire(action, Params::new(), None);
787 }
788 }
789 });
790 }
791
792 // Every leaf is answered by `draw`, which is exhaustive, so this
793 // reaches nothing. It is here because the split is this crate's and
794 // not the vocabulary's: `Node` still has no wildcard anywhere, and a
795 // member added upstream still stops `draw` compiling.
796 _ => unreachable!("a leaf reached the container walk"),
797 }
798 }
799
800 /// A control, with whatever the screen wants gathered behind it.
801 fn act_node(pass: &mut Pass<'_>, ui: &mut Ui, act: &Act) {
802 // The set it acts on, read off the control. This was a parameter until
803 // 2026-08-20 and both callers passed `None`, so `Act::over` reached this
804 // renderer and did nothing: the count was never drawn, the control over an
805 // empty set stayed live, and the ticks never left with the call. The two
806 // other renderers read the member directly, which is why neither drifted.
807 let over = act.over.as_deref();
808 // What the press asks for before it fires, drawn above the control. A
809 // webview hides these behind the verb in a `details`; there is no such
810 // affordance here that would not be a popup this renderer opened and closed
811 // on its own, and an immediate-mode screen redraws every frame anyway, so
812 // the boxes stand in the open. `as_asked` drops the write members: the value
813 // is answered by the control below, not by a route of the box's own.
814 for field in &act.asks {
815 field_node(pass, ui, &field.as_asked());
816 }
817 // A control over the screen's selection says how many it would act on, and
818 // is disabled while that is none. Neither is sayable in the description: the
819 // ticks are the host's until something submits them, so a screen built from
820 // the store cannot know the number, and a commit control over an empty set
821 // is otherwise offered, pressed, and answers "0 tasks completed" -- a screen
822 // letting the user find out by trying.
823 //
824 // Disabled rather than hidden. `bulk-actions.js` hides its bar and can
825 // afford to, because its rows keep their checkboxes; a bar that vanishes
826 // takes with it the only evidence that bulk actions exist.
827 let chosen = over.map(|_| pass.view.ticks().count());
828 let label = match chosen {
829 Some(0) | None => act.label.clone(),
830 Some(chosen) => format!("{} ({chosen})", act.label),
831 };
832 // A control that has been pressed and not answered yet is drawn as what it
833 // is: working, and not taking another press. The label is untouched, so the
834 // control does not change width the moment it is pressed, and disabled is
835 // the guard as well as the saying here -- a disabled widget reports no
836 // click, so the second press does not exist rather than being discarded.
837 let busy = pass.view.busy(&act.action);
838 let described = layout::Act {
839 label: &label,
840 key: act.key.as_deref(),
841 tone: act.tone,
842 state: if chosen == Some(0) || busy {
843 Some(layout::State::Disabled)
844 } else {
845 act.state
846 },
847 // Carried across since makeover-layout 0.40.0, so `widget::act` draws
848 // the hover this function used to apply to its answer.
849 hint: act.hint.as_deref(),
850 };
851
852 // `db998898`. What the control shows, above what it says, which is the tile
853 // shape both measured sites have. Drawn here rather than handed down for
854 // `Act::hint`'s reason: `makeover_layout::Act` carries no picture, that
855 // crate declares `links`, and a member there is one version bump across 25
856 // manifests in 12 repos.
857 //
858 // Grouped so the picture and the button are one item in the flow and the
859 // press stays the button's: a picture that answered a click would be this
860 // renderer inventing an affordance the description did not ask for, since
861 // `shows` says what a control looks like and never that the picture is a
862 // second control.
863 let pressed = match &act.shows {
864 Some(picture) => {
865 ui.vertical(|ui| {
866 image(pass.immediate, ui, picture);
867 widget::act(
868 ui,
869 &described,
870 &pass.immediate.palette,
871 &pass.immediate.widget,
872 )
873 })
874 .inner
875 }
876 None => widget::act(
877 ui,
878 &described,
879 &pass.immediate.palette,
880 &pass.immediate.widget,
881 ),
882 };
883 // `ca7b5200`. Standing help, as a hover -- honest on this host in a way it
884 // is not on a terminal, because egui has a pointer.
885 //
886 // `widget::act` does it since makeover-layout 0.40.0 moved `hint` down, so
887 // nothing is applied here any more. It was applied outside the widget for
888 // three releases because `makeover_layout::Act` carried no hint, and the
889 // consequence was that a makeover host that was not quasi could not say it.
890 //
891 // `ae8e8836`. Where this control was drawn, for an `Outcome::Anchored` that
892 // names it. Only a named control is noted -- `Act::id` is `None` on nearly
893 // all of them -- so a screen anchoring nothing pays one `Option` check per
894 // control and no allocation.
895 if let Some(id) = &act.id {
896 crate::geometry::note_act(ui, id, pressed.rect);
897 }
898 if pressed.clicked() && chosen != Some(0) && !busy {
899 let mut payload = gathered(pass, over);
900 payload.absorb(asked(pass, &act.asks));
901 deposit(pass, act);
902 // `c3e145e0`. This host is the one of the three that needs no help: egui
903 // owns the clipboard already, so the copy happens here rather than
904 // reaching the host as a webview script or a `quasi_tui::Step`.
905 //
906 // Beside `deposit` and before the call, matching the other two: the
907 // local half of a press happens whether or not anything is asked, and a
908 // copying act asks nothing -- its action is local, so `fire` below has
909 // no route to call.
910 if let Some(value) = &act.copies {
911 ui.ctx().copy_text(value.clone());
912 }
913 pass.fire(&act.action, payload, act.confirm.as_deref());
914 }
915 }
916
917 /// Put the act's value into the box it named.
918 ///
919 /// [`Act::fills`](quasi_router::Act::fills) names a field on the same screen
920 /// and the renderer decides where in it the value lands; here that is the end
921 /// of what is already there. egui holds a text cursor per widget and this
922 /// deliberately does not reach for it: the description names a destination and
923 /// never a position, and a value that arrives at the end is what the
924 /// vocabulary calls correct rather than a fallback.
925 ///
926 /// After the payload is gathered, matching the other two renderers: a webview's
927 /// htmx listener sits on the control and its fill script on the document, so
928 /// there the press sends what the boxes held before it. One description sending
929 /// two different things on two hosts is the drift this stack exists to end.
930 ///
931 /// From the box's own buffer, which is what it is showing: [`View::buffer`]
932 /// seeds one from the description the first time the field is drawn, and a
933 /// control cannot be pressed on a frame before the screen holding it was drawn.
934 /// So a deposit into a box somebody has typed in goes after their typing, and
935 /// one into an untouched box goes after whatever the description offered rather
936 /// than over it.
937 ///
938 /// [`View::buffer`]: crate::View::buffer
939 fn deposit(pass: &mut Pass<'_>, act: &Act) {
940 let Some(fill) = &act.fills else {
941 return;
942 };
943 let mut value = pass.view.edit(&fill.field).unwrap_or_default().to_owned();
944 value.push_str(&fill.value);
945 pass.view.set(&fill.field, value);
946 }
947
948 /// A checkbox, which is the one field held as a bool rather than as text.
949 ///
950 /// Split out of [`field_node`] because it shares none of the rest: no buffer to
951 /// outlive the frame, no consults, no suggestion list. `Node::SELECTED` is
952 /// quasi's submission convention, so a tick travels as a string on the way out
953 /// and is a bool only while it is on screen.
954 fn checkbox_node(
955 pass: &mut Pass<'_>,
956 ui: &mut Ui,
957 described: &quasi_router::Field,
958 name: &str,
959 offered: bool,
960 ) {
961 let mut on = pass
962 .view
963 .edit(name)
964 .map_or(offered, |value| !value.is_empty());
965 let before = on;
966 described.with_layout(|borrowed| {
967 field(
968 ui,
969 &borrowed,
970 Filling::On(&mut on),
971 None,
972 &pass.immediate.palette,
973 &pass.immediate.field,
974 );
975 });
976 if on == before {
977 return;
978 }
979 pass.view.set(name, if on { "on" } else { "" });
980 // A tick is a value that moved, so a region recomputing from a set of dials
981 // hears it the same way it hears a typed one.
982 pass.stirred.insert(name.to_owned());
983 if let Some(action) = &described.writes {
984 let mut payload = Params::new();
985 payload.insert(name.to_owned(), if on { "on" } else { "" }.to_owned());
986 pass.fire(action, payload, None);
987 }
988 }
989
990 /// The upper end of an interval moved.
991 ///
992 /// Its own function rather than a branch inside [`field_node`], which is long
993 /// enough already, and the split falls where the two ends genuinely differ:
994 /// only this one has to be remembered under a second name.
995 ///
996 /// It fires the same [`quasi_router::Field::writes`] the lower end does. A
997 /// filter that moved is a filter that moved, whichever box the user dragged,
998 /// and both ends travel in the payload because an interval is one answer -- a
999 /// handler reading only the end that moved would narrow the filter to a single
1000 /// bound every time either box was touched.
1001 fn upper_moved(
1002 pass: &mut Pass<'_>,
1003 described: &quasi_router::Field,
1004 lower: (&str, &str),
1005 upper: (&str, &str),
1006 ) {
1007 let (lower_name, lower_value) = lower;
1008 let (upper_name, upper_value) = upper;
1009 pass.view.set(upper_name, upper_value.to_owned());
1010 let Some(action) = &described.writes else {
1011 return;
1012 };
1013 let mut payload = Params::new();
1014 payload.insert(lower_name.to_owned(), lower_value.to_owned());
1015 payload.insert(upper_name.to_owned(), upper_value.to_owned());
1016 pass.fire(action, payload, None);
1017 }
1018
1019 /// A question answered zero or more times: the slots, and the controls the
1020 /// reader adds and removes them with.
1021 ///
1022 /// Each slot is [`quasi_router::Field::instance`] put back through
1023 /// [`field_node`], so a slot is drawn by everything this renderer already does
1024 /// to a field and there is no second field emitter.
1025 ///
1026 /// How many slots stand is the view's, for the reason the buffers are: the
1027 /// description says how many answers it was given, and how many boxes there are
1028 /// now is a fact about what the reader has done since. Adding one asks nothing
1029 /// and neither does taking one away.
1030 fn repeat_node(pass: &mut Pass<'_>, ui: &mut Ui, described: &quasi_router::Field) {
1031 let Some(repeat) = described.repeats.clone() else {
1032 return;
1033 };
1034 ui.label(RichText::new(described.label.as_str()).color(pass.immediate.palette.content));
1035 // What is wrong with the *set*, which no slot's own message can carry. A
1036 // slot's error rides on the slot, through the field it is drawn as.
1037 if let Some(error) = &described.error {
1038 ui.label(
1039 RichText::new(error.as_str()).color(pass.immediate.palette.tone(layout::Tone::Danger)),
1040 );
1041 }
1042 let standing = pass.view.standing(described);
1043 for at in 0..standing {
1044 // One box for an ordinary slot, one per part for a grouped one, and
1045 // the branch lives in the vocabulary rather than here so the three
1046 // renderers cannot disagree about what a slot is.
1047 let slots = described.instance_fields(at);
1048 let held = repeat.instances.get(at);
1049 let busy = held.is_some_and(|slot| slot.progress.busy());
1050 ui.horizontal(|ui| {
1051 for slot in &slots {
1052 field_node(pass, ui, slot);
1053 }
1054 // A slot the work has not finished with is one the reader may not
1055 // pull out from under it.
1056 if repeat.fewer(standing)
1057 && !busy
1058 && ui.button(RichText::new(repeat.remove.as_str())).clicked()
1059 {
1060 pass.view.remove_slot(described, at);
1061 }
1062 });
1063 // What is wrong with the slot as a whole, and how far the work on it
1064 // has got. A part's own message rides on the part, as a field's does.
1065 if let Some(held) = held {
1066 if let Some(error) = &held.error {
1067 ui.label(
1068 RichText::new(error.as_str())
1069 .color(pass.immediate.palette.tone(layout::Tone::Danger)),
1070 );
1071 }
1072 if let quasi_router::Progress::Working(Some(meter)) = &held.progress {
1073 widget::meter(
1074 ui,
1075 &meter.as_layout(),
1076 &pass.immediate.palette,
1077 &pass.immediate.widget,
1078 );
1079 }
1080 }
1081 }
1082 // Nothing for a question whose slots come from another control, for the
1083 // reason the other two renderers draw nothing: there is no blank a reader
1084 // could fill.
1085 if let Some(label) = repeat.add.label().filter(|_| repeat.more(standing))
1086 && ui.button(RichText::new(label)).clicked()
1087 {
1088 pass.view.add_slot(described);
1089 }
1090 }
1091
1092 /// One field, filled from the view rather than from the description.
1093 /// Move each of a field's consult deadlines on, or cancel it.
1094 ///
1095 /// Split out of [`field_node`] while that was over clippy's line bound, and it
1096 /// is a whole idea on its own: a keystroke pushes every question this box raises
1097 /// further out, which is what makes it a debounce.
1098 fn push_deadlines(pass: &mut Pass<'_>, described: &quasi_router::Field, name: &str, buffer: &str) {
1099 for (at, consult) in questions(described) {
1100 if consult.asks_about(buffer) {
1101 pass.view.wait_to_consult(
1102 Asking::Field(name.to_owned()),
1103 at,
1104 std::time::Instant::now() + consult.after,
1105 );
1106 } else {
1107 // Deleting back under the floor cancels a question that was already
1108 // waiting, rather than letting it fire against a value the
1109 // description says is too short to ask about.
1110 pass.view.consulted(Asking::Field(name.to_owned()), at);
1111 }
1112 }
1113 }
1114
1115 fn field_node(pass: &mut Pass<'_>, ui: &mut Ui, described: &quasi_router::Field) {
1116 // A question that does not apply is left out, which is what this renderer
1117 // already does with a region that does not. What was typed into it is kept
1118 // in the view and still submitted, exactly as a browser sends a hidden
1119 // input. `8fdb814c`.
1120 if pass.hidden.field_out(&described.name) {
1121 return;
1122 }
1123 let name = described.name.clone();
1124 let kind = described.kind;
1125 let offered = described.value.clone();
1126
1127 // A question answered N times is N slots, each an ordinary field of this
1128 // same function. `60d1753c`.
1129 if described.repeats.is_some() {
1130 repeat_node(pass, ui, described);
1131 return;
1132 }
1133
1134 if kind == layout::FieldKind::Checkbox {
1135 checkbox_node(pass, ui, described, &name, offered.is_some());
1136 return;
1137 }
1138
1139 // The buffer has to outlive the frame, so it is the view's. Taken out and
1140 // put back rather than borrowed across the closure, because the closure
1141 // also needs the palette off `pass`.
1142 let mut buffer = pass.view.buffer(&name, offered.as_deref()).clone();
1143 let before = buffer.clone();
1144 // An interval edits two buffers under the two names it submits under. Its
1145 // upper end is a second buffer of the view's for the same reason the first
1146 // one is: what is being typed outlives the frame.
1147 let upper_name = described
1148 .upper_name
1149 .clone()
1150 .filter(|_| kind == layout::FieldKind::Interval);
1151 let mut upper = upper_name.as_ref().map(|upper_name| {
1152 pass.view
1153 .buffer(upper_name, described.upper_value.as_deref())
1154 .clone()
1155 });
1156 let upper_before = upper.clone();
1157 let width = described.width;
1158 let response = sized(ui, width, |ui| {
1159 described.with_layout(|borrowed| {
1160 let filling = match upper.as_mut() {
1161 Some(upper) => Filling::Between {
1162 lower: &mut buffer,
1163 upper,
1164 },
1165 None => Filling::Text(&mut buffer),
1166 };
1167 field(
1168 ui,
1169 &borrowed,
1170 filling,
1171 None,
1172 &pass.immediate.palette,
1173 &pass.immediate.field,
1174 )
1175 })
1176 });
1177 // Where the caret starts, on the arrival frame and no other. Focus is
1178 // egui's here, so this is a request rather than a placement, and the claim
1179 // is taken rather than read: a flag left standing would ask again every
1180 // frame and the reader could never move the caret off the box. See
1181 // `View::claims_caret`.
1182 if let Some(response) = response.as_ref()
1183 && pass.view.claims_caret(&name)
1184 {
1185 response.request_focus();
1186 }
1187
1188 if upper_before != upper
1189 && let Some((upper_name, upper)) = upper_name.as_ref().zip(upper.as_ref())
1190 {
1191 // An interval's upper end is a value of its own, under its own name,
1192 // and moving it moves the interval.
1193 pass.stirred.insert(upper_name.clone());
1194 upper_moved(pass, described, (&name, &buffer), (upper_name, upper));
1195 }
1196 if buffer != before {
1197 pass.view.set(&name, buffer.clone());
1198 // Said once here, read by every region that contains this box. A field
1199 // has no way to know which regions those are and a region has no way to
1200 // know its body moved, so the two meet on the frame.
1201 pass.stirred.insert(name.clone());
1202 // Every keystroke pushes the deadline out, which is what makes this a
1203 // debounce: a slug typed in one go is asked about once, when it stops
1204 // moving. Per keystroke and NOT on settle, unlike `changes` below: a
1205 // question asked while typing is the whole point of a consult, and
1206 // `Consult::after` is the description's own wait.
1207 //
1208 // Each question keeps its own deadline. A box asking two routes at two
1209 // rates is MNW's discover search, and one deadline per field would have
1210 // the faster of the two cancel the slower.
1211 push_deadlines(pass, described, &name, &buffer);
1212 }
1213 // `8032fe61`, `de2376bd`. **When the value is complete, not on the way to
1214 // it.** This fired on `buffer != before`, so typing 30 into audiofiles'
1215 // bounded `row_height` posted a row height of 3 on the way -- outside the
1216 // 20-32 the field's own hint states -- and dragging a classifier threshold
1217 // wrote once per frame instead of once per drag.
1218 //
1219 // `quasi-webview` has always meant this: `Field::writes` is emitted as
1220 // `hx-trigger="change"`, and a browser raises `change` on blur or Enter for
1221 // a text control and on release for a range. Two shipped renderers
1222 // disagreeing about one member is the drift this stack exists to end, and
1223 // the host that was already right says what right is. Ruling on
1224 // quasicoherent `8032fe61`.
1225 //
1226 // What is NOT settled here is whether these controls should write with no
1227 // submit at all, which is Max's rule in wiki `explicit-commit-affordance`
1228 // and is that task's remaining half.
1229 let settled = response
1230 .as_ref()
1231 .is_some_and(|response| response.lost_focus() || response.drag_stopped());
1232 // A control whose value is chosen in one go was already complete when it
1233 // changed, and a browser fires `change` for it immediately. Only what the
1234 // reader builds up -- typed into or dragged across -- has an "on the way".
1235 // A theme is picked in one gesture, the way an option is.
1236 let built_up = !(kind.offers_options() || kind.takes_files() || kind.offers_themes());
1237 let complete = if built_up { settled } else { buffer != before };
1238 if complete
1239 && described.writes.is_some()
1240 && pass.view.unwritten(&name, &buffer, offered.as_deref())
1241 {
1242 pass.view.wrote(&name, &buffer);
1243 if let Some(action) = &described.writes {
1244 let mut payload = Params::new();
1245 payload.insert(name.clone(), buffer.clone());
1246 // Both ends, because an interval is one answer. A handler reading
1247 // only the end that moved would narrow the filter to a single
1248 // bound every time either box was touched.
1249 if let Some((upper_name, upper)) = upper_name.as_ref().zip(upper.as_ref()) {
1250 payload.insert(upper_name.clone(), upper.clone());
1251 }
1252 pass.fire(action, payload, None);
1253 }
1254 }
1255
1256 // Candidates for a value that no longer clears the floor are an answer to a
1257 // question the field would not ask now, so a delete takes them with it.
1258 if let Some(owned) = described.suggests.as_ref()
1259 && !owned.asks_about(&buffer)
1260 {
1261 pass.view.unsuggest();
1262 }
1263
1264 // Checked every frame rather than only on a keystroke, because the whole
1265 // point is what happens when the keystrokes stop.
1266 for (at, consult) in questions(described) {
1267 let Some(due) = pass.view.consult_due(Asking::Field(name.clone()), at) else {
1268 continue;
1269 };
1270 let now = std::time::Instant::now();
1271 if now >= due {
1272 pass.view.consulted(Asking::Field(name.clone()), at);
1273 // This box's value, plus whatever else the question said it
1274 // carries. No ticks: a consult asks about this box, never about a
1275 // set of rows.
1276 let mut payload = pass.view.contributed(&consult.sends);
1277 payload.insert(name.clone(), buffer.clone());
1278 pass.fire(&consult.action, payload, None);
1279 } else {
1280 // An idle app stops repainting, and a deadline nobody wakes up for
1281 // is a question never asked. This is the one place the renderer
1282 // needs a clock, and egui already owns one.
1283 ui.ctx().request_repaint_after(due - now);
1284 }
1285 }
1286
1287 suggestions(pass, &name, &mut buffer, ui);
1288 }
1289
1290 /// Every question this field asks, the one it owns first.
1291 ///
1292 /// The owned question is keyed just past the end of
1293 /// [`Field::consults`](quasi_router::Field::consults), so it gets a deadline of
1294 /// its own for the reason each consult does — a box asking two routes at two
1295 /// rates would otherwise have the faster question cancel the slower — and no
1296 /// index can collide with a consult's.
1297 fn questions(field: &quasi_router::Field) -> impl Iterator<Item = (usize, &quasi_router::Consult)> {
1298 field.consults.iter().enumerate().chain(
1299 field
1300 .suggests
1301 .iter()
1302 .map(|owned| (field.consults.len(), owned)),
1303 )
1304 }
1305
1306 /// The candidates under the box, and what picking one does.
1307 ///
1308 /// Drawn where the list is described to be — under the field that owns it, in
1309 /// flow — because an immediate-mode frame has no z-order to float in and a
1310 /// popup here would be a window this renderer opened on its own. The webview
1311 /// floats and the terminal paints over; each host's answer is its own, and
1312 /// what they share is the description.
1313 ///
1314 /// Picking writes [`Candidate::value`](quasi_router::Candidate::value) into the
1315 /// box and closes the list. It costs exactly what a settled value costs, which
1316 /// is why a [`Field::writes`](quasi_router::Field::writes) route fires: the
1317 /// buffer this writes into is compared against its previous contents on the
1318 /// next frame, the same way a keystroke is, so no second write path exists here
1319 /// to keep in step.
1320 ///
1321 /// That is the default and not the definition. A candidate carrying
1322 /// [`picks`](quasi_router::Candidate::picks) has that action fired instead,
1323 /// and nothing is written -- which is the whole of MNW's search box, where a
1324 /// pick navigates and the typed value is discarded.
1325 ///
1326 /// [`detail`](quasi_router::Candidate::detail) is drawn after the label and
1327 /// dimmed, which is this host's answer to the same question the terminal
1328 /// answers with the rest of the row and a webview with a second line.
1329 fn suggestions(pass: &mut Pass<'_>, name: &str, buffer: &mut String, ui: &mut egui::Ui) {
1330 let Some(options) = pass.view.suggesting(name) else {
1331 return;
1332 };
1333 // Cloned rather than borrowed across the loop: the list is read off the
1334 // view and picking writes back to it, and a candidate is two short strings.
1335 let options = options.to_vec();
1336 let mut picked = None;
1337 for candidate in options {
1338 let mut text = egui::text::LayoutJob::default();
1339 text.append(
1340 &candidate.label,
1341 0.0,
1342 egui::TextFormat::simple(egui::FontId::default(), ui.visuals().text_color()),
1343 );
1344 if let Some(detail) = &candidate.detail {
1345 text.append(
1346 detail,
1347 8.0,
1348 egui::TextFormat::simple(egui::FontId::default(), ui.visuals().weak_text_color()),
1349 );
1350 }
1351 if ui.add(egui::Button::new(text).frame(false)).clicked() {
1352 picked = Some(candidate.clone());
1353 }
1354 }
1355 let Some(candidate) = picked else {
1356 return;
1357 };
1358 pass.view.unsuggest();
1359 // Performed as written, with no payload rule invented here: the action
1360 // carries its own params and the view it was offered under, exactly as a
1361 // control's does.
1362 if let Some(action) = candidate.picks {
1363 pass.fire(&action, Params::new(), None);
1364 return;
1365 }
1366 buffer.clone_from(&candidate.value);
1367 pass.view.set(name, candidate.value);
1368 }
1369
1370 /// How wide a control asks to be, in the one unit egui takes.
1371 ///
1372 /// `Fill` is what an egui text field does already, and is the default, so a
1373 /// described field that says nothing draws exactly as it did before the member
1374 /// existed: `TextEdit`'s desired width is infinite, so in a row it takes what is
1375 /// left.
1376 ///
1377 /// The other two need a number the description does not carry, which is the same
1378 /// place `Column` leaves this renderer and the terminal one -- see `quasi-tui`'s
1379 /// table sizing, whose `fallback` is a guess "until the vocabulary carries a
1380 /// measure". This is that guess for a field: a guess rather than nothing,
1381 /// because the alternative is `Content` and `Fill` drawing identically, which
1382 /// would make the member unfalsifiable here.
1383 const ASKED: f32 = 220.0;
1384
1385 /// Draw within whatever the control asked for.
1386 fn sized<R>(ui: &mut Ui, width: layout::Width, draw: impl FnOnce(&mut Ui) -> R) -> R {
1387 match width {
1388 layout::Width::Fill => draw(ui),
1389 _ => {
1390 ui.scope(|ui| {
1391 ui.set_max_width(ASKED.min(ui.available_width()));
1392 draw(ui)
1393 })
1394 .inner
1395 }
1396 }
1397 }
1398
1399 /// The values a control asked for before it fired, read as a submit reads them.
1400 ///
1401 /// `Act::asks`. Empty for a control that asked for nothing, which is nearly all
1402 /// of them.
1403 fn asked(pass: &Pass<'_>, asks: &[quasi_router::Field]) -> Params {
1404 if asks.is_empty() {
1405 return Params::new();
1406 }
1407 let (names, described) = submitted(pass, asks);
1408 pass.view.submission(&names, &described)
1409 }
1410
1411 /// The names a set of fields submits under, and what the description offered
1412 /// for each.
1413 ///
1414 /// One name per field, except for a question answered N times, which is N
1415 /// names: `60d1753c`. How many those are is the view's, because the slots the
1416 /// reader added are not in the description, which is why this is not a
1417 /// function of the fields alone.
1418 fn submitted(
1419 pass: &Pass<'_>,
1420 fields: &[quasi_router::Field],
1421 ) -> (Vec<String>, BTreeMap<String, String>) {
1422 let mut names = Vec::new();
1423 let mut described = BTreeMap::new();
1424 for field in fields {
1425 for at in 0..pass.view.standing(field) {
1426 let slots = if field.repeats.is_some() {
1427 field.instance_fields(at)
1428 } else {
1429 vec![field.clone()]
1430 };
1431 for slot in slots {
1432 if let Some(value) = slot.value {
1433 described.insert(slot.name.clone(), value);
1434 }
1435 names.push(slot.name);
1436 }
1437 }
1438 }
1439 (names, described)
1440 }
1441
1442 /// The ticks a control writing over a selection sends, and nothing for one that
1443 /// does not.
1444 ///
1445 /// Under `Node::TICKED`, which is the name every renderer sends them under and
1446 /// what the vocabulary documents.
1447 fn gathered(pass: &Pass<'_>, over: Option<&str>) -> Params {
1448 over.map_or_else(Params::new, |_| {
1449 pass.view.gathering(quasi_router::Node::TICKED)
1450 })
1451 }
1452
1453 /// A form: its fields, then the one control that answers all of them.
1454 fn form(
1455 pass: &mut Pass<'_>,
1456 ui: &mut Ui,
1457 fields: &[quasi_router::Field],
1458 submit: &str,
1459 action: &Action,
1460 ) {
1461 for described in fields {
1462 field_node(pass, ui, described);
1463 }
1464 if ui.button(RichText::new(submit)).clicked() {
1465 // A question answered N times sends N values under N indexed names, in
1466 // one submission with the rest of the form. That is the whole of what
1467 // the member is for. `60d1753c`.
1468 let (names, described) = submitted(pass, fields);
1469 let payload = pass.view.submission(&names, &described);
1470 pass.fire(action, payload, None);
1471 }
1472 }
1473
1474 /// The rows a shut branch is not covering, as the reader has left the outline.
1475 ///
1476 /// [`quasi_router::folded_by`] does the reading, so a window folds what a
1477 /// terminal and a browser fold. The reader's own answer comes first and the
1478 /// description's stands until there is one -- `Row::open` says where an outline
1479 /// starts, not where it stays.
1480 fn unfolded<'a, T: quasi_router::Outline>(rows: &'a [T], view: &crate::View) -> Vec<&'a T> {
1481 let hidden = quasi_router::folded_by(rows.iter().map(|row| {
1482 let open = row.open().map(|described| view.open(&row.key(), described));
1483 (row.depth(), open)
1484 }));
1485 rows.iter()
1486 .zip(hidden)
1487 .filter(|(_, hidden)| !*hidden)
1488 .map(|(row, _)| row)
1489 .collect()
1490 }
1491
1492 /// A row's place in an outline, drawn before its words: the indent, then the
1493 /// chevron when the row is a branch.
1494 ///
1495 /// Answers the key and the state a press would flip, so a caller can collect
1496 /// the fold and apply it once the pass is free. The chevron's rect comes back
1497 /// with it, because a caller claiming the row's own hit target has to start
1498 /// after it -- egui gives an overlapping rect to whichever widget claimed it
1499 /// last, and a chevron the row swallows is the one thing `Row::open` says must
1500 /// not happen.
1501 ///
1502 /// The room is spent on a leaf too, or its label sits left of its own parent's.
1503 fn outline_lead(
1504 ui: &mut Ui,
1505 view: &crate::View,
1506 row: &impl quasi_router::Outline,
1507 ) -> (Option<(String, bool)>, Option<egui::Rect>) {
1508 let step = ui.spacing().indent;
1509 let depth = f32::from(row.depth().level);
1510 if row.depth().is_nested() {
1511 ui.add_space(step * depth);
1512 }
1513 let Some(described) = row.open() else {
1514 ui.add_space(step);
1515 return (None, None);
1516 };
1517 let key = row.key();
1518 let open = view.open(&key, described);
1519 let pressed = ui.small_button(if open { "\u{25bc}" } else { "\u{25b6}" });
1520 let folded = pressed.clicked().then_some((key, open));
1521 (folded, Some(pressed.rect))
1522 }
1523
1524 /// One row of a list.
1525 ///
1526 /// `within` is the row's place among its siblings, and it is there only to name
1527 /// the row's interact rect. See the id below for why the value cannot do it
1528 /// alone.
1529 fn list_row(pass: &mut Pass<'_>, ui: &mut Ui, row: &Row, within: usize, branches: bool) {
1530 // Whether the press that just happened was on the chevron rather than on
1531 // the row. Collected here because the row claims the whole strip below,
1532 // and a fold that also opened the row would be one press doing two things.
1533 let mut folded = None;
1534 // Where the chevron landed, so the row's own hit target can start after it.
1535 // egui gives an overlapping rect to whichever widget claimed it last, and
1536 // the row claims the whole strip below -- so without this the row swallows
1537 // every press meant for the chevron, which is the one thing `Row::open`
1538 // says must not happen.
1539 let mut chevron = None;
1540 let strip = ui.horizontal(|ui| {
1541 // The row's own indent, then its disclosure. `ccaa7e4b`, and the
1542 // chevron is **a separate hit target from the label** on purpose: this
1543 // is the shipped egui sidebar's own behaviour rather than an
1544 // improvement on it, since pressing a tag filters by it and pressing
1545 // its chevron does not.
1546 if branches {
1547 (folded, chevron) = outline_lead(ui, pass.view, row);
1548 }
1549
1550 // The tick, where the row can carry one. `toggle` first, because a row
1551 // carrying one has said the tick *is* the write and that beats the
1552 // screen's staged set.
1553 if let Some(ticked) = row.selected {
1554 let mut on = row
1555 .value
1556 .as_ref()
1557 .map_or(ticked, |value| pass.view.is_ticked(value));
1558 if ui.checkbox(&mut on, "").changed() {
1559 if let Some(action) = &row.toggle {
1560 pass.fire(action, Params::new(), None);
1561 } else if let Some(value) = &row.value {
1562 pass.view.tick(value);
1563 }
1564 }
1565 }
1566
1567 // `Part::worth` is not read here, and unlike `Flow` that is a statement
1568 // about this renderer rather than a deferral. A terminal wraps one
1569 // shared flow, so a trailing fact costs the row a whole extra line and
1570 // dropping it by worth buys back that line. Here the parts are a
1571 // horizontal strip and each elides itself, so nothing is lost to *the
1572 // run* being too tall -- what is lost is whatever sits past the right
1573 // edge once an earlier part has taken the width. That is a budget
1574 // problem rather than a worth problem, and `budget` below is the
1575 // answer to it.
1576 let spacing = ui.spacing().item_spacing.x;
1577 for (index, part) in row.cells.iter().enumerate() {
1578 let after = tail_width(ui, pass.immediate, &row.cells[index + 1..], spacing);
1579 row_part(pass, ui, part, after);
1580 }
1581 });
1582
1583 // Where this row landed, for a host reading a gesture the description does
1584 // not carry. Noted for every row rather than only pressable ones: "is the
1585 // pointer over a row" has to be answerable for the row that answers
1586 // nothing, which is the case a drag guard needs. See `crate::geometry`.
1587 crate::geometry::note_row(
1588 ui,
1589 row.value.as_deref(),
1590 within,
1591 strip.response.rect,
1592 row.chosen == Some(true),
1593 );
1594
1595 // Opening the row, and asking what else it offers, both land on the strip
1596 // the row just drew.
1597 //
1598 // **The rect is the `horizontal`'s own response, not `ui.min_rect()`.** That
1599 // was the rect until 2026-08-17, and every row of a list is drawn into one
1600 // shared `Ui` -- a column-less table loops `list_row` over it -- so `min_rect` grew
1601 // with each row and the fourth row's target covered the first four. Four
1602 // overlapping rects, one pointer, and the row that answered was whichever
1603 // egui hit last rather than the one under the cursor. The `horizontal`
1604 // answers this row's strip and nothing above it.
1605 //
1606 // One `interact` for both gestures, claimed whenever the row has either: a
1607 // row that only offers a menu still needs somewhere to right-click.
1608 // `Sense::click()` covers the secondary button -- egui reads a context click
1609 // off the same sense -- so nothing about the opening gesture changes.
1610 // The fold, applied after the strip: the view is behind the pass, which the
1611 // closure above cannot hold. `folded` also stands in for "the press was the
1612 // chevron's", so the row below is not opened by it.
1613 if let Some((key, open)) = folded {
1614 pass.view.fold(&key, open);
1615 return;
1616 }
1617
1618 if row.activate.is_some() || !row.menu.is_empty() {
1619 // **The id falls back to the row's position, and has to.** It was
1620 // `("row", value.unwrap_or(""))` until 2026-08-17, and `Row::value` is
1621 // only set by `ticking`, so every row of a list without checkboxes was
1622 // `("row", "")` -- one id for all of them. egui answers a repeated id
1623 // with a "First use / Second use" error label and one shared
1624 // interaction, so the second row onwards could not be clicked at all.
1625 // Sibling of the `min_rect` defect fixed above and invisible for the
1626 // same reason: nothing here could press a row until this file could
1627 // measure text.
1628 //
1629 // The value still wins where there is one, because it survives a
1630 // reorder and an index does not.
1631 let id = match row.value.as_deref() {
1632 Some(value) => ui.id().with(("row", value)),
1633 None => ui.id().with(("row-at", within)),
1634 };
1635 // Everything the row drew except its chevron. See `chevron` above.
1636 let mut target = strip.response.rect;
1637 if let Some(chevron) = chevron {
1638 target.min.x = target.min.x.max(chevron.max.x);
1639 }
1640 let response = ui.interact(target, id, egui::Sense::click());
1641 // Say what was claimed, so the row exists for something other than a
1642 // pointer.
1643 //
1644 // A bare `ui.interact` registers no `WidgetInfo`, and egui builds its
1645 // accessibility tree from `WidgetInfo` alone, so until 2026-08-22 a row
1646 // that opens or carries a menu contributed **no node at all**. Not
1647 // mislabelled: absent. It worked under a mouse and did not exist for a
1648 // keyboard or a screen reader, which on audiofiles' tag queue meant
1649 // there was no way but the mouse to open one of the tag groups the
1650 // screen navigates by. Found by a harness reading what the renderer
1651 // drew.
1652 //
1653 // Named by the row's first text, which is the same thing a reader would
1654 // call the row and the same thing `Row::new` takes.
1655 let named = row_name(row);
1656 response.widget_info(|| {
1657 egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &named)
1658 });
1659 // The menu first. A right-click lands on the same rect as a left-click,
1660 // and firing `activate` off it would open the row the user was asking
1661 // what they could do to.
1662 if let Some((action, confirm)) = row_menu(pass.immediate, &response, &row.menu) {
1663 pass.fire(&action, Params::new(), confirm.as_deref());
1664 } else if let Some(action) = &row.activate
1665 && response.clicked()
1666 {
1667 pass.fire(action, choosing(ui, row.chosen), None);
1668 }
1669 }
1670 }
1671
1672 /// Say that a table row can be opened, once, on its first column.
1673 ///
1674 /// The press is claimed per cell, and saying so per cell would put a button in
1675 /// the tree for every column of every row -- five buttons all called "kick.wav"
1676 /// on a five-column table, which is noise rather than access. The first column
1677 /// is where the row's name already is, so that is where it is announced;
1678 /// pressing any cell still opens it, exactly as before.
1679 ///
1680 /// Second half of `461582b5`. The list half is [`row`], where a row has one
1681 /// rect and one place to say this.
1682 fn announce_row(ui: &Ui, response: &egui::Response, index: usize, row: &Row) {
1683 if index != 0 || (row.activate.is_none() && row.menu.is_empty()) {
1684 return;
1685 }
1686 let named = row_name(row);
1687 response.widget_info(|| {
1688 egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &named)
1689 });
1690 }
1691
1692 /// What a table row is called: the first thing in its first cell.
1693 /// The first thing a run of leaves says.
1694 fn row_leaf_text(parts: &[Node]) -> String {
1695 parts
1696 .iter()
1697 .find_map(|part| match part {
1698 Node::Text { text, .. } | Node::Heading { text, .. } | Node::Link { text, .. } => {
1699 Some(text.clone())
1700 }
1701 _ => None,
1702 })
1703 .unwrap_or_default()
1704 }
1705
1706 /// What a row is called: the first thing it says.
1707 ///
1708 /// For [`egui::WidgetInfo`], which needs one string where a row is a run of
1709 /// leaves. The first textual part is what `Row::new` takes and what a reader
1710 /// would call the row, so there is nothing to invent here.
1711 ///
1712 /// One function since the 2026-09-05 collapse. `cells_name` was its counterpart
1713 /// for the other container and asked the same question of the same type, cell
1714 /// by cell rather than over a flattened copy. This keeps the cell-by-cell
1715 /// walk: it is the one that does not clone every node in the row, and this
1716 /// runs per frame.
1717 fn row_name(row: &quasi_router::Row) -> String {
1718 row.cells
1719 .iter()
1720 .find_map(|cell| {
1721 let said = row_leaf_text(&cell.content);
1722 (!said.is_empty()).then_some(said)
1723 })
1724 .unwrap_or_default()
1725 }
1726
1727 /// One part of a row's run.
1728 ///
1729 /// A part is a role and a node, so the drawing is `draw` again: the role says
1730 /// where it sits in the run and the node says what it is. That is the property
1731 /// the containment migration bought every renderer, and it is why a row does not
1732 /// need a second switch over member types here.
1733 fn row_part(pass: &mut Pass<'_>, ui: &mut Ui, part: &quasi_router::Cell, after: Option<f32>) {
1734 // The cap, settled by `7bfb554a`: a run is a line, and a part takes the
1735 // lines its flow allows and no more. Only the text leaves need it -- a
1736 // token, a control and a meter are single-line widgets already, and putting
1737 // them through a galley would be saying something about them that the
1738 // description did not.
1739 // A cell holds a run since the 2026-09-05 collapse, so this walks it. The
1740 // flow is the cell's, which is the column's unless the description narrowed
1741 // it, and it applies to every text leaf in the run.
1742 let flow = part.room();
1743 for node in &part.content {
1744 match node {
1745 Node::Text { text, .. } => capped(pass.immediate, ui, text, flow, after),
1746 Node::Rich { source, .. } => {
1747 capped(
1748 pass.immediate,
1749 ui,
1750 &docengine::render_plain(source),
1751 flow,
1752 after,
1753 );
1754 }
1755 other => draw(pass, ui, other),
1756 }
1757 }
1758 }
1759
1760 /// What the rest of the run needs, if this crate can say.
1761 ///
1762 /// `None` the moment one part cannot be measured. A budget built on a guess
1763 /// about an unknown widget is worse than no budget: too small and the flexible
1764 /// part is elided for room nobody used, too large and the guess bought nothing.
1765 /// The parts that can be measured are the ones whose width is their text --
1766 /// which is every part a row has ever held in practice.
1767 fn tail_width(
1768 ui: &Ui,
1769 immediate: &Immediate,
1770 rest: &[quasi_router::Cell],
1771 spacing: f32,
1772 ) -> Option<f32> {
1773 if rest.is_empty() {
1774 return None;
1775 }
1776 let mut total = 0.0;
1777 for part in rest {
1778 total += intrinsic_width(ui, immediate, part)? + spacing;
1779 }
1780 Some(total)
1781 }
1782
1783 /// The width a cell wants when nothing is squeezing it.
1784 ///
1785 /// A cell holds a run since the 2026-09-05 collapse, so this is the sum of its
1786 /// leaves. `None` if any leaf cannot say, which is the same answer the single
1787 /// node gave before: a run containing something unmeasurable is unmeasurable.
1788 fn intrinsic_width(ui: &Ui, immediate: &Immediate, part: &quasi_router::Cell) -> Option<f32> {
1789 let mut total = 0.0;
1790 for node in &part.content {
1791 total += leaf_width(ui, immediate, node)?;
1792 }
1793 Some(total)
1794 }
1795
1796 /// The width one leaf wants when nothing is squeezing it.
1797 fn leaf_width(ui: &Ui, immediate: &Immediate, node: &Node) -> Option<f32> {
1798 match node {
1799 Node::Text { text, .. } => Some(text_width(ui, text)),
1800 Node::Rich { source, .. } => Some(text_width(ui, &docengine::render_plain(source))),
1801 // A token is its words plus `token_padding` on both sides, which is
1802 // `widget::token`'s own arithmetic rather than a guess at it. Taking
1803 // the button padding here instead would be close enough to look right
1804 // and wrong enough to lose the last part in the run.
1805 Node::Token(tag) => {
1806 Some(text_width(ui, &tag.label) + immediate.widget.token_padding.x * 2.0)
1807 }
1808 Node::Act(act) => {
1809 let drawn = act.as_layout();
1810 let label = match drawn.key {
1811 Some(key) => format!("{} ({key})", drawn.label),
1812 None => drawn.label.to_owned(),
1813 };
1814 // A control is an `egui::Button`, so this padding really is the
1815 // style's button padding.
1816 Some(text_width(ui, &label) + ui.spacing().button_padding.x * 2.0)
1817 }
1818 _ => None,
1819 }
1820 }
1821
1822 /// How wide this text is laid out with nothing in its way.
1823 fn text_width(ui: &Ui, text: &str) -> f32 {
1824 let job = egui::text::LayoutJob::single_section(
1825 text.to_owned(),
1826 egui::TextFormat {
1827 font_id: egui::TextStyle::Body.resolve(ui.style()),
1828 ..Default::default()
1829 },
1830 );
1831 // Measured in the frame that draws, never remembered between frames. The
1832 // renderer's standing promise is that the same description at the same
1833 // width is the same picture, and a measurement carried over from a
1834 // narrower frame is exactly how that would stop being true.
1835 ui.painter().layout_job(job).rect.width()
1836 }
1837
1838 /// The width a flexible part may take, once the rest of the run is accounted.
1839 ///
1840 /// Reserving only when the reservation leaves something behind, which is the
1841 /// rule quasi-tui arrived at from the other end: a budget that cannot fit the
1842 /// tail anyway would elide the primary to make room for parts that are still
1843 /// past the edge, spending the one thing on screen for nothing.
1844 fn budget(available: f32, after: Option<f32>) -> f32 {
1845 match after {
1846 Some(reserved) if reserved < available => available - reserved,
1847 _ => available,
1848 }
1849 }
1850
1851 /// A text leaf drawn under a line budget.
1852 ///
1853 /// `ui.label(RichText)` grows to as many rows as the words need, which is what
1854 /// `leaf` does everywhere outside a run and is right there: a block may be as
1855 /// tall as it is. Inside a run it is the unbounded behaviour `Flow` exists to
1856 /// end, so this is the same text through a `LayoutJob`, whose wrapping carries
1857 /// both the budget and the ellipsis.
1858 ///
1859 /// `max_width` has to be set from the `Ui`: a job defaults to infinite width,
1860 /// so a budget without it would never wrap and never elide, and the cap would
1861 /// silently do nothing.
1862 fn capped(immediate: &Immediate, ui: &mut Ui, text: &str, flow: layout::Flow, after: Option<f32>) {
1863 let mut job = egui::text::LayoutJob::single_section(
1864 text.to_owned(),
1865 egui::TextFormat {
1866 font_id: egui::TextStyle::Body.resolve(ui.style()),
1867 color: immediate.palette.content,
1868 ..Default::default()
1869 },
1870 );
1871 job.wrap = egui::text::TextWrapping {
1872 max_width: budget(ui.available_width(), after),
1873 max_rows: flow.lines() as usize,
1874 // Words, unless one line is all there is: a single row elided at a word
1875 // boundary can lose most of the row, which is the case egui's own docs
1876 // name for breaking anywhere.
1877 break_anywhere: flow.lines() == 1,
1878 overflow_character: Some('\u{2026}'),
1879 };
1880 // Laid out here rather than handed to `ui.label`, and this is the whole
1881 // difference between a budget that works and one that reads as though it
1882 // does. A `Label` re-lays a job it is given, overwriting `max_width` with
1883 // the `Ui`'s available width: the row's first part then elided at the full
1884 // pane, took all of it, and the parts after it were left with nothing --
1885 // which is the defect this budget was written to fix, surviving the fix.
1886 // A galley is already laid out, so a `Label` built from one draws exactly
1887 // what was measured.
1888 let galley = ui.painter().layout_job(job);
1889 ui.add(egui::Label::new(galley));
1890 }
1891
1892 /// The cell just drawn, sensed for the two gestures a table row answers.
1893 ///
1894 /// **`ui.response()` cannot answer either of them**, and was what stood here
1895 /// The response a `Ui` gives for itself carries the sense it was built with,
1896 /// and `UiBuilder`'s default is `Sense::hover`, so `clicked` and the secondary
1897 /// click `Response::context_menu` reads are both permanently false. A table
1898 /// row could not be opened and its menu could not be raised, in the one
1899 /// consumer whose file list is a `Node::Table`.
1900 ///
1901 /// Interacting for the sense we need is the move `list_row` already makes. The
1902 /// id needs no disambiguator the way a list row's does: `makeover_immediate`
1903 /// gives each cell its own `Ui`, so `ui.id()` differs per cell already.
1904 fn cell_row_response(ui: &mut Ui) -> egui::Response {
1905 ui.interact(
1906 ui.min_rect(),
1907 ui.id().with("cell-row"),
1908 egui::Sense::click(),
1909 )
1910 }
1911
1912 /// What a press on a chosen-able row meant, as a payload.
1913 ///
1914 /// The renderer's half of `Row::chosen`: the description says which rows are
1915 /// chosen and the app owns the set, and this is the part only this side can
1916 /// answer -- what the press *meant*, read off the modifiers this host has.
1917 ///
1918 /// The mapping is the desktop's, unchanged since the Macintosh Finder: a plain
1919 /// click chooses the row alone, ctrl (command on macOS) toggles it into the set,
1920 /// and shift takes everything between the app's pointer and here. egui's
1921 /// `Modifiers::command` is already the platform's own answer to which of ctrl
1922 /// and command means "the modifier key", so nothing here is per-OS.
1923 ///
1924 /// **Shift wins over command** when both are held, which `Choosing` rules on
1925 /// rather than leaving to a renderer. audiofiles' own shipped list read them
1926 /// the other way round (`ui/file_list.rs::handle_click`, before `49b7429`), so
1927 /// this is a change to that app of the rarest press it has.
1928 ///
1929 /// That is what makes this additive rather than a change to every row
1930 /// activation in the tree.
1931 fn choosing(ui: &Ui, chosen: Option<bool>) -> Params {
1932 if chosen.is_none() {
1933 return Params::new();
1934 }
1935 let modifiers = ui.ctx().input(|input| input.modifiers);
1936 let meant = if modifiers.shift {
1937 quasi_router::Choosing::Through
1938 } else if modifiers.command {
1939 quasi_router::Choosing::Also
1940 } else {
1941 quasi_router::Choosing::Only
1942 };
1943 Params::new().with(
1944 quasi_router::Node::CHOOSING.to_owned(),
1945 meant.as_str().to_owned(),
1946 )
1947 }
1948
1949 /// What a row offers, on the gesture this host means by asking.
1950 ///
1951 /// A list row and a table row are one type, so a menu draws through one
1952 /// function. The description's own instruction is that a menu
1953 /// is "reached by right-click on a pointer host, long-press on a touch one, and
1954 /// a key in a terminal"; egui is a pointer host, and `Response::context_menu` is
1955 /// its right-click, so there is nothing for this renderer to invent.
1956 ///
1957 /// A menu dropped silently, with no arm and no comment, is the failure mode the
1958 /// wildcard arms in this file are written to avoid: the description says what
1959 /// the row offers, and a renderer that quietly offers nothing is the one
1960 /// outcome that must not happen.
1961 ///
1962 /// Returns what was pressed rather than firing it. Every collecting site in this
1963 /// file has the same reason -- the closure holds `&mut Ui` and firing wants the
1964 /// pass mutably -- and a menu inside a table cell is the case where it is forced,
1965 /// so both callers do it the one way.
1966 ///
1967 /// A disabled act draws and does not answer, which is `widget::act`'s own
1968 /// contract; a menu that omitted it would be a menu whose length changed with
1969 /// state, and the reader loses the place they had learned.
1970 fn row_menu(
1971 immediate: &crate::Immediate,
1972 response: &egui::Response,
1973 menu: &[Act],
1974 ) -> Option<(Action, Option<String>)> {
1975 if menu.is_empty() {
1976 return None;
1977 }
1978 let mut picked = None;
1979 response.context_menu(|ui| {
1980 for act in menu {
1981 let pressed = widget::act(ui, &act.as_layout(), &immediate.palette, &immediate.widget);
1982 if pressed.clicked() && act.state != Some(layout::State::Disabled) {
1983 picked = Some((act.action.clone(), act.confirm.clone()));
1984 // The menu closes on the press, not on the answer. A described
1985 // act may raise a confirmation before anything happens, and a
1986 // menu still standing behind that dialog is two surfaces asking
1987 // at once.
1988 ui.close();
1989 }
1990 }
1991 });
1992 picked
1993 }
1994
1995 /// What the tick column calls itself.
1996 ///
1997 /// `makeover_immediate::table` addresses a cell by its column's name, so the
1998 /// one this renderer adds needs one. Never shown as a heading.
1999 const TICK_COLUMN: &str = "select";
2000
2001 /// A described table.
2002 ///
2003 /// The narrowing, the header carets and the tracks are all
2004 /// `makeover_immediate::table`'s; what is here is the walk that turns a
2005 /// described cell into the nodes inside it, and the two facts a `Screen` adds
2006 /// over a `Column`: the address a row opens, and the address a heading reorders
2007 /// by.
2008 ///
2009 /// **A cell is a run of nodes, so drawing one is [`draw`] again.** That is the
2010 /// property the containment migration bought every renderer, and it is why a
2011 /// button in a cell needs no special case here: it is a `Node::Act` like any
2012 /// other, and it fires through the same `Pass`.
2013 /// The columns as `makeover-immediate` wants them, with the tick column in
2014 /// front when the table has ticks.
2015 ///
2016 /// Split out of [`table`] rather than inlined, which is where it was: a tick
2017 /// takes no column in the description, so this renderer adds one, and that is a
2018 /// self-contained fact about the two vocabularies meeting.
2019 fn table_columns<'a>(columns: &'a [quasi_router::Column], ticks: bool) -> Vec<layout::Column<'a>> {
2020 let mut borrowed: Vec<layout::Column<'a>> =
2021 Vec::with_capacity(columns.len() + usize::from(ticks));
2022 if ticks {
2023 borrowed.push(layout::Column {
2024 width: layout::Width::Fixed,
2025 priority: layout::Priority::Essential,
2026 ..layout::Column::new(TICK_COLUMN)
2027 });
2028 }
2029 borrowed.extend(columns.iter().map(|c| c.as_layout()));
2030 borrowed
2031 }
2032
2033 /// One table row's tick, and the value to toggle when it was pressed.
2034 ///
2035 /// Drawn from the set the view holds rather than from the description, which is
2036 /// the rule every renderer follows for a tick: the description says what
2037 /// arrived and the view says what the user has done since. The view always,
2038 /// because a table row's tick is a member of the screen's set by construction --
2039 /// unlike a list row, there is no case where the description's flag is the
2040 /// answer.
2041 ///
2042 /// Split out of [`table`] to keep that function under the line cap, and it is
2043 /// the right piece to split: a tick is the one cell in a table that is not a
2044 /// value in the grid.
2045 fn tick_cell(ui: &mut Ui, view: &crate::View, row: &Row) -> Option<String> {
2046 let (Some(_), Some(value)) = (row.selected, row.value.as_ref()) else {
2047 return None;
2048 };
2049 let mut on = view.is_ticked(value);
2050 ui.checkbox(&mut on, "").changed().then(|| value.clone())
2051 }
2052
2053 fn table(pass: &mut Pass<'_>, ui: &mut Ui, columns: &[quasi_router::Column], rows: &[Row]) {
2054 // A tick takes no column in the description, so this renderer adds one:
2055 // `makeover_immediate::table` addresses cells by column, and a checkbox
2056 // drawn outside that has no track to sit in. Essential, so narrowing never
2057 // takes the affordance away, and it is named rather than blank because two
2058 // unnamed columns would be one column twice.
2059 // The rows a shut branch is not covering, which is what the table has this
2060 // frame. Taken before anything is counted, so an index into the body is an
2061 // index into what was drawn. `quasi_router::folded_by` is the reading, so a
2062 // window and a terminal fold the same rows.
2063 let branches = rows.iter().any(|row| row.open.is_some());
2064 let shown = unfolded(rows, pass.view);
2065 let rows = shown.as_slice();
2066
2067 let ticks = rows.iter().any(|row| row.selected.is_some());
2068 let borrowed = table_columns(columns, ticks);
2069
2070 // What is drawn as a selected row: the app's pointer, and every row of a
2071 // live selection. `Body::selected` takes a predicate rather than a set,
2072 // which is what lets both facts answer through one call -- an app whose
2073 // selection is a range does not have to build a collection to be asked.
2074 //
2075 // `1894e95d`. Before this, `Row::current` was the only thing that lit a
2076 // row, so a five-hundred-row Cmd+A in audiofiles drew the same as a one-row
2077 // click. A chosen row and the current row look alike here on purpose: the
2078 // pointer sits inside the selection nearly always, and a second mark for
2079 // "and this is the one the detail pane is showing" is a distinction the
2080 // detail pane is already making.
2081 let current = |at: usize| {
2082 rows.get(at)
2083 .is_some_and(|row| row.current || row.chosen == Some(true))
2084 };
2085 let body = makeover_immediate::table::Body {
2086 rows: rows.len(),
2087 selected: Some(&current),
2088 scroll_to: None,
2089 };
2090 let sizing = makeover_immediate::table::Sizing {
2091 lengths: &[],
2092 fallback: CELL_WIDTH,
2093 };
2094
2095 // What the user pressed, collected rather than fired inside the closure:
2096 // the closure holds `&mut Ui` and the pass at once, and firing needs the
2097 // pass mutably.
2098 let mut fired: Option<(Action, Params)> = None;
2099 // Same reason as `fired`: ticking is a write to the view, and the view is
2100 // behind the pass the closure cannot hold.
2101 let mut toggled: Option<String> = None;
2102 // A menu press, kept apart from `fired` because it carries a confirmation
2103 // and because it wins: a right-click lands on the same cell a left-click
2104 // does, so collecting both in one slot would let opening the row beat the
2105 // menu the user actually asked for.
2106 let mut menued: Option<(Action, Option<String>)> = None;
2107 // A press on a branch's chevron, collected for `toggled`'s reason: folding
2108 // writes to the view and the view is behind the pass.
2109 let mut folded: Option<(String, bool)> = None;
2110
2111 let reordered = makeover_immediate::table::table(
2112 ui,
2113 &borrowed,
2114 &body,
2115 &sizing,
2116 &pass.immediate.palette,
2117 &pass.immediate.table,
2118 |ui, column, at| {
2119 let Some(row) = rows.get(at) else { return };
2120 // Labelled, so the rect below is measured whichever way this
2121 // cell left. `600c9e42`.
2122 'cell: {
2123 if ticks && column.name == TICK_COLUMN {
2124 if let Some(value) = tick_cell(ui, pass.view, row) {
2125 toggled = Some(value);
2126 }
2127 break 'cell;
2128 }
2129
2130 let Some(index) = columns.iter().position(|c| c.name == column.name) else {
2131 break 'cell;
2132 };
2133 let Some(cell) = row.cells.get(index) else {
2134 break 'cell;
2135 };
2136 // The outline, in the first column and nowhere else. A table
2137 // has no gutter to indent in and the indent is not a value in
2138 // the grid, so it rides in front of the row's leading cell --
2139 // which is the one the eye reads the hierarchy from anyway.
2140 if index == 0 && branches {
2141 let (pressed, _) = outline_lead(ui, pass.view, *row);
2142 folded = folded.take().or(pressed);
2143 }
2144 // Which side of a change this line is on, when the table is a
2145 // diff. `19d7602d`. A sign in front of the leading cell, on the
2146 // terminal renderer's reasoning: this renderer has no per-row
2147 // background to tint either, and the sign is what a reader of
2148 // diffs already reads.
2149 if index == 0
2150 && let Some(change) = row.change
2151 {
2152 let (sign, colour) = match change {
2153 layout::Change::Added => ("+", pass.immediate.palette.success),
2154 layout::Change::Removed => ("-", pass.immediate.palette.danger),
2155 // Including a kind this renderer has not learned: an
2156 // unchanged line, which draws and loses only the sign.
2157 _ => (" ", pass.immediate.palette.content_muted),
2158 };
2159 ui.label(RichText::new(sign).monospace().color(colour));
2160 }
2161 for node in &cell.content {
2162 // A cell's contents are ordinary nodes, but a press inside one
2163 // cannot reach the pass from here. Only the two that carry an
2164 // address are collected; everything else draws.
2165 match node {
2166 Node::Act(act) if act.state != Some(layout::State::Disabled) => {
2167 if widget::act(
2168 ui,
2169 &act.as_layout(),
2170 &pass.immediate.palette,
2171 &pass.immediate.widget,
2172 )
2173 .clicked()
2174 {
2175 fired = Some((act.action.clone(), Params::new()));
2176 }
2177 }
2178 Node::Link { text, action } => {
2179 if ui
2180 .link(RichText::new(text).color(pass.immediate.palette.action))
2181 .clicked()
2182 {
2183 fired = Some((action.clone(), Params::new()));
2184 }
2185 }
2186 other => leaf(pass.immediate, ui, other),
2187 }
2188 }
2189 // Opening the row itself, from whichever cell was clicked. A table
2190 // row has no single element to hang it on the way a list row hangs
2191 // it on its primary text.
2192 //
2193 // The menu hangs off the same response, and therefore off every cell:
2194 // `makeover_immediate::table` calls this per cell and answers no
2195 // row-wide rect back, so "right-click the row" is "right-click any of
2196 // its cells". That is the honest reading of what this renderer can
2197 // see, and it is also what a user expects of a table row.
2198 let response = cell_row_response(ui);
2199 announce_row(ui, &response, index, row);
2200 if let Some((action, confirm)) = row_menu(pass.immediate, &response, &row.menu) {
2201 menued = Some((action, confirm));
2202 } else if let Some(action) = &row.activate
2203 && response.clicked()
2204 {
2205 fired = Some((action.clone(), choosing(ui, row.chosen)));
2206 }
2207 }
2208
2209 // After it has drawn: a `Ui`'s `min_rect` is empty until
2210 // something is in it, so measuring first recorded a sliver at the
2211 // cell's left edge. Per cell, so the row answers under any of them.
2212 crate::geometry::note_row(
2213 ui,
2214 row.value.as_deref(),
2215 at,
2216 ui.min_rect(),
2217 row.chosen == Some(true),
2218 );
2219 },
2220 );
2221
2222 if let Some(value) = toggled {
2223 pass.view.tick(&value);
2224 }
2225 // A heading that was pressed reorders by that column, and the column says
2226 // what that calls. `sortable` is `reorder.is_some()`, so a column with no
2227 // address answers no press.
2228 let reorder = reordered
2229 .and_then(|pressed| columns.iter().find(|column| column.name == pressed.name))
2230 .and_then(|column| column.reorder.clone());
2231 table_pressed(pass, folded, menued, fired, reorder);
2232 }
2233
2234 /// What a press on a table did, once the closure that saw it has let go of the
2235 /// `Ui` and the pass is free again.
2236 fn table_pressed(
2237 pass: &mut Pass<'_>,
2238 folded: Option<(String, bool)>,
2239 menued: Option<(Action, Option<String>)>,
2240 fired: Option<(Action, Params)>,
2241 reorder: Option<Action>,
2242 ) {
2243 // The fold, instead of either press below rather than beside them: a
2244 // chevron is a separate hit target from the row, so folding a branch never
2245 // also opens it. `ccaa7e4b`. The reorder below still stands, because a
2246 // heading is not a row.
2247 if let Some((key, open)) = folded {
2248 pass.view.fold(&key, open);
2249 } else {
2250 // The menu before the row, matching the order inside the closure:
2251 // `fire` takes the first press of the frame, so whichever of the two is
2252 // offered first wins, and the menu is the one the user asked for by
2253 // name.
2254 if let Some((action, confirm)) = menued {
2255 pass.fire(&action, Params::new(), confirm.as_deref());
2256 }
2257
2258 if let Some((action, payload)) = fired {
2259 pass.fire(&action, payload, None);
2260 }
2261 }
2262
2263 if let Some(action) = reorder {
2264 pass.fire(&action, Params::new(), None);
2265 }
2266 }
2267
2268 /// A region, drawn as the surface its kind names.
2269 pub(crate) fn region(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot) {
2270 // `ae8e8836`. Scoped so there is a rect to note, and noted whether or not
2271 // anything is ever anchored here: nothing in a description says which
2272 // regions an app anchors to. A scope adds no spacing and no frame -- it is
2273 // a child `Ui` over the same available space -- so the drawing is what it
2274 // was before this wrapper existed.
2275 //
2276 // Outside the early returns in `region_body`, deliberately. A region that is
2277 // pending or failed still occupies space and can still be anchored to, and a
2278 // menu that could not open over a region that had not loaded yet would be a
2279 // rule nobody stated.
2280 let drawn = ui.scope(|ui| region_body(pass, ui, slot));
2281 crate::geometry::note_region(ui, &slot.id, drawn.response.rect);
2282 }
2283
2284 /// What a region asks when the questions inside it move.
2285 ///
2286 /// The browser puts one trigger on the region and lets the document gather
2287 /// what it contains; here the containment is walked instead, through
2288 /// [`Slot::questions`](quasi_router::Slot::questions), so the two hosts gather
2289 /// the same set from the same walk rather than from two readings of the word
2290 /// "inside".
2291 ///
2292 /// Two halves, and both are needed every frame. A dial that moved pushes the
2293 /// deadline out, which is what makes it a debounce; and a deadline that has
2294 /// come due fires, which has to be checked whether or not anything moved this
2295 /// frame -- the whole point is what happens once the moving stops.
2296 fn region_consults(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot) {
2297 if slot.consults.is_empty() {
2298 return;
2299 }
2300 let inside = slot.questions();
2301 let moved = inside
2302 .iter()
2303 .find(|field| pass.stirred.contains(&field.name))
2304 .map(|field| {
2305 pass.view
2306 .buffer(&field.name, field.value.as_deref())
2307 .clone()
2308 });
2309
2310 for (at, consult) in slot.consults.iter().enumerate() {
2311 // The floor is read against the value that moved, never against the
2312 // gathered set: `Consult::asks_about` says so, and a set has no length
2313 // a reader could predict.
2314 if let Some(moved) = &moved {
2315 if consult.asks_about(moved) {
2316 pass.view.wait_to_consult(
2317 Asking::Region(slot.id.clone()),
2318 at,
2319 std::time::Instant::now() + consult.after,
2320 );
2321 } else {
2322 // Deleting back under the floor cancels a question already
2323 // waiting, exactly as it does for a box's own.
2324 pass.view.consulted(Asking::Region(slot.id.clone()), at);
2325 }
2326 }
2327
2328 let Some(due) = pass.view.consult_due(Asking::Region(slot.id.clone()), at) else {
2329 continue;
2330 };
2331 let now = std::time::Instant::now();
2332 if now < due {
2333 // An idle app stops repainting, and a deadline nobody wakes up for
2334 // is a question never asked.
2335 ui.ctx().request_repaint_after(due - now);
2336 continue;
2337 }
2338 pass.view.consulted(Asking::Region(slot.id.clone()), at);
2339 // What rides along from outside the region goes in first, so a dial
2340 // inside it wins where a name sits on both sides.
2341 let mut payload = pass.view.contributed(&consult.sends);
2342 for field in &inside {
2343 // What the reader has typed, falling back to what the description
2344 // offered, which is the order a submit reads a form in: an
2345 // untouched dial still sends what it is showing.
2346 payload.insert(
2347 field.name.clone(),
2348 pass.view
2349 .buffer(&field.name, field.value.as_deref())
2350 .clone(),
2351 );
2352 }
2353 pass.fire(&consult.action, payload, None);
2354 }
2355 }
2356
2357 /// The region itself, inside the scope that measures it.
2358 fn region_body(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot) {
2359 // A region that does not apply right now is not drawn at all. `079a011e`:
2360 // the region names the control and the value that bring it out, and this
2361 // renderer answers it from the view it is already holding -- no request,
2362 // and nothing re-rendered on a form the reader is midway through.
2363 if pass.hidden.out(&slot.id) {
2364 return;
2365 }
2366
2367 // Readiness first: a region that is not ready has nothing to draw and says
2368 // so, which is the whole of what the axis is for.
2369 match slot.readiness {
2370 layout::Readiness::Pending => {
2371 // Wiki `loading-and-progress-standard`, rule 2. This was
2372 // `ui.spinner()` until `5eccb6aa`, drawn identically whether the
2373 // region was waiting on a measured payload or on nothing anyone
2374 // could count -- and a turning arc reads as progress, which is the
2375 // claim an unmeasured wait has not earned.
2376 //
2377 // The amount, when there is one, is what the region's own feeding
2378 // action described. The numerator is the host's and is usually
2379 // absent, which is what keeps a bar from being drawn out of a total
2380 // alone.
2381 widget::awaiting(
2382 ui,
2383 slot.awaiting().unwrap_or_default(),
2384 pass.view.progress_at(std::time::Instant::now()),
2385 pass.immediate.reduced_motion(),
2386 pass.immediate.palette(),
2387 &pass.immediate.widget,
2388 );
2389 return;
2390 }
2391 layout::Readiness::Failed => {
2392 ui.label(
2393 RichText::new("This did not load.")
2394 .color(pass.immediate.palette.tone(layout::Tone::Danger)),
2395 );
2396 return;
2397 }
2398 // `Empty` is drawn: what says a region is empty is a `Node::StandIn`
2399 // inside it, per `703f4cd2`, because a column with a heading and no
2400 // rows still has content.
2401 _ => {}
2402 }
2403
2404 let cutoff = cutoff(ui.available_width());
2405 run(pass, ui, slot, cutoff);
2406 if slot.showing().selective() {
2407 showing_body(pass, ui, slot, cutoff);
2408 } else if let Some(repeating) = slot.repeating.as_deref() {
2409 repeating_body(pass, ui, slot, repeating);
2410 } else {
2411 for placed in slot.body.iter().filter(|placed| placed.kept_at(cutoff)) {
2412 draw(pass, ui, &placed.node);
2413 }
2414 }
2415
2416 // After the body, because what the region asks about is what the body is
2417 // holding and half of it would not have been drawn yet from anywhere else.
2418 region_consults(pass, ui, slot);
2419
2420 // The host's drawing under the described blocks, and only for a bespoke
2421 // region: a fill named against a pane is a host reaching into a region the
2422 // description already owns. The ordering is the arrangement
2423 // `Containment::Opaque` describes -- a heading the description owns above a
2424 // canvas it does not.
2425 if let RegionKind::Handover { .. } | RegionKind::Ceded { .. } = slot.kind {
2426 if let Some(fill) = pass.immediate.fill(&slot.id) {
2427 fill(pass.immediate, ui);
2428 } else if slot.kind.as_layout().owed() {
2429 // The half the split exists for. Before it, a region the app had
2430 // ruled undescribable and one nobody had filled yet were the same
2431 // value here, and both drew as nothing at all.
2432 unfilled(pass.immediate, ui);
2433 }
2434 }
2435 }
2436
2437 /// A region whose children are answers to one question.
2438 ///
2439 /// Each slot under its number, with the control that takes it away, and the
2440 /// control that adds one under the lot. audiofiles' rule editor is the
2441 /// consumer and this is its host, so this is the arm the ruling was measured
2442 /// against: a condition is three questions that only mean anything together,
2443 /// which is the shape `Repeat` could not say.
2444 ///
2445 /// Everything is derived as nodes and drawn through [`draw`], so nothing here
2446 /// invents styling: a slot's number is the same heading a described one gets,
2447 /// and both controls take everything [`act_node`] knows about tone, waiting and
2448 /// confirmation.
2449 ///
2450 /// Nothing is narrowed by [`cutoff`]. A slot of a repeating question is not an
2451 /// optional member of a run -- dropping the third condition at a narrow width
2452 /// would hide an answer the reader gave, which is a different thing from
2453 /// dropping a toolbar button they can still reach from a menu.
2454 fn repeating_body(
2455 pass: &mut Pass<'_>,
2456 ui: &mut Ui,
2457 slot: &Slot,
2458 repeating: &quasi_router::Repeating,
2459 ) {
2460 let standing = slot.body.len();
2461 for (at, placed) in slot.body.iter().enumerate() {
2462 // One-based, because it is read by a person.
2463 draw(
2464 pass,
2465 ui,
2466 &Node::section(format!("{} {}", repeating.one, at + 1)),
2467 );
2468 draw(pass, ui, &placed.node);
2469
2470 // The child's own, because only the child knows which slot it is. The
2471 // boundary is drawn rather than hidden, which is the call audiofiles'
2472 // editor already made for its last condition -- what changed is that
2473 // `Repeating::least` says it once instead of the app disabling its own
2474 // button.
2475 if let Node::Region(child) = &placed.node
2476 && let Some(removes) = &child.removes
2477 {
2478 draw(
2479 pass,
2480 ui,
2481 &Node::Act(bounded(removes, repeating.may_remove(standing))),
2482 );
2483 }
2484 }
2485 draw(
2486 pass,
2487 ui,
2488 &Node::Act(bounded(&repeating.add, repeating.may_add(standing))),
2489 );
2490 }
2491
2492 /// One control, at whatever the floor or the ceiling says.
2493 fn bounded(act: &quasi_router::Act, allowed: bool) -> quasi_router::Act {
2494 if allowed {
2495 act.clone()
2496 } else {
2497 act.clone().disabled()
2498 }
2499 }
2500
2501 /// A region showing one child at a time, and the chrome that moves between them.
2502 ///
2503 /// The derivation quasi-webview and quasi-tui both make, arriving here third.
2504 /// Nothing reads [`RegionKind::Widget`]'s name: a carousel, a tab group and a
2505 /// disclosure are one region that shows some of its children, and which idiom
2506 /// comes out falls out of what the children carry.
2507 ///
2508 /// **This renderer drew none of it until now.** `region_body` walked the whole
2509 /// body whatever [`layout::Showing`] said, so a described tab group came out as
2510 /// every panel stacked with no strip -- the same shape of defect as `run`'s,
2511 /// where a description said something and this renderer silently drew something
2512 /// else. The two findings this module's header recorded as gaps in the
2513 /// *description* ("a tabbed arrangement does not say which tab is showing", "a
2514 /// tab has no label") were both answered by `Showing` and [`Slot::label`]
2515 /// before this; what was left was nobody here reading them.
2516 ///
2517 /// # The three shapes, and what picks between them
2518 ///
2519 /// quasi-webview's, unchanged, because the picking is the vocabulary's and not
2520 /// the host's:
2521 ///
2522 /// - One dismissible child with a label is a summary line that opens.
2523 /// - Children carrying labels get a strip of them.
2524 /// - Anything else gets previous, position, next.
2525 ///
2526 /// # Where the bytes come from
2527 ///
2528 /// A child carrying [`Slot::fed_by`] is a panel behind a route, and pressing its
2529 /// tab calls it; the answer lands as a fragment naming that child. A child
2530 /// holding its content already is moved to locally with no request, which is
2531 /// what a carousel is. Presence is the only thing that picks between the two,
2532 /// exactly as in the browser.
2533 fn showing_body(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot, cutoff: layout::Priority) {
2534 let at = pass.view.shown(slot);
2535 let labels = slot.labels();
2536 let total = slot.body.len();
2537
2538 // A named single child that can close is a disclosure, and the check comes
2539 // first because such a child is also a labelled one: a strip of one tab is
2540 // not what a summary line is.
2541 let disclosure = slot.showing().dismissible() && total == 1 && labels.len() == 1;
2542
2543 if disclosure {
2544 if ui.selectable_label(at.is_some(), labels[0]).clicked() {
2545 pass.view.disclose(slot);
2546 }
2547 } else if !labels.is_empty() {
2548 // A strip sits above the panes it opens. The folder semantic, and the
2549 // same placement the other two renderers derive.
2550 ui.horizontal(|ui| {
2551 for (index, label) in labels.iter().enumerate() {
2552 if ui.selectable_label(at == Some(index), *label).clicked() {
2553 pass.view.show(&slot.id, index);
2554 // The panel's address, when the panel is a route rather
2555 // than content already here. No target rides with it: the
2556 // router answers with a fragment naming the slot it
2557 // changed, so the party that knows stays the party that
2558 // says.
2559 if let Some(action) = fed_child(slot, index) {
2560 pass.fire(action, Params::new(), None);
2561 }
2562 }
2563 }
2564 });
2565 }
2566
2567 // One child, or none at all: `Showing::AtMostOne` closed is the only way to
2568 // reach `None` here, and drawing nothing is what closed means.
2569 if let Some(index) = at
2570 && let Some(placed) = slot.body.get(index)
2571 && placed.kept_at(cutoff)
2572 {
2573 draw(pass, ui, &placed.node);
2574 }
2575
2576 // A counter row sits under the content it counts, and only where there was
2577 // no strip to put above it. The position reads back one step: it says where
2578 // you are among the children and it is not one of them.
2579 if labels.is_empty() && !disclosure {
2580 ui.horizontal(|ui| {
2581 if ui.button("Prev").clicked() {
2582 pass.view.show_by(slot, -1);
2583 }
2584 ui.label(format!("{} / {total}", at.map_or(0, |index| index + 1)));
2585 if ui.button("Next").clicked() {
2586 pass.view.show_by(slot, 1);
2587 }
2588 });
2589 }
2590 }
2591
2592 /// The route a child of a showing region is fetched from, if it is fetched.
2593 ///
2594 /// quasi-webview's function of the same name, verbatim. Only a region can carry
2595 /// [`Slot::fed_by`], so a child that is a bare node is content already here by
2596 /// construction.
2597 fn fed_child(slot: &Slot, at: usize) -> Option<&Action> {
2598 match &slot.body.get(at)?.node {
2599 Node::Region(child) => child.fed_by.as_deref(),
2600 _ => None,
2601 }
2602 }
2603
2604 /// The region's leading row: the members the description said share it.
2605 ///
2606 /// Ruling: wiki `layout-room-and-fallback`. A member nobody can see is worse
2607 /// than one drawn in the wrong direction, so the drop is what made this a
2608 /// defect rather than a shortfall.
2609 ///
2610 /// # What each fallback gets, and what it costs
2611 ///
2612 /// The room is measured here, per frame, from the width egui is offering, the
2613 /// same reading `cutoff` already takes for a region's body. Nothing is
2614 /// authored and nothing is remembered between frames.
2615 ///
2616 /// [`Fallback::Wrap`](layout::Fallback::Wrap) and
2617 /// [`Fallback::Stack`](layout::Fallback::Stack) are both `horizontal_wrapped`.
2618 /// egui breaks the row when the next member does not fit and measures each
2619 /// member from its own galley, which is the derived minimum the ruling asks
2620 /// for. The two differ in the webview by whether a wrapped member fills its
2621 /// line; egui has no equivalent knob on a wrapped layout, so this renderer
2622 /// answers both the same way and says so rather than authoring a width.
2623 ///
2624 /// # A member that asks to fill
2625 ///
2626 /// [`layout::Width::Fill`] members share what the content-sized ones did not
2627 /// take, equally, which is that type's own rule and the same answer the
2628 /// webview's `flex: 1 1 0` gives. Each is allocated an equal share of what is
2629 /// left where it stands, so the division is exact when the fills follow the
2630 /// members that take what they need. Both shapes the description reaches for
2631 /// are that: a pane beside a pane, and a row of peers.
2632 ///
2633 /// [`layout::Width::Fixed`] takes no allocation. A run carries no size, so
2634 /// there is nothing to fix a member at.
2635 ///
2636 /// [`Shed`](layout::Fallback::Shed) and [`Menu`](layout::Fallback::Menu) drop
2637 /// by [`layout::Priority`] through [`Run::kept_at`], which the webview cannot
2638 /// do at all -- there is no `@container (inline-size < min-content)` -- and
2639 /// this renderer can, because it is holding the width. `Menu` then puts what
2640 /// it shed behind one control, so every member stays reachable; `Shed` does
2641 /// not, which is what the description asked for when it chose the word.
2642 fn run(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot, cutoff: layout::Priority) {
2643 let Some(run) = slot.run.as_ref() else {
2644 return;
2645 };
2646 let kept = run.kept_at(cutoff);
2647 ui.horizontal_wrapped(|ui| {
2648 let mut fills = kept
2649 .iter()
2650 .filter(|placed| matches!(placed.width, layout::Width::Fill))
2651 .count();
2652 for placed in &kept {
2653 if !matches!(placed.width, layout::Width::Fill) {
2654 draw(pass, ui, &placed.node);
2655 continue;
2656 }
2657 // An equal share of what is left where it stands. Exact when the
2658 // fills come after the members that take what they need, which is
2659 // both shapes the description reaches for -- a pane beside a pane,
2660 // and a row of peers -- and an approximation when a fill is written
2661 // before a content member, because an immediate-mode library has
2662 // not measured that member yet and a sizing pass to find out would
2663 // be the remembered measurement the ruling forbids.
2664 let share = ui.available_width() / fills as f32;
2665 fills = fills.saturating_sub(1);
2666 let height = ui.available_height();
2667 ui.allocate_ui(egui::vec2(share, height), |ui| {
2668 draw(pass, ui, &placed.node);
2669 });
2670 }
2671 if matches!(run.fallback, layout::Fallback::Menu) {
2672 let shed: Vec<&quasi_router::Ranked> = run
2673 .members
2674 .iter()
2675 .filter(|member| !member.kept_at(cutoff))
2676 .collect();
2677 if !shed.is_empty() {
2678 // The label is the count rather than a name, because the
2679 // description did not give the row one and inventing "More
2680 // actions" here would be this renderer writing copy.
2681 ui.menu_button(format!("{} more", shed.len()), |ui| {
2682 for placed in shed {
2683 draw(pass, ui, &placed.node);
2684 }
2685 });
2686 }
2687 }
2688 });
2689 }
2690
2691 /// The cutoff a region narrows to at this width, in points.
2692 ///
2693 /// egui measures in the same unit a browser's media query does, so the
2694 /// boundaries here are `makeover-geometry`'s size classes verbatim rather than
2695 /// a second set of numbers: a described screen narrows at the same width in a
2696 /// window and in a webview, which is the point of the classes being quoted in
2697 /// one place. The terminal renderer has to convert, because a cell is not a
2698 /// point, and says so where it does.
2699 ///
2700 /// Read off the width egui is offering right now and nothing else. That is
2701 /// what "Any width, one answer" costs in an immediate-mode library, and it is
2702 /// almost nothing -- the temptation the rule guards against is the memory
2703 /// store beside it, where a cutoff worked out once would be cheap to keep and
2704 /// would make the layout a function of the frame that put it there.
2705 pub(crate) fn cutoff(width: f32) -> layout::Priority {
2706 if width < 600.0 {
2707 layout::Priority::Essential
2708 } else if width < 840.0 {
2709 layout::Priority::Secondary
2710 } else {
2711 layout::Priority::Optional
2712 }
2713 }
2714