Skip to main content

max / quasi

7.0 KB · 199 lines History Blame Raw
1 //! A shape that answers a run of members, spliced into the region that holds it.
2 //!
3 //! The shape MNW's feed panel is: `panel_body` answers `Vec<Node>` and the
4 //! region around it takes them whole, so the members of two shapes end up in
5 //! one container and the marks of the inner one have to move to where they
6 //! landed.
7 //!
8 //! # Why it is a fixture rather than a case in `declared`
9 //!
10 //! A bare `Vec` is the one container with nowhere to keep a mark. Everything
11 //! else in the tree carries its own -- a region, a table, a row, a field -- so
12 //! a guard inside one of those is recorded where it happened. A list is
13 //! answered by `Staged<Vec<_>>` instead: the value keeps its own type and the
14 //! marks ride beside it until the container splicing them in absorbs them at
15 //! the offset its own members reached.
16 //!
17 //! 52 of the population's 484 shapes answer `Vec<Node>`, third after `Slot` and
18 //! `Node`, so this is not a corner.
19
20 // The fixture data below is built by this module's tests and by nothing else:
21 // the bench itself measures the shapes, not the rows behind them.
22 #![allow(dead_code)]
23
24 use quasi_declare::declare;
25 use quasi_router::Action;
26 use quasi_router::screen::{Jump, Rest};
27
28 /// What the panel read.
29 pub(crate) struct Feed {
30 pub heading: String,
31 pub items: Vec<String>,
32 /// The pages the strip offers, which is the description's own window.
33 pub pages: Vec<usize>,
34 }
35
36 impl Feed {
37 /// Whether this is the page the reader is on.
38 ///
39 /// A fixed answer rather than a field, because what the strip needs is one
40 /// jump differing from its siblings and which one is not interesting here.
41 pub(crate) fn here(&self, page: usize) -> bool {
42 page == self.pages.first().copied().unwrap_or_default()
43 }
44 }
45
46 declare! {
47 /// A table of the feed's items, in a shape of its own.
48 ///
49 /// The third level MNW's feed has and this fixture did not: the loop is
50 /// not in the spliced shape, it is in a shape that spliced shape INCLUDES,
51 /// under a guard. So the branch is numbered in one scope and the loop
52 /// inside it in another, and the marks reach the region through two
53 /// boundaries rather than one.
54 #[staged]
55 pub(crate) shape rows(feed: &Feed) -> Node;
56
57 list {
58 for item in feed.items.iter() {
59 row "{item}";
60 }
61
62 // A described pager, which is a loop inside an argument's body rather
63 // than inside a container's. MNW's feed has one here, and it is the
64 // last structural feature this fixture was missing.
65 more Rest::page(0, 10).of(feed.items.len()) {
66 for &page in feed.pages.iter() {
67 jumping Jump::new(page, Action::get("/feed?page={page}").navigating()) {
68 here when feed.here(page);
69 }
70 }
71 }
72 }
73 }
74
75 declare! {
76 /// The panel's members, in order, with nothing wrapping them.
77 ///
78 /// A heading that is only there when the feed is named, an empty state that
79 /// is only there when it has nothing, and a row per item. All three land in
80 /// the caller's region, so all three marks are numbered here and moved
81 /// there.
82 #[staged]
83 pub(crate) shape body(feed: &Feed) -> Vec<Node>;
84
85 text "{feed.heading}" unless feed.heading.is_empty();
86
87 empty "Nothing here yet." when feed.items.is_empty();
88
89 // A guarded include of a shape that loops, which is the feed's shape and
90 // the one that was never exercised.
91 include rows(feed) unless feed.items.is_empty();
92 }
93
94 declare! {
95 /// The region the panel's members are spliced into.
96 ///
97 /// Its own members either side of the splice, so a mark that failed to move
98 /// would land on one of these rather than quietly on nothing.
99 #[staged]
100 pub(crate) shape panel(feed: &Feed) -> Node;
101
102 region "FEED" as Pane {
103 text "Above.";
104 include each body(feed);
105 text "Below.";
106 }
107 }
108
109 #[cfg(test)]
110 mod tests {
111 use super::*;
112 use quasi_http::Serves as _;
113 use quasi_router::stage::{Op, Residual};
114 use quasi_webview::Webview;
115
116 fn feed(heading: &str, items: &[&str]) -> Feed {
117 Feed {
118 heading: heading.to_owned(),
119 items: items.iter().map(|item| (*item).to_owned()).collect(),
120 pages: vec![1, 2, 3],
121 }
122 }
123
124 fn residual() -> Residual {
125 quasi_webview::stage::derive(&Webview::new(), panel_staged)
126 }
127
128 /// The spliced shape's own structure survives the splice.
129 ///
130 /// Two branches and a loop, all three declared in `body` and all three
131 /// recorded against the region that took its members. A mark that did not
132 /// move would cover the caller's own text instead, which is what the
133 /// members either side of the splice are here to catch.
134 #[test]
135 fn a_spliced_shape_carries_its_marks_into_the_region() {
136 fn count(ops: &[Op], branches: &mut usize, loops: &mut usize) {
137 for op in ops {
138 match op {
139 Op::Branch(body) => {
140 *branches += 1;
141 count(body, branches, loops);
142 }
143 Op::Loop(body) => {
144 *loops += 1;
145 count(body, branches, loops);
146 }
147 Op::Arms(arms) => {
148 for arm in arms.iter() {
149 count(arm, branches, loops);
150 }
151 }
152 Op::Lit(_) | Op::Hole { .. } => {}
153 }
154 }
155 }
156
157 let residual = residual();
158 let (mut branches, mut loops) = (0, 0);
159 count(residual.ops(), &mut branches, &mut loops);
160 // Two guards in the spliced shape, one loop over the rows the shape it
161 // includes draws, and one over the pager's jumps.
162 assert_eq!(branches, 3, "{:#?}", residual.ops());
163 assert_eq!(loops, 2, "{:#?}", residual.ops());
164
165 // And the caller's own members are outside all of it.
166 let settled: String = residual
167 .ops()
168 .iter()
169 .filter_map(|op| match op {
170 Op::Lit(text) => Some(text.as_ref()),
171 _ => None,
172 })
173 .collect();
174 assert!(settled.contains("Above."), "{settled}");
175 assert!(settled.contains("Below."), "{settled}");
176 }
177
178 /// Filling the residual gives back what the renderer would have written.
179 ///
180 /// Across the cases the marks separate: named or not, empty or not, and
181 /// three lengths of the run.
182 #[test]
183 fn a_filled_panel_is_what_the_renderer_would_have_produced() {
184 let webview = Webview::new();
185 let residual = residual();
186 for heading in ["", "Today"] {
187 for items in [&[][..], &["one"][..], &["one", "two", "three"][..]] {
188 let feed = feed(heading, items);
189 assert_eq!(
190 webview.fragment(&panel(&feed)),
191 panel_serve(&residual, &feed),
192 "heading {heading:?}, {} items",
193 items.len()
194 );
195 }
196 }
197 }
198 }
199