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