| 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 |
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 |
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 |
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 |
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 |
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 |
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
|