//! A described pager, derived and filled. //! //! The second shape here written for a feature rather than ported from a //! screen, and it exists for the same reason [`crate::marked`] does: the //! feature is what makes a paged screen derivable at all. //! //! `more rest(loaded)` was a hole in a slot that takes a `Rest`, which has no //! sentinel, and staging through the constructor did not help because the //! supplier takes a struct. So a paged table could not reach the seam however //! it was written. Said as a description the pager is a `Rest` settled by its //! own body -- one guard per direction -- and everything it carries is a number //! or an address, which are the two things a residual already holds. //! //! MNW's `/git` is the screen this is drawn from, down to its pager rendering //! `Show more` rather than a page count: `Rest::page` with no total leaves //! `pages_total` unsaid, so the only request-varying bytes are the two //! addresses. // The fixture data below is built by this module's tests and by nothing else: // the bench itself measures the shapes, not the rows behind them. #![allow(dead_code)] use quasi_declare::declare; use quasi_router::{Action, Jump, Rest}; /// How many rows a page holds. A `const`, so the residual bakes it. const PER: usize = 20; /// One page of repositories, as the screen reads it. pub(crate) struct Loaded { pub names: Vec, pub page: usize, pub has_more: bool, } impl Loaded { pub(crate) fn new(rows: usize, page: usize, has_more: bool) -> Self { Self { names: (0..rows).map(|n| format!("repo{n}")).collect(), page, has_more, } } /// Where this page starts. A supplier because the form has no arithmetic. fn offset(&self) -> usize { (self.page - 1) * PER } fn previous(&self) -> usize { self.page - 1 } fn next(&self) -> usize { self.page + 1 } } declare! { /// The listing, with the pager that says what it has not shown. #[staged] pub(crate) shape listing(loaded: &Loaded) -> Node; table { column "Repository" { width Fill; } for name in loaded.names.iter() { cells { cell name.clone(); } } // One page of one is the whole listing, and the pager is what says so. // The two directions are guarded separately because they are separate // facts: a middle page offers both, the first only forward, the last // only back. more Rest::page(loaded.offset(), PER) { back Action::get("/git?page={loaded.previous()}").navigating() when loaded.page over 1; forward Action::get("/git?page={loaded.next()}").navigating() when loaded.has_more; } when loaded.page over 1 or loaded.has_more; } } #[cfg(test)] mod tests { use quasi_http::Serves as _; use quasi_router::stage::{Op, Plan, Residual}; use quasi_webview::Webview; use super::*; fn residual() -> Residual { quasi_webview::stage::derive(&Webview::new(), listing_staged) } /// The pager is a branch, and each direction is a branch inside it. #[test] fn the_pager_and_both_its_directions_are_branches() { let residual = residual(); fn deepest(ops: &[Op], depth: usize) -> usize { ops.iter() .map(|op| match op { Op::Lit(_) | Op::Hole { .. } => depth, Op::Branch(body) => deepest(body, depth + 1), Op::Arms(arms) => arms .iter() .map(|arm| deepest(arm, depth + 1)) .max() .unwrap_or(depth), Op::Loop(body) => deepest(body, depth), }) .max() .unwrap_or(depth) } assert!( deepest(residual.ops(), 0) >= 2, "the pager holds its directions: {:#?}", residual.ops() ); } #[test] fn replaying_the_residual_reproduces_the_staged_render() { let webview = Webview::new(); let residual = residual(); fn replay(ops: &[Op], rows: usize, out: &mut String) { for op in ops { match op { Op::Lit(text) => out.push_str(text), Op::Hole { scope, id } => { out.push_str(&quasi_router::stage::sentinel_at(*scope, *id)); } Op::Branch(body) => replay(body, rows, out), Op::Arms(arms) => replay(&arms[0], rows, out), Op::Loop(body) => { for _ in 0..rows { replay(body, rows, out); } } } } } for rows in [1, 2, 5] { let mut replayed = String::new(); replay(residual.ops(), rows, &mut replayed); assert_eq!( webview.fragment(&listing_staged(&Plan::full(rows))), replayed, "the residual and the renderer disagree at {rows} rows" ); } } /// Every page shape a pager has: the first, a middle one, the last, and the /// one that is the whole listing and draws no pager at all. #[test] fn a_filled_listing_is_what_the_renderer_would_have_produced() { let webview = Webview::new(); let residual = residual(); for (page, has_more) in [(1, true), (2, true), (3, false), (1, false)] { for rows in [0, 3] { let loaded = Loaded::new(rows, page, has_more); assert_eq!( webview.fragment(&listing(&loaded)), listing_serve(&residual, &loaded), "page {page}, has_more {has_more}, {rows} rows" ); } } } } /// One page a strip offers, and whether it is the one being read. pub(crate) struct Offered { pub page: usize, pub here: bool, } /// A windowed set, whose strip marks the page being read. pub(crate) struct Windowed { pub page: usize, pub offered: Vec, } impl Windowed { /// A set of `pages`, the reader on `page`, every page offered. pub(crate) fn new(page: usize, pages: usize) -> Self { Self { page, offered: (1..=pages) .map(|at| Offered { page: at, here: at == page, }) .collect(), } } fn offset(&self) -> usize { (self.page - 1) * PER } } declare! { /// The same listing, paged with numbers rather than with two directions. /// /// This shape does NOT derive, and that is what it is here to hold. The /// strip marks the page a reader is on by drawing a readout where every /// other page is a control, which is a substitution rather than a gap -- /// `here` places nothing that turning it off would delete. See /// `strip_tests` below. #[staged] pub(crate) shape windowed(loaded: &Windowed) -> Node; table { column "Page" { width Fill; } more Rest::page(loaded.offset(), PER) { for offered in loaded.offered.iter() { jumping Jump::new( offered.page, Action::get("/git?page={offered.page}").navigating() ) { here when offered.here; } } } } } #[cfg(test)] mod strip_tests { use quasi_http::Serves as _; use quasi_router::stage::{Op, Residual}; use quasi_webview::Webview; use super::*; fn residual() -> Residual { quasi_webview::stage::derive(&Webview::new(), windowed_staged) } /// The strip is arms, and each arm holds what that markup needs. /// /// The shape `Op::Arms` exists for. The page a reader is on is a readout /// and every other page is a control, which is two markups at one position /// rather than markup that is there or is not -- so there is nothing for a /// branch to measure. Note what the two arms carry: the readout has the /// page number and no address, the control has both. Neither is a subset of /// the other, which is why the holes are numbered in the level around them. #[test] fn the_strip_marks_its_page_with_arms() { let residual = residual(); fn arms(ops: &[Op]) -> Option<&[std::borrow::Cow<'static, [Op]>]> { ops.iter().find_map(|op| match op { Op::Arms(arms) => Some(&**arms), Op::Branch(body) | Op::Loop(body) => arms(body), Op::Lit(_) | Op::Hole { .. } => None, }) } let found = arms(residual.ops()).expect("the strip derived arms"); assert_eq!(found.len(), 2, "{found:#?}"); fn holes(ops: &[Op]) -> Vec { ops.iter() .filter_map(|op| match op { Op::Hole { id, .. } => Some(*id), _ => None, }) .collect() } let (here, elsewhere) = (holes(&found[0]), holes(&found[1])); assert_eq!(here.len(), 1, "the readout is the page number: {here:?}"); assert_eq!( elsewhere.len(), 2, "the control is an address and a number: {elsewhere:?}" ); assert!( elsewhere.contains(&here[0]), "both arms say which page they are: {here:?} {elsewhere:?}" ); } /// Every page of a four-page set, so the marked arm is each of them in turn. #[test] fn a_filled_strip_is_what_the_renderer_would_have_produced() { let webview = Webview::new(); let residual = residual(); for page in 1..=4 { let loaded = Windowed::new(page, 4); assert_eq!( webview.fragment(&windowed(&loaded)), windowed_serve(&residual, &loaded), "page {page} of 4" ); } } /// The page numbers are filled rather than baked. /// /// A residual that had taken one arm for every row would serve the first /// page's strip to every reader, which is what happened before `Op::Arms`. #[test] fn each_page_gets_its_own_strip() { let residual = residual(); let first = windowed_serve(&residual, &Windowed::new(1, 4)); let third = windowed_serve(&residual, &Windowed::new(3, 4)); assert_ne!(first, third); assert!(first.contains("aria-current=\"page\">1"), "{first}"); assert!(third.contains("aria-current=\"page\">3"), "{third}"); // And the page a reader is on is the only one that is not a control. assert_eq!(third.matches("aria-current").count(), 1, "{third}"); assert_eq!(third.matches("rest-page\" href").count(), 3, "{third}"); } }