//! A shape that answers a run of members, spliced into the region that holds it. //! //! The shape MNW's feed panel is: `panel_body` answers `Vec` and the //! region around it takes them whole, so the members of two shapes end up in //! one container and the marks of the inner one have to move to where they //! landed. //! //! # Why it is a fixture rather than a case in `declared` //! //! A bare `Vec` is the one container with nowhere to keep a mark. Everything //! else in the tree carries its own -- a region, a table, a row, a field -- so //! a guard inside one of those is recorded where it happened. A list is //! answered by `Staged>` instead: the value keeps its own type and the //! marks ride beside it until the container splicing them in absorbs them at //! the offset its own members reached. //! //! 52 of the population's 484 shapes answer `Vec`, third after `Slot` and //! `Node`, so this is not a corner. // 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; use quasi_router::screen::{Jump, Rest}; /// What the panel read. pub(crate) struct Feed { pub heading: String, pub items: Vec, /// The pages the strip offers, which is the description's own window. pub pages: Vec, } impl Feed { /// Whether this is the page the reader is on. /// /// A fixed answer rather than a field, because what the strip needs is one /// jump differing from its siblings and which one is not interesting here. pub(crate) fn here(&self, page: usize) -> bool { page == self.pages.first().copied().unwrap_or_default() } } declare! { /// A table of the feed's items, in a shape of its own. /// /// The third level MNW's feed has and this fixture did not: the loop is /// not in the spliced shape, it is in a shape that spliced shape INCLUDES, /// under a guard. So the branch is numbered in one scope and the loop /// inside it in another, and the marks reach the region through two /// boundaries rather than one. #[staged] pub(crate) shape rows(feed: &Feed) -> Node; list { for item in feed.items.iter() { row "{item}"; } // A described pager, which is a loop inside an argument's body rather // than inside a container's. MNW's feed has one here, and it is the // last structural feature this fixture was missing. more Rest::page(0, 10).of(feed.items.len()) { for &page in feed.pages.iter() { jumping Jump::new(page, Action::get("/feed?page={page}").navigating()) { here when feed.here(page); } } } } } declare! { /// The panel's members, in order, with nothing wrapping them. /// /// A heading that is only there when the feed is named, an empty state that /// is only there when it has nothing, and a row per item. All three land in /// the caller's region, so all three marks are numbered here and moved /// there. #[staged] pub(crate) shape body(feed: &Feed) -> Vec; text "{feed.heading}" unless feed.heading.is_empty(); empty "Nothing here yet." when feed.items.is_empty(); // A guarded include of a shape that loops, which is the feed's shape and // the one that was never exercised. include rows(feed) unless feed.items.is_empty(); } declare! { /// The region the panel's members are spliced into. /// /// Its own members either side of the splice, so a mark that failed to move /// would land on one of these rather than quietly on nothing. #[staged] pub(crate) shape panel(feed: &Feed) -> Node; region "FEED" as Pane { text "Above."; include each body(feed); text "Below."; } } #[cfg(test)] mod tests { use super::*; use quasi_http::Serves as _; use quasi_router::stage::{Op, Residual}; use quasi_webview::Webview; fn feed(heading: &str, items: &[&str]) -> Feed { Feed { heading: heading.to_owned(), items: items.iter().map(|item| (*item).to_owned()).collect(), pages: vec![1, 2, 3], } } fn residual() -> Residual { quasi_webview::stage::derive(&Webview::new(), panel_staged) } /// The spliced shape's own structure survives the splice. /// /// Two branches and a loop, all three declared in `body` and all three /// recorded against the region that took its members. A mark that did not /// move would cover the caller's own text instead, which is what the /// members either side of the splice are here to catch. #[test] fn a_spliced_shape_carries_its_marks_into_the_region() { fn count(ops: &[Op], branches: &mut usize, loops: &mut usize) { for op in ops { match op { Op::Branch(body) => { *branches += 1; count(body, branches, loops); } Op::Loop(body) => { *loops += 1; count(body, branches, loops); } Op::Arms(arms) => { for arm in arms.iter() { count(arm, branches, loops); } } Op::Lit(_) | Op::Hole { .. } => {} } } } let residual = residual(); let (mut branches, mut loops) = (0, 0); count(residual.ops(), &mut branches, &mut loops); // Two guards in the spliced shape, one loop over the rows the shape it // includes draws, and one over the pager's jumps. assert_eq!(branches, 3, "{:#?}", residual.ops()); assert_eq!(loops, 2, "{:#?}", residual.ops()); // And the caller's own members are outside all of it. let settled: String = residual .ops() .iter() .filter_map(|op| match op { Op::Lit(text) => Some(text.as_ref()), _ => None, }) .collect(); assert!(settled.contains("Above."), "{settled}"); assert!(settled.contains("Below."), "{settled}"); } /// Filling the residual gives back what the renderer would have written. /// /// Across the cases the marks separate: named or not, empty or not, and /// three lengths of the run. #[test] fn a_filled_panel_is_what_the_renderer_would_have_produced() { let webview = Webview::new(); let residual = residual(); for heading in ["", "Today"] { for items in [&[][..], &["one"][..], &["one", "two", "three"][..]] { let feed = feed(heading, items); assert_eq!( webview.fragment(&panel(&feed)), panel_serve(&residual, &feed), "heading {heading:?}, {} items", items.len() ); } } } }