Skip to main content

max / makeover-layout

9.5 KB · 225 lines History Blame Raw
1 use crate::Tone;
2
3 // Names this module's prose links to, resolved for rustdoc.
4 #[allow(unused_imports)]
5 use crate::{Figure, Meter};
6
7 /// What is in a region right now.
8 ///
9 /// The state, not the shimmer. Whether pending paints a skeleton, a spinner or
10 /// nothing at all is renderer policy, the same class of decision that got
11 /// `Fill::fallback` deleted from this crate. goingson and Balanced Breakfast
12 /// each grew a skeleton with differently-named parts; both keep them, as the
13 /// webview renderer's expression of [`Readiness::Pending`]. audiofiles has none
14 /// and needs none, because an immediate-mode renderer simply repaints.
15 ///
16 /// # Four states and not two
17 ///
18 /// Naming only `Ready` and `Pending` leaves a screen whose list came back empty
19 /// with nothing to say about it, so it renders an empty region or invents its
20 /// own placeholder text and neither says what it is. Left to the apps, the
21 /// class family drifts: `empty-state`, `empty-state--error`, `error-state` and
22 /// six more.
23 ///
24 /// The four are one axis because they are mutually exclusive: a region shows its
25 /// content, or a sign that it is coming, or a sign that there is none, or a sign
26 /// that it broke. Never two. That is the test for one enum against several
27 /// fields, and it is why this grew rather than a new member arriving beside it.
28 ///
29 /// # What is not here
30 ///
31 /// **The message.** "No projects yet" is content, and this names a state. It
32 /// lives with whatever holds the region — in quasi's case a `Slot` — alongside
33 /// the action that leads out of the emptiness, since an address is the one thing
34 /// this crate never names.
35 ///
36 /// **How much room it gets.** goingson's `--compact`, `--dashboard` and
37 /// `--padded` are the same state at three sizes, and a size is
38 /// `makeover-geometry`'s question. Naming them here would be this crate stating
39 /// values again.
40 ///
41 /// **The icon.** Presentation, and each host has its own answer or none.
42 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43 #[non_exhaustive]
44 pub enum Readiness {
45 /// The content is here.
46 Ready,
47 /// The content is on its way.
48 ///
49 /// For a region that changes *after* the first paint, and never for the
50 /// first paint itself: see "First paint is final paint" in the crate header.
51 /// A host that renders once, with its data already in hand, has nothing to
52 /// say this about, and a screen arriving in this state is describing a
53 /// moment its host should not have been in.
54 ///
55 /// What stands in occupies the geometry the content will occupy. A stand-in
56 /// sized to itself rather than to what replaces it is the reflow the rule
57 /// forbids, arriving one repaint later.
58 Pending,
59 /// The content arrived and there is none of it.
60 ///
61 /// Not a failure. An empty list is the normal state of a new install, and a
62 /// renderer that drew it in a danger tone would be reporting a fault where
63 /// there is none.
64 Empty,
65 /// The content did not arrive.
66 Failed,
67 }
68
69 impl Readiness {
70 /// Whether the region draws its own content, or something standing in for
71 /// it.
72 ///
73 /// The question every renderer asks first, so it is answered once here
74 /// rather than by a `matches!` in each. A state added later is a stand-in
75 /// until proven otherwise: falling back to drawing content that may not be
76 /// there is the worse of the two mistakes.
77 #[must_use]
78 pub const fn shows_content(self) -> bool {
79 matches!(self, Self::Ready)
80 }
81
82 /// What the state means, for a renderer choosing a colour.
83 ///
84 /// Derived rather than carried, which is the opposite of [`Meter`] and
85 /// [`Figure`], and the difference is worth stating: a proportion's meaning
86 /// depends on what is being counted and only the app knows it, while
87 /// "nothing here yet" and "this broke" mean the same thing in every app that
88 /// will ever have them.
89 #[must_use]
90 pub const fn tone(self) -> Tone {
91 match self {
92 Self::Failed => Tone::Danger,
93 _ => Tone::Neutral,
94 }
95 }
96 }
97
98 /// An action is waiting on something that resolves once, in expected finite
99 /// time.
100 ///
101 /// The control-side sibling of [`Readiness`]. That enum names four states for a
102 /// region and named nothing at all for the button that is currently doing what
103 /// it was clicked for, so the in-flight treatment is hand-written wherever it
104 /// exists: the MNW server carries 57 in-flight indicators against 2 guards
105 /// against a second press, which is the spinner mostly present and the guard
106 /// mostly absent, on a codebase whose money path is a purchase button.
107 ///
108 /// # What is described here, and what is not
109 ///
110 /// The fact is that there is an outstanding thing which will complete. Not that
111 /// the address is remote: a heavy local query waits too, and a server calling a
112 /// payment provider is not the browser leaving the app. Not that the call is
113 /// slow either, which is a judgement about a call rather than a property of one.
114 ///
115 /// Resolving **once** is the boundary, and it is what separates this from a
116 /// screen that keeps changing. A live screen never resolves and has no name in
117 /// this crate yet.
118 ///
119 /// # One mark, two renderings
120 ///
121 /// | what reads it | what it does |
122 /// |---|---|
123 /// | a control that was pressed | goes busy and refuses a second press until it resolves |
124 /// | a region fed by it | stands in as [`Readiness::Pending`], then fills |
125 ///
126 /// The two were on the table separately and both were taken. Controls alone
127 /// leaves a slow region hand-split into its own route, which is what MNW's user
128 /// dashboard does with its payout summary; regions alone leaves the purchase
129 /// button unguarded.
130 ///
131 /// # A quantity when it is measured, never a duration
132 ///
133 /// [`amount`](Self::amount) is stated only when it is a measured fact about the
134 /// payload. An upload's file length, yes; a round trip to a payment provider,
135 /// [`None`]. A duration is described nowhere, and a renderer may not manufacture
136 /// one from the amount either: a determinate bar shows what is done over what
137 /// there is, plus the time it has taken so far, and never a remaining time, an
138 /// arrival time or a rate extrapolated forwards. A prediction is wrong the
139 /// moment the transfer stalls, and being confidently wrong is worse than being
140 /// honestly indeterminate.
141 ///
142 /// This is why the crate refuses to say how long an undo stays offered and
143 /// accepts a byte count here. The refusal is about naming a decision that
144 /// belongs to the renderer; a file's length is not a decision, nobody chose it.
145 ///
146 /// # Not [`Meter`]
147 ///
148 /// [`Meter`] is how much of a set is done, and its own docs refuse the progress
149 /// of an operation on the grounds that a description is built once and dropped
150 /// while an operation runs between renders. That refusal stands. This names the
151 /// operation and its size, which is all that is known before it starts; how much
152 /// of it has gone through is the renderer's to observe live, and nothing round
153 /// trips through a description to say so.
154 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
155 #[non_exhaustive]
156 pub struct Awaiting {
157 /// Total work to get through, when it is a measured fact about the payload.
158 ///
159 /// `None` when the wait has no countable size, which is the common case and
160 /// the default.
161 ///
162 /// Unit-agnostic on purpose. Bytes for an upload, rows for an import; what
163 /// is being counted is the app's business and a renderer draws a proportion
164 /// either way.
165 pub amount: Option<u64>,
166 }
167
168 impl Awaiting {
169 /// A wait with no countable size.
170 #[must_use]
171 pub const fn unmeasured() -> Self {
172 Self { amount: None }
173 }
174
175 /// A wait whose size is known.
176 ///
177 /// Reach for it only with a measured figure. An estimate written in here is
178 /// a prediction wearing a fact's clothes, and the renderer has no way to
179 /// tell the two apart.
180 #[must_use]
181 pub const fn of(amount: u64) -> Self {
182 Self {
183 amount: Some(amount),
184 }
185 }
186
187 /// Whether there is a proportion to draw.
188 ///
189 /// The question every renderer asks first, answered once here rather than by
190 /// a `matches!` in each. False means indeterminate, which is the honest
191 /// drawing when nothing countable was measured.
192 #[must_use]
193 pub const fn is_determinate(self) -> bool {
194 self.amount.is_some()
195 }
196 }
197
198 /// When a picture is needed.
199 ///
200 /// A claim about *importance and position* rather than a fetch mechanism, which
201 /// is why it is the description's to make: only the app knows whether a picture
202 /// is the first thing on the screen or the fortieth thing down a list.
203 ///
204 /// # Eager is the default, and that is a correctness choice
205 ///
206 /// Emitting the webview's `loading="lazy"` for every picture reads one
207 /// consumer's habit as a rule. Deferring a picture that is on screen at first paint does not
208 /// save anything -- it is needed immediately either way -- and it delays the
209 /// arrival, so the space it eventually takes is claimed later and the shift is
210 /// more visible, not less.
211 ///
212 /// So the safe answer is the default and the optimisation is opted into. A
213 /// carousel is the case that proves the two cannot be one setting for the
214 /// renderer to choose: its first frame is on screen and its other frames are
215 /// not, in the same widget, at the same moment.
216 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
217 #[non_exhaustive]
218 pub enum Loading {
219 /// Needed with the screen. Fetch it now.
220 #[default]
221 Eager,
222 /// Not on screen yet. It can wait until it is near.
223 Lazy,
224 }
225