Skip to main content

max / quasi

Declare the carousel, and give the widget tier a read quasicoherent 7d21dbe2. The premise was that a widget is the easy case because its body is structure with no data in it, and that held: two declarations, no production, no new member, and the grammar a widget needs is a strict subset of the screen grammar. The two things a description cannot say became suppliers beside it rather than escapes inside it. `RegionKind::Widget` is a struct variant and an aggregate is the hard limit, so `region id as kind()` reads a supplier answering a `RegionKind`, which is one of the deferred leaves and sits outside the shape population by rule. The real computation was `if i > 0 { lazy }`: the frame on screen is fetched now and the rest can wait, which is the case that proves loading cannot be a renderer-wide setting. A `for` has no counter and adding one would be arithmetic in a description, so `Gallery` does the single pass that knows the order and every frame arrives carrying `defer`. `Frame::intrinsic` goes from `Option<(u32, u32)>` to `Option<Size>`, because `.0` is not a name a hole can say. Seventh time unnamed data has been given named fields on contact with a description. `Gallery` is split from the widget rather than folded into it: a set of pictures in order is worth having without the widget name around it, and `extend frames(gallery)` is what keeps them two shapes. The eleven markup tests pass unaltered, including the two that pin the lazy/eager split and the reserved space. One added at the read, for the gallery's one job. Also drops a doc link to `makeover_layout::Image`, a type that no longer exists.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-05 13:47 UTC
Signed with PGP, not checked
Commit: 8e71fe5d68f458a7e8186bfae861fc588086ac38
Parent: 8e593f9
4 files changed, +163 insertions, -67 deletions
M Cargo.lock +8 -7
@@ -3494,9 +3494,10 @@
3494 3494
3495 3495 [[package]]
3496 3496 name = "quasi-basics"
3497 - version = "0.101.1"
3497 + version = "0.101.2"
3498 3498 dependencies = [
3499 3499 "makeover-layout",
3500 + "quasi-declare",
3500 3501 "quasi-http",
3501 3502 "quasi-router",
3502 3503 "quasi-webview",
@@ -3524,7 +3525,7 @@
3524 3525
3525 3526 [[package]]
3526 3527 name = "quasi-declare"
3527 - version = "0.1.0"
3528 + version = "0.1.7"
3528 3529 dependencies = [
3529 3530 "proc-macro2",
3530 3531 "quote",
@@ -3563,7 +3564,7 @@
3563 3564
3564 3565 [[package]]
3565 3566 name = "quasi-router"
3566 - version = "0.101.13"
3567 + version = "0.101.14"
3567 3568 dependencies = [
3568 3569 "makeover-layout",
3569 3570 ]
@@ -6279,6 +6280,10 @@
6279 6280 "winnow 1.0.4",
6280 6281 ]
6281 6282
6283 + [[patch.unused]]
6284 + name = "quasi-type"
6285 + version = "0.1.3"
6286 +
6282 6287 [[patch.unused]]
6283 6288 name = "kberg"
6284 6289 version = "0.1.0"
@@ -6298,7 +6303,3 @@
6298 6303 [[patch.unused]]
6299 6304 name = "synckit-client"
6300 6305 version = "0.10.0"
6301 -
6302 - [[patch.unused]]
6303 - name = "quasi-type"
6304 - version = "0.1.3"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-basics"
3 - version = "0.101.1"
3 + version = "0.101.2"
4 4 description = "The first-party widget set: named assemblies of primitives, shared across our apps"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -14,6 +14,7 @@
14 14
15 15 [dependencies]
16 16 quasi-router = { path = "../quasi-router", version = "0.101.1" }
17 + quasi-declare = { path = "../quasi-declare", version = "0.1" }
17 18 makeover-layout = "0.44"
18 19
19 20 [dev-dependencies]
@@ -1,9 +1,10 @@
1 1 //! An ordered set of frames, said once.
2 2 //!
3 3 //! The widget tier's first consumer, and the gap that started look wave 2.
4 - //! [`makeover_layout::Region::Widget`] arrived at 0.20.0 to make this sayable
5 - //! and [`makeover_layout::Image`] at 0.21.0 because the first attempt found
6 - //! nothing named a picture.
4 + //! [`makeover_layout::Region::Widget`] is what makes an assembly sayable at
5 + //! all, and the picture it holds is [`quasi_router::Image`], which carries a
6 + //! source. That pairing is why the widget tier sits above the description
7 + //! suite rather than inside it; the crate docs carry the argument.
7 8 //!
8 9 //! Proved against three real MNW pages before it moved here, which is the
9 10 //! order Max asked for: a widget earns the shared crate by working somewhere
@@ -33,12 +34,37 @@
33 34 //! rest. Progressive enhancement in the direction that degrades to *more*
34 35 //! content instead of less.
35 36
36 - use makeover_layout::Fit;
37 - use quasi_router::{Image, Node, Slot};
37 + use quasi_declare::declare;
38 + use quasi_router::RegionKind;
38 39
39 40 /// What the recognising renderer keys on. Never interpreted by the description.
40 41 pub const NAME: &str = "carousel";
41 42
43 + /// The region kind, which carries the widget's name.
44 + ///
45 + /// A supplier because [`RegionKind::Widget`] is a struct variant and a
46 + /// declaration admits no aggregate. `RegionKind` is one of the deferred leaves,
47 + /// so a supplier answering one sits outside the shape population by rule rather
48 + /// than by exception.
49 + fn kind() -> RegionKind {
50 + RegionKind::Widget {
51 + name: NAME.to_string(),
52 + }
53 + }
54 +
55 + /// A picture's own dimensions.
56 + ///
57 + /// Two named fields rather than a pair, because a declaration reads a field by
58 + /// name and `.0` is not a name it can say. The rule is older than this widget:
59 + /// unnamed data gets named fields the first time a description has to read it.
60 + #[derive(Debug, Clone, Copy, PartialEq, Eq)]
61 + pub struct Size {
62 + /// Across.
63 + pub width: u32,
64 + /// Down.
65 + pub height: u32,
66 + }
67 +
42 68 /// One frame: a picture and what it says.
43 69 ///
44 70 /// Deliberately not an app's own frame type. Those carry what a template layer
@@ -58,7 +84,19 @@
58 84 /// it the frame occupies nothing until the bytes land and then takes its
59 85 /// full height at once, which measured as a 478px jump on MNW's landing
60 86 /// page.
61 - pub intrinsic: Option<(u32, u32)>,
87 + pub intrinsic: Option<Size>,
88 + /// Whether this picture may wait for the bytes.
89 + ///
90 + /// True of every frame but the one on screen, and the position that decides
91 + /// it is [`Gallery::new`]'s to know. It is carried here because a
92 + /// description reads a fact and counts nothing: a declaration has no index
93 + /// and wants none.
94 + ///
95 + /// Getting this backwards is what MNW's old partial did by lazily loading
96 + /// all three, including the one the visitor was already looking at. That
97 + /// delays the only picture that matters and buys nothing, because the other
98 + /// two are display:none and were never going to be fetched early anyway.
99 + pub defer: bool,
62 100 }
63 101
64 102 impl Frame {
@@ -69,13 +107,14 @@
69 107 alt: alt.into(),
70 108 caption: None,
71 109 intrinsic: None,
110 + defer: false,
72 111 }
73 112 }
74 113
75 114 /// The picture's own dimensions.
76 115 #[must_use]
77 116 pub const fn intrinsic(mut self, width: u32, height: u32) -> Self {
78 - self.intrinsic = Some((width, height));
117 + self.intrinsic = Some(Size { width, height });
79 118 self
80 119 }
81 120
@@ -87,59 +126,99 @@
87 126 }
88 127 }
89 128
90 - /// The frames as description nodes.
129 + /// An ordered set of frames, with the one on screen already decided.
91 130 ///
92 - /// Split out from [`carousel`] because a set of pictures in order is worth
93 - /// having on its own: a gallery that does not page is this without the widget
94 - /// name around it, and that is the second consumer this module expects.
95 - pub fn frames(frames: impl IntoIterator<Item = Frame>) -> impl Iterator<Item = Node> {
96 - frames.into_iter().enumerate().map(|(i, frame)| {
97 - // Natural, and it is the whole reason `Fit` has three members rather
98 - // than the one MNW uses at 15 of its 17 other sites. A screenshot
99 - // cropped to fill its box is a screenshot with its edges cut off, and
100 - // the edges of a screenshot of an interface are where the interface is.
101 - let mut picture = Image::new(frame.src, frame.alt).fit(Fit::Natural);
102 - if let Some((w, h)) = frame.intrinsic {
103 - picture = picture.intrinsic(w, h);
104 - }
105 - // The first frame is the one on screen, and the rest are not. Eager for
106 - // the one, lazy for the others -- which is the case that proves loading
107 - // cannot be a single setting the renderer picks: both answers are
108 - // correct, in one widget, at one moment.
109 - //
110 - // Getting this backwards is what MNW's old partial did by lazily
111 - // loading all three, including the one the visitor was already looking
112 - // at. That delays the only picture that matters and buys nothing,
113 - // because the other two are display:none and were never going to be
114 - // fetched early anyway.
115 - if i > 0 {
116 - picture = picture.lazy();
117 - }
118 - Node::Image(match frame.caption {
119 - Some(caption) => picture.caption(caption),
120 - None => picture,
121 - })
122 - })
131 + /// The read the two declarations below are written against. It exists because
132 + /// a description reads facts and derives none: which frame is on screen is a
133 + /// property of position, and a declaration has no index to ask for it. So the
134 + /// one pass that knows the order happens here, once, and every frame arrives
135 + /// carrying its own answer.
136 + ///
137 + /// Split from the widget rather than folded into it because a set of pictures
138 + /// in order is worth having on its own: a gallery that does not page is this
139 + /// without the widget name around it, and that is the second consumer this
140 + /// module expects.
141 + #[derive(Debug, Clone, PartialEq, Eq, Default)]
142 + pub struct Gallery {
143 + /// The frames, in order. The first is the one on screen.
144 + pub frames: Vec<Frame>,
123 145 }
124 146
125 - /// A carousel under an address.
126 - ///
127 - /// The id is the region's, which is how a fragment finds its way back to the
128 - /// right place, so it has to be unique on the page the way every slot id does.
129 - ///
130 - /// # The chrome is not here, and that is the whole of what this widget is
131 - ///
132 - /// A position, prev, next and a dot strip were listed as parts of this assembly
133 - /// when it was designed, and none of them is in the body. They are chrome, and
134 - /// chrome is what a recognising renderer draws its own way -- which is why they
135 - /// came out of here and went into the renderers, once, for every widget.
136 - ///
137 - /// What this says is `Showing::One`: an ordered set of pictures, one of them
138 - /// showing, and the reader can change which. Everything a host draws around
139 - /// that falls out of it, so a carousel needs no code in any renderer and a
140 - /// second assembly saying the same thing gets the same chrome for free.
141 - pub fn carousel(id: &str, items: impl IntoIterator<Item = Frame>) -> Slot {
142 - Slot::widget(id, NAME).extend(frames(items)).showing_one(0)
147 + impl Gallery {
148 + /// The frames in order, with everything behind the first marked to wait.
149 + ///
150 + /// Eager for the one on screen, lazy for the others, which is the case that
151 + /// proves loading cannot be a single setting the renderer picks: both
152 + /// answers are correct, in one widget, at one moment.
153 + pub fn new(frames: impl IntoIterator<Item = Frame>) -> Self {
154 + Self {
155 + frames: frames
156 + .into_iter()
157 + .enumerate()
158 + .map(|(i, frame)| Frame {
159 + defer: i > 0,
160 + ..frame
161 + })
162 + .collect(),
163 + }
164 + }
165 + }
166 +
167 + impl FromIterator<Frame> for Gallery {
168 + fn from_iter<I: IntoIterator<Item = Frame>>(frames: I) -> Self {
169 + Self::new(frames)
170 + }
171 + }
172 +
173 + declare! {
174 + /// The frames as description nodes.
175 + ///
176 + /// `Fit::Natural` is the whole reason `Fit` has three members rather than
177 + /// the one MNW uses at 15 of its 17 other sites. A screenshot cropped to
178 + /// fill its box is a screenshot with its edges cut off, and the edges of a
179 + /// screenshot of an interface are where the interface is.
180 + pub shape frames(gallery: &Gallery) -> Vec<Node>;
181 +
182 + for frame in gallery.frames.iter() {
183 + picture &frame.src &frame.alt {
184 + fit Natural;
185 + lazy when frame.defer;
186 + for size in frame.intrinsic.iter() {
187 + intrinsic size.width size.height;
188 + }
189 + for line in frame.caption.iter() {
190 + caption line;
191 + }
192 + }
193 + }
194 + }
195 +
196 + declare! {
197 + /// A carousel under an address.
198 + ///
199 + /// The id is the region's, which is how a fragment finds its way back to
200 + /// the right place, so it has to be unique on the page the way every slot
201 + /// id does.
202 + ///
203 + /// # The chrome is not here, and that is the whole of what this widget is
204 + ///
205 + /// A position, prev, next and a dot strip were listed as parts of this
206 + /// assembly when it was designed, and none of them is in the body. They are
207 + /// chrome, and chrome is what a recognising renderer draws its own way --
208 + /// which is why they came out of here and went into the renderers, once,
209 + /// for every widget.
210 + ///
211 + /// What this says is `Showing::One`: an ordered set of pictures, one of
212 + /// them showing, and the reader can change which. Everything a host draws
213 + /// around that falls out of it, so a carousel needs no code in any renderer
214 + /// and a second assembly saying the same thing gets the same chrome for
215 + /// free.
216 + pub shape carousel(id: &str, gallery: &Gallery) -> Slot;
217 +
218 + region id as kind() {
219 + showing_one 0;
220 + extend frames(gallery);
221 + }
143 222 }
144 223
145 224 #[cfg(test)]
@@ -148,7 +227,8 @@
148 227 use quasi_http::Serves as _;
149 228
150 229 fn render(id: &str, items: Vec<Frame>) -> String {
151 - quasi_webview::Webview::new().fragment(&Node::Region(carousel(id, items)))
230 + let gallery = Gallery::new(items);
231 + quasi_webview::Webview::new().fragment(&quasi_router::Node::Region(carousel(id, &gallery)))
152 232 }
153 233
154 234 fn three() -> Vec<Frame> {
@@ -263,6 +343,20 @@
263 343 assert!(!html.contains("<img"), "{html}");
264 344 }
265 345
346 + #[test]
347 + fn the_gallery_decides_which_frame_waits_and_the_description_only_reads_it() {
348 + // The one thing the read is for. A declaration has no index, so the
349 + // pass that knows the order happens here and every frame arrives
350 + // carrying its own answer.
351 + let gallery = Gallery::new(three());
352 + let waiting: Vec<bool> = gallery.frames.iter().map(|frame| frame.defer).collect();
353 + assert_eq!(waiting, vec![false, true, true]);
354 +
355 + // And an empty set decides nothing, rather than reaching for a first
356 + // frame that is not there.
357 + assert!(Gallery::new(Vec::new()).frames.is_empty());
358 + }
359 +
266 360 #[test]
267 361 fn the_chrome_is_derived_and_no_renderer_was_told_what_a_carousel_is() {
268 362 // What this widget stopped carrying. The row is not in the body and not
@@ -46,4 +46,4 @@
46 46
47 47 pub mod carousel;
48 48
49 - pub use carousel::{Frame, carousel};
49 + pub use carousel::{Frame, Gallery, Size, carousel};