Skip to main content

max / goingson

Move the task list's paging onto the table Finding 2 in the module header, closed: this was a Node::Act pushed after the table because Node::Table carried no Rest, and the renderer could not tell the control belonged to the table above it. The ceiling case stays a sentence. At PAGE * PAGES there is no address that would show anything new, so a control there would ask for the rows already on screen.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-16 20:28 UTC
Signed with PGP, not checked
Commit: e2cbeeac643730dbc0c2eeec70e06b33f09cc637
Parent: 91786ef
4 files changed, +73 insertions, -43 deletions
M Cargo.lock +4 -4
@@ -8371,10 +8371,6 @@
8371 8371 "winnow 1.0.4",
8372 8372 ]
8373 8373
8374 - [[patch.unused]]
8375 - name = "ops-status"
8376 - version = "0.1.0"
8377 -
8378 8374 [[patch.unused]]
8379 8375 name = "quasi-axum"
8380 8376 version = "0.15.0"
@@ -8390,3 +8386,7 @@
8390 8386 [[patch.unused]]
8391 8387 name = "quasi-store"
8392 8388 version = "0.1.0"
8389 +
8390 + [[patch.unused]]
8391 + name = "ops-status"
8392 + version = "0.1.0"
@@ -32,7 +32,7 @@
32 32 # `quasi` feature. Optional here as well as below so a default build compiles
33 33 # neither: one `dep:` name covers both tables, which is what lets the feature
34 34 # gate the build script and the crate together.
35 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.15", optional = true }
35 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.16", optional = true }
36 36
37 37 [dependencies]
38 38 goingson-core = { workspace = true }
@@ -50,10 +50,10 @@
50 50 # dependency's source even with the feature off -- so a public clone could not
51 51 # build at all against a URL it could not reach. quasi went public that day and
52 52 # the URL moved here with it.
53 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.15", optional = true }
54 - quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.15", optional = true }
55 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.15", optional = true }
56 - quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.15", optional = true }
53 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.16", optional = true }
54 + quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.16", optional = true }
55 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.16", optional = true }
56 + quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.16", optional = true }
57 57
58 58 # Tauri
59 59 tauri = { workspace = true, features = ["image-png", "devtools"] }
@@ -59,14 +59,31 @@
59 59 //! would be permanent machinery in service of a frontend being removed. Add a
60 60 //! column in both by hand in the meantime.
61 61 //!
62 - //! **2. A table cannot say there is more.** [`Node::List`] carries a
63 - //! [`Rest`](quasi_router::screen::Rest) and `Node::Table` carries nothing of the
64 - //! kind, so paging is a [`Node::Act`] sitting under the table rather than a
65 - //! property of it. The shipped screen streams pages into a virtual scroller as
66 - //! you scroll; a description has no word for that either, and the address-shaped
67 - //! answer the mail list already settled on — the view says how many rows it
68 - //! shows, and asking for more is an address — is what this uses. What is lost is
69 - //! the renderer knowing the act belongs to the table above it.
62 + //! **2. A table could not say there is more. Closed in quasi 0.15.0.**
63 + //! [`Node::List`] carried a [`Rest`](quasi_router::screen::Rest) and
64 + //! `Node::Table` carried nothing of the kind, so paging was a [`Node::Act`]
65 + //! sitting under the table rather than a property of it, and what that lost was
66 + //! the renderer knowing the act belonged to the table above it. This screen was
67 + //! the finding's only consumer and is what closed it: `Node::Table` takes a
68 + //! `Rest` now and the act is gone.
69 + //!
70 + //! The reshape that made it possible is worth knowing about, because it changed
71 + //! what a description can say rather than only where it says it. `Rest` used to
72 + //! be a count and one address; it is a `layout::Paging` and up to two now, so
73 + //! the description carries *where the reader is* and not merely that there is
74 + //! more. That is the same `layout::Window` a carousel instantiates — a frame is
75 + //! a window of one over children that are present, a page is a window over rows
76 + //! that were never fetched — and presence shows up as whether there are
77 + //! addresses rather than as a second copy of the arithmetic.
78 + //!
79 + //! The ceiling case is still a sentence rather than a control: at `PAGE * PAGES`
80 + //! there is no address that would show anything new, and narrowing is the way
81 + //! through a list that long.
82 + //!
83 + //! The shipped screen streams pages into a virtual scroller as you scroll, and a
84 + //! description still has no word for that. It is deliberately not one: virtual
85 + //! scrolling windows rows the app already holds, which is a renderer performance
86 + //! technique, where a `Rest` is a fact about rows that were never fetched.
70 87 //!
71 88 //! **3. A table row could not join a selection. Closed in quasi 0.13.0.**
72 89 //! `Screen::selecting`, [`Row::ticking`](quasi_router::screen::Row::ticking) and
@@ -135,7 +152,7 @@
135 152 TaskStatus,
136 153 };
137 154 use makeover_layout::{Sort, Tone, Width};
138 - use quasi_router::screen::{Act, Cell, Cells, Choice, Column, Field, Figure, Meter, Tag};
155 + use quasi_router::screen::{Act, Cell, Cells, Choice, Column, Field, Figure, Meter, Rest, Tag};
139 156 use quasi_router::{Action, Node, RegionKind, Response, RouteError, Router, Screen, Slot};
140 157
141 158 use crate::state::{AppState, DESKTOP_USER_ID};
@@ -871,31 +888,39 @@
871 888 return Ok(vec![nothing(state, view)?]);
872 889 }
873 890
891 + let shown = i64::try_from(tasks.len()).unwrap_or(i64::MAX);
892 + let next = (view.shown + PAGE).min(PAGE * PAGES);
893 +
894 + // The table's own, since quasi 0.15.0. This was a `Node::Act` pushed after
895 + // the table until then, because `Node::Table` carried no `Rest` and there
896 + // was nowhere else to put it; what that cost was the renderer knowing the
897 + // control belonged to the table above it.
898 + let more = (shown < total && next > view.shown).then(|| {
899 + Rest::more(
900 + usize::try_from(shown).unwrap_or(usize::MAX),
901 + View {
902 + shown: next,
903 + ..view.clone()
904 + }
905 + .list(),
906 + )
907 + .of(usize::try_from(total).unwrap_or(usize::MAX))
908 + });
909 +
874 910 let mut out = vec![Node::Table {
875 911 columns: columns(view),
876 912 rows: tasks.iter().map(|task| row_for(task, view)).collect(),
913 + more,
877 914 }];
878 915
879 - let shown = i64::try_from(tasks.len()).unwrap_or(i64::MAX);
880 - if shown < total {
881 - let next = (view.shown + PAGE).min(PAGE * PAGES);
882 - // At the ceiling the act would ask for the rows already on screen, so
883 - // what is offered instead is the honest sentence. Narrowing is the way
884 - // through a list this long, and the filters are on the same screen.
885 - if next > view.shown {
886 - out.push(Node::act(
887 - format!("Show more ({shown} of {total})"),
888 - View {
889 - shown: next,
890 - ..view.clone()
891 - }
892 - .list(),
893 - ));
894 - } else {
895 - out.push(Node::text(format!(
896 - "Showing {shown} of {total}. Narrow the list to see the rest."
897 - )));
898 - }
916 + // At the ceiling there is no address that would show anything new, so the
917 + // honest sentence is offered instead of a control that asks for the rows
918 + // already on screen. Narrowing is the way through a list this long, and the
919 + // filters are on the same screen.
920 + if shown < total && next <= view.shown {
921 + out.push(Node::text(format!(
922 + "Showing {shown} of {total}. Narrow the list to see the rest."
923 + )));
899 924 }
900 925
901 926 Ok(out)
@@ -414,22 +414,27 @@
414 414 task(&state, "The only one");
415 415
416 416 let short = screen(&state);
417 - assert!(!short.contains("Show more"), "{short}");
417 + assert!(!short.contains("rest-position"), "{short}");
418 418
419 419 // `shown` is clamped to at least a page, so a short list never offers it
420 420 // however the address is typed.
421 421 let typed_low = html(get(&state, "/tasks/list", Params::new().with("shown", "1")));
422 - assert!(!typed_low.contains("Show more"), "{typed_low}");
422 + assert!(!typed_low.contains("rest-position"), "{typed_low}");
423 423
424 424 // Past one page it does, and it names how far along the reader is, which is
425 - // what the shipped count chip says while pages stream in. Finding 2 in the
426 - // module header: this act is what a table cannot say for itself.
425 + // what the shipped count chip says while pages stream in.
426 + //
427 + // Finding 2 in the module header, closed in quasi 0.15.0: this used to be a
428 + // `Node::Act` under the table because a table could not say it for itself,
429 + // and it is the table's own `Rest` now. So the assertion is on the pager the
430 + // renderer draws from the description rather than on an act's label.
427 431 for n in 0..super::PAGE {
428 432 task(&state, &format!("Filler {n}"));
429 433 }
430 434 let long = screen(&state);
431 - assert!(long.contains("Show more (200 of 201)"), "{long}");
435 + assert!(long.contains("200 of 201"), "{long}");
432 436 assert!(long.contains("shown=400"), "{long}");
437 + assert!(long.contains("rest-position"), "{long}");
433 438 }
434 439
435 440 // Writes