Skip to main content

max / audiofiles

Say what a table is not showing, now that Table can Forward fix for quasi 0.15, which gave Node::Table the `more: Option<Rest>` that Node::List already had. That was the gap this port noted three commits ago in bulk.rs: the preview table and the chosen-names list both said their overflow as a sentence at the end, because a described table of the first fifty of five hundred was indistinguishable from a table of fifty. Both say it properly now, and the subjects list stops being loose prose and becomes rows. Neither carries a `forward`: the cap is a rendering budget, the operation acts on every chosen item either way, and there is no next page to ask for -- which `Rest` can say and a sentence could not. The other four tables say `more: None` and mean it. The file list shows everything the app loaded and filtered, since windowing rows already in memory is renderer policy; the metadata and in-common tables are nine fields and three; and the shortcuts table listing anything less than every bound key would be the drift the help overlay exists to end. 451 tests pass with the feature on.
Author: Max Johnson <me@maxj.phd> · 2026-08-16 20:22 UTC
Signed with PGP, not checked
Commit: fafa664325779db4f91bda5041a180a8f1f71df9
Parent: 874aaf3
5 files changed, +88 insertions, -19 deletions
@@ -66,7 +66,7 @@
66 66 use quasi_router::layout::{FieldKind, Notice, Selector, Tone};
67 67 use quasi_router::{
68 68 Act, Action, Cell, Cells, Choice, Column, Field, Node, Outcome, RegionKind, Request, Response,
69 - RouteError, Router, Screen, Slot, Tag,
69 + Rest, RouteError, Router, Screen, Slot, Tag,
70 70 };
71 71
72 72 use super::{Chosen, Panels};
@@ -122,12 +122,14 @@
122 122 /// cost — "egui materialises every cell every frame; for a 500-row rename this
123 123 /// matters" — which is renderer policy and not the description's business. Here
124 124 /// it is honesty: a described list of the first fifty of five hundred is
125 - /// otherwise indistinguishable from a list of fifty.
125 + /// otherwise indistinguishable from a list of fifty, and [`Rest`] is what says
126 + /// which it is.
126 127 ///
127 - /// `Node::List` has `more: Option<Rest>` for exactly this and **`Node::Table`
128 - /// has nothing**, which is why the preview table says its overflow in prose
129 - /// beside itself and the chosen-names list does the same. A small gap, noted
130 - /// rather than filed: the fix is the member `List` already has.
128 + /// This port was written against 0.14, where `Node::List` had `more` and
129 + /// `Node::Table` had nothing, so both overflows were sentences at the end of a
130 + /// list. **0.15 gave `Table` the same member** and both now say it properly.
131 + /// Neither carries a `forward`: the cap is a rendering budget, the operation
132 + /// acts on every chosen item either way, and there is no next page to ask for.
131 133 const SHOWN: usize = 50;
132 134
133 135 /// Register the three modals' routes.
@@ -295,6 +297,9 @@
295 297 .with(Node::Table {
296 298 columns: vec![Column::new("Folder")],
297 299 rows,
300 + // Every folder in the vault, because a destination the picker does
301 + // not show is a destination you cannot choose.
302 + more: None,
298 303 });
299 304
300 305 closing(subjects(body, &chosen.names))
@@ -422,8 +427,19 @@
422 427 *seen.entry(new.as_str()).or_insert(0) += 1;
423 428 }
424 429
430 + let total = previews.len();
425 431 Node::Table {
426 432 columns: vec![Column::new("Old"), Column::new("New")],
433 + // Said rather than implied, as of quasi 0.15: a described table of the
434 + // first fifty of five hundred was indistinguishable from a table of
435 + // fifty until `Node::Table` grew `more`, which is the gap this port
436 + // noted at 65abb3c. No `forward`, because there is nowhere to ask --
437 + // the cap is a rendering budget and the rename acts on all of them.
438 + more: (total > SHOWN).then(|| Rest {
439 + paging: quasi_router::layout::Paging::more(SHOWN).of(total),
440 + forward: None,
441 + back: None,
442 + }),
427 443 rows: previews
428 444 .iter()
429 445 .take(SHOWN)
@@ -449,16 +465,23 @@
449 465 /// Every name the operation touches.
450 466 ///
451 467 /// The shipped modals each scroll this list at a fixed height; how much of it
452 - /// fits is the host's, and how many there are is the description's.
468 + /// fits is the host's, and how many there are is the description's. `Rest` is
469 + /// what says the second part, so the overflow is a described fact rather than a
470 + /// sentence at the end of the list.
453 471 fn subjects(body: Slot, names: &[String]) -> Slot {
454 - let mut body = body.with(Node::section(format!("{} chosen", names.len())));
455 - for name in names.iter().take(SHOWN) {
456 - body = body.with(Node::text(name));
457 - }
458 - if names.len() > SHOWN {
459 - body = body.with(Node::text(format!("...and {} more", names.len() - SHOWN)));
460 - }
461 - body
472 + body.with(Node::section(format!("{} chosen", names.len())))
473 + .with(Node::List {
474 + rows: names
475 + .iter()
476 + .take(SHOWN)
477 + .map(quasi_router::Row::new)
478 + .collect(),
479 + more: (names.len() > SHOWN).then(|| Rest {
480 + paging: quasi_router::layout::Paging::more(SHOWN).of(names.len()),
481 + forward: None,
482 + back: None,
483 + }),
484 + })
462 485 }
463 486
464 487 /// The screen a finished modal leaves behind.
@@ -278,6 +278,8 @@
278 278 .into_iter()
279 279 .map(|(field, value)| Cells::new(vec![Cell::new(field), Cell::new(value)]))
280 280 .collect(),
281 + // Nine fields at most, and every one that was found is here.
282 + more: None,
281 283 })
282 284 }
283 285
@@ -473,6 +475,7 @@
473 475 .into_iter()
474 476 .map(|(field, value)| Cells::new(vec![Cell::new(field), Cell::new(reads(value))]))
475 477 .collect(),
478 + more: None,
476 479 })
477 480 }
478 481
@@ -143,6 +143,10 @@
143 143 .iter()
144 144 .map(|sample| row(sample, shown, current))
145 145 .collect(),
146 + // Everything the app has loaded and filtered is here. Windowing
147 + // rows it already holds is a renderer's job, which is the note in
148 + // this module's header.
149 + more: None,
146 150 })
147 151 };
148 152
@@ -175,6 +175,10 @@
175 175 .activate(binding.action.clone())
176 176 })
177 177 .collect(),
178 + // Every key that is bound is listed, which is the whole claim of this
179 + // screen. A shortcuts table with something withheld would be the drift
180 + // it exists to end.
181 + more: None,
178 182 }
179 183 }
180 184
@@ -286,7 +286,9 @@
286 286 fn find(body: &[Node]) -> Option<(Vec<quasi_router::Column>, Vec<quasi_router::Cells>)> {
287 287 for node in body {
288 288 match node {
289 - Node::Table { columns, rows } => return Some((columns.clone(), rows.clone())),
289 + Node::Table { columns, rows, .. } => {
290 + return Some((columns.clone(), rows.clone()));
291 + }
290 292 Node::Region(slot) => {
291 293 if let Some(found) = find(&slot.body) {
292 294 return Some(found);
@@ -2354,7 +2356,10 @@
2354 2356 let screen = overlay(&response);
2355 2357
2356 2358 assert!(said(screen).contains("Tag 2 samples"));
2357 - assert!(said(screen).contains("kick.wav"));
2359 + // The subjects are rows rather than prose, so a host can scroll them as a
2360 + // list and the description can say how many were withheld.
2361 + let named: Vec<String> = list_of(screen).iter().filter_map(primary_of).collect();
2362 + assert_eq!(named, ["kick.wav", "snare.wav"]);
2358 2363
2359 2364 // The known tags, as badges. THE FINDING is that a field cannot say what
2360 2365 // completes it, so the set is named beside it -- the same workaround the
@@ -2633,8 +2638,19 @@
2633 2638 let bulk = FakeBulk::of(&names, 60);
2634 2639 let response = bulking(&bulk, Request::get("/bulk/tag")).unwrap();
2635 2640
2636 - assert!(said(overlay(&response)).contains("60 chosen"));
2637 - assert!(said(overlay(&response)).contains("and 10 more"));
2641 + let screen = overlay(&response);
2642 + assert!(said(screen).contains("60 chosen"));
2643 +
2644 + // The overflow is a described fact as of quasi 0.15, not a sentence at the
2645 + // end of the list: fifty rows, and `Rest` saying there are sixty.
2646 + let rows = list_of(screen);
2647 + assert_eq!(rows.len(), 50);
2648 + let more = more_of(screen).expect("the list says it is not all of them");
2649 + assert_eq!(more.paging.window.of, Some(60));
2650 + assert_eq!(more.paging.window.count, 50);
2651 + // Nothing to ask for: the cap is a rendering budget and the operation acts
2652 + // on all sixty either way.
2653 + assert!(more.forward.is_none());
2638 2654 }
2639 2655
2640 2656 // The help overlay.
@@ -2790,3 +2806,22 @@
2790 2806 });
2791 2807 assert_eq!(chosen.as_deref(), Some("shortcuts"));
2792 2808 }
2809 +
2810 + /// A row's primary part.
2811 + fn primary_of(row: &quasi_router::Row) -> Option<String> {
2812 + row.parts
2813 + .iter()
2814 + .find(|part| part.role == quasi_router::layout::RowPart::Primary)
2815 + .and_then(|part| match &part.node {
2816 + Node::Text { text, .. } => Some(text.clone()),
2817 + _ => None,
2818 + })
2819 + }
2820 +
2821 + /// What the list on a screen says it is not showing.
2822 + fn more_of(screen: &Screen) -> Option<quasi_router::Rest> {
2823 + nodes(screen).iter().find_map(|node| match node {
2824 + Node::List { more, .. } => more.clone(),
2825 + _ => None,
2826 + })
2827 + }