makeover-layout 0.47.0

The renderer-agnostic half of the make-family design system: what a thing IS, named as intents and relationships and never as values. Colour defers to makeover, spacing to makeover-geometry; what is left is composition.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
use crate::Tone;

// Names this module's prose links to, resolved for rustdoc.
#[allow(unused_imports)]
use crate::{Choice, Notice, Readiness};

/// How much of a set is done.
///
/// Nine sites across the two webview apps drew a bar and nothing here named
/// one, so every described screen concatenated the two numbers into its
/// heading text instead: "Subtasks 3/7", "Time Tracking 45m tracked / 30m est,
/// over". Every fact survives that and the reading does not, which is the same
/// loss `RowPart::Tokens` closed when a toned status badge became prose.
///
/// # Why a pair and not a percentage
///
/// Both numbers, not the percentage the apps compute from them. The percentage
/// was the obvious shape and it had already been tried: goingson's
/// `Task::time_progress` divides, rounds, and then clamps to 100, which throws
/// away the one case the bar exists to show — 45 minutes tracked against a
/// 30-minute estimate. It carries a separate `is_over_estimate` boolean beside
/// it to recover the fact the clamp dropped. A pair keeps the over-run without a
/// companion flag, and [`percent`](Meter::percent) is still one call away for a
/// renderer that wants it.
///
/// The pair is also what the apps already have at every site. All seven
/// determinate bars write the ratio into the accessible layer and never the
/// percentage: `title="3/7 subtasks"`, `aria-label="3 of 7 subtasks completed"`,
/// a milestone's own `3/7` span. Given 43 nothing can recover "3 of 7", so a
/// percentage member would have made [`label`](Meter::label) mandatory at every
/// call site, which is the concatenated text this member removes, moved one
/// layer down.
///
/// # What this is not
///
/// The progress of an *operation*. Two of the nine sites are that — goingson's
/// focus timer, Balanced Breakfast's feed fetch — and they get nothing here, on
/// purpose. Both are imperative controllers over a live handle, driven by a tick
/// or an event stream, and a description is built once and dropped. Holding one
/// would mean growing a way to update a description between renders, which is a
/// different feature. [`Readiness::Pending`] and a [`Notice::Toast`] carry the
/// honest part.
///
/// The two cases are distinguishable in the markup rather than by taste: every
/// determinate bar in both apps carries a tone, and neither operation bar
/// carries one. Two codebases drew that line the same way without coordinating.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Meter<'a> {
    /// How much is done. May exceed [`total`](Self::total), and that is the
    /// case worth drawing.
    pub done: u32,
    /// How much there is to do. Zero means there is no set, not that the set is
    /// complete.
    pub total: u32,
    /// What the proportion means right now.
    ///
    /// Carried rather than derived, because no renderer can work it out. The
    /// same 90% is [`Tone::Success`] on a subtask rollup and [`Tone::Danger`] on
    /// a time estimate, and goingson picks between them from `is_over_estimate`,
    /// a fact about the data and not about the number.
    pub tone: Tone,
    /// What is being counted, if the bar says so: "subtasks", "tasks".
    ///
    /// The noun, not the ratio. A renderer builds "3 of 7 subtasks" from this
    /// and the two numbers; handing it the assembled string would put the
    /// sentence order in the description, where a terminal at one line and a
    /// tooltip want different ones.
    pub label: Option<&'a str>,
}

impl<'a> Meter<'a> {
    /// A proportion with no tone and no label.
    #[must_use]
    pub const fn new(done: u32, total: u32) -> Self {
        Self {
            done,
            total,
            tone: Tone::Neutral,
            label: None,
        }
    }

    /// What the proportion means.
    #[must_use]
    pub const fn tone(mut self, tone: Tone) -> Self {
        self.tone = tone;
        self
    }

    /// What is being counted.
    #[must_use]
    pub const fn label(mut self, label: &'a str) -> Self {
        self.label = Some(label);
        self
    }

    /// How full the bar is, 0 to 100, clamped.
    ///
    /// For drawing, which is the only thing a clamped number is good for. Ask
    /// [`overflowing`](Self::overflowing) before reporting it as a fact, or this
    /// is `time_progress`'s bug again with the clamp moved.
    ///
    /// An empty set reads as 0. Nothing is done, because there is nothing to do
    /// and no bar to fill; the apps guard on the count before drawing at all.
    #[must_use]
    pub const fn percent(&self) -> u8 {
        if self.total == 0 {
            return 0;
        }
        let scaled = (self.done as u64 * 100) / self.total as u64;
        if scaled > 100 { 100 } else { scaled as u8 }
    }

    /// Whether more is done than there was to do.
    ///
    /// The fact [`percent`](Self::percent) destroys, kept reachable so a
    /// renderer can mark the over-run rather than drawing a full bar and
    /// implying it landed exactly.
    #[must_use]
    pub const fn overflowing(&self) -> bool {
        self.done > self.total
    }

    /// Whether there is a set at all.
    ///
    /// A meter over nothing is sayable on purpose, for the same reason a field
    /// with no options is: it is what an app with an unloaded count actually
    /// has, and a renderer that shows an empty bar says so on screen rather than
    /// dividing by zero.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.total == 0
    }
}

/// One figure with a caption: a number and what it counts.
///
/// The dashboard shape. A large value over a small caption, several of them in
/// a strip: a current streak, a completion rate, a total. Four put the value
/// above the caption and one inverts it, which is drift inside the shape
/// rather than a second shape.
///
/// # Why the value is text
///
/// "17", "84%", "12/30", "3d". A figure is whatever the app computed, already
/// formatted, and the formatting is the app's because only it knows whether the
/// number is a percentage, a duration or a ratio. This carries none of the
/// arithmetic [`Meter`] carries, and that is the difference between them: a
/// meter is a proportion a renderer draws, and a figure is a fact a renderer
/// sets in type.
///
/// # Tone is carried, for [`Meter`]'s reason
///
/// Three of the five sites tone the figure by their own means — `red`/`blue` on
/// the weekly review, a `${type}` class on the monthly one, `sync-stat-warn` on
/// sync. So tone is carried at every site that needs it and derived at none, and
/// no renderer can work out that a streak of zero is worth colouring.
///
/// # What is not here
///
/// Whether the figure answers a click. One of the five is a control — sync's
/// "Not Applied: 3" opens the list — and an action is not something this crate
/// can name: nothing here knows what a route is. That belongs beside the figure
/// in whatever layer holds the actions, the same way a row's activation sits
/// beside its parts rather than inside them.
///
/// The arrangement is not here either. Several figures in a strip is a set, and
/// a renderer given them one at a time cannot tell it is looking at one; the
/// layer that holds the tree is where the set gets said.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Figure<'a> {
    /// The number, formatted the way the app means it to read.
    pub value: &'a str,
    /// What it counts. The caption under the value.
    pub caption: &'a str,
    /// How the value has moved, if the app is tracking that.
    ///
    /// Text, for [`value`](Self::value)'s reason: only the app knows whether a
    /// move reads as `+12.5%`, `+3` or `2x`, and a renderer handed a number
    /// would have to guess.
    ///
    /// This is what [`tone`](Self::tone) was for and had no consumer of. The MNW
    /// server has four screens whose stat card is a label, a value and a delta,
    /// and the delta is the toned part: the figure itself is an ordinary fact
    /// and it is the movement that reads as good or bad. Without this the delta
    /// has to be folded into the caption, which loses the tone and reads as a
    /// longer caption rather than as a second, smaller line.
    pub change: Option<&'a str>,
    /// What the figure means right now. [`Tone::Neutral`] is an ordinary fact.
    ///
    /// Applies to [`change`](Self::change) where there is one, since that is the
    /// part that carries the judgement, and to the value where there is not.
    pub tone: Tone,
}

impl<'a> Figure<'a> {
    /// A figure that is an ordinary fact.
    #[must_use]
    pub const fn new(value: &'a str, caption: &'a str) -> Self {
        Self {
            value,
            caption,
            change: None,
            tone: Tone::Neutral,
        }
    }

    /// How the value has moved.
    #[must_use]
    pub const fn change(mut self, change: &'a str) -> Self {
        self.change = Some(change);
        self
    }

    /// What the figure means.
    #[must_use]
    pub const fn tone(mut self, tone: Tone) -> Self {
        self.tone = tone;
        self
    }
}

/// Something the user can do, and what it costs to say so.
///
/// Beside [`Meter`] and [`Figure`] for the reason those are here: a renderer
/// that is handed the parts has to decide how to say them, and a renderer that
/// is handed a finished string has already had the decision made for it.
///
/// No address. Where a control goes is the app's business and every host
/// follows it differently — an `hx-get`, a protocol URL, a function call — so
/// the description says what the control *is* and the caller keeps what it
/// does. That is the same split [`Choice`] makes.
///
/// No confirmation flag either, and that one is a finding rather than an
/// omission: a question asked *after* a control is pressed belongs to whatever
/// is holding the interaction, and a renderer that drew it would be asking
/// before there was anything to answer.
/// How a picture sits in the box it is given.
///
/// An intent rather than a value, so a renderer picks the expression it has:
/// `object-fit` in a webview, a texture's UV rect in egui, and in a terminal a
/// choice about how many cells the blit gets. Named because MNW already makes
/// the distinction deliberately at 17 sites and makes it three different ways,
/// which is a policy the app decided rather than one a shared crate would be
/// picking by accident.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Fit {
    /// The picture's own proportions, and the box takes the height they imply.
    ///
    /// The default because it is the only one that shows the whole picture at
    /// its own shape, so a renderer that ignores this enum entirely is still
    /// right about the common case. A screenshot wants this; the shipped MNW
    /// carousel sets no `object-fit` at all, which is this.
    #[default]
    Natural,
    /// Fill the box and crop whatever does not fit.
    ///
    /// For a picture in a slot whose shape the layout fixed: a thumbnail, an
    /// avatar, cover art. 15 of MNW's 17 sites.
    Cover,
    /// Fit inside the box whole, leaving space on two sides.
    ///
    /// The letterbox. For when the whole picture matters more than filling the
    /// space, and the space is not the picture's shape.
    Contain,
}

/// A picture's own pixel dimensions.
///
/// Deliberately not [`makeover_geometry`]'s business. Geometry answers *how
/// much space a thing should get*, which is a scale question with the same
/// answer on every screen. This is the intrinsic size of one asset, which is a
/// fact about that asset and varies per picture.
///
/// [`makeover_geometry`]: https://docs.rs/makeover-geometry
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Extent {
    /// Width in the picture's own pixels.
    pub width: u32,
    /// Height in the picture's own pixels.
    pub height: u32,
}

impl Extent {
    /// A picture's dimensions.
    #[must_use]
    pub const fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }

    /// Width over height, or `None` if either side is zero.
    ///
    /// The form a renderer actually reserves space with: a box that knows its
    /// proportion holds the right height at any width, which is what a
    /// responsive picture needs and what a fixed pixel height cannot give.
    #[must_use]
    pub fn ratio(self) -> Option<f32> {
        (self.width > 0 && self.height > 0).then(|| self.width as f32 / self.height as f32)
    }
}

/// A run of magnitudes read against one axis.
///
/// [`Meter`] is one proportion; this is a series of them that share a maximum,
/// and the shared maximum is the whole difference. A run of meters draws each
/// bar against its own `total`, so a chart said that way states the axis once
/// per bar and nothing holds the copies together. Here the axis is stated once
/// and a bar carries only where it sits on it.
///
/// # Why the axis and not a percentage per bar
///
/// [`Meter`]'s reason, one layer out. The app that drew MNW's revenue chart
/// computed `revenue / most * 100.0` and put the percentage in the markup, and
/// what reached the reader was a width with no numbers behind it: a bar at 100%
/// because it is the largest and a bar at 100% because the axis is wrong are the
/// same width and are not the same fact. Carrying both integers keeps the fact,
/// and [`Bar::fraction`] is one call away for a renderer that wants the ratio.
///
/// It is also the only shape that survives a compiled template. A residual holds
/// numbers the description HANDS a renderer, never ones a renderer works out
/// from two of them, so a chart drawn from a supplied percentage could be
/// described and could not be compiled. See `quasi_router::stage::number_at`.
///
/// # What is worded here and what is not
///
/// [`Bar::at`] is where the bar sits on the axis and [`label`](Self::label) is
/// what the magnitudes are, which is [`Meter::label`]'s split exactly. What
/// differs is [`Bar::reading`] and [`Bar::note`]: both arrive already worded,
/// because a magnitude's own units are the app's ("$42.10", not 4210) and a
/// count's noun inflects ("1 sale", "3 sales"). A renderer that pluralised
/// would be growing a lexer for one language.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Chart<'a> {
    /// The magnitude the axis runs to. Every bar is read against this.
    ///
    /// Zero means there is no axis, not that every bar is full. A renderer draws
    /// nothing rather than dividing by it; see [`is_empty`](Self::is_empty).
    ///
    /// `usize` because that is what a description counts in -- a pager's offset
    /// and page size are the same -- and because it is the only width
    /// `quasi_router::stage::number_at` has a stand-in for, which is what lets a
    /// chart reach a compiled template at all.
    pub most: usize,
    /// What the magnitudes are: "revenue", "plays".
    ///
    /// The noun, not the unit and not the ratio. The unit is already in each
    /// [`Bar::reading`], where it belongs, because only the app knows it.
    pub label: Option<&'a str>,
    /// What the axis means, where it means anything.
    pub tone: Tone,
}

impl<'a> Chart<'a> {
    /// An axis running to `most`, untoned and unlabelled.
    #[must_use]
    pub const fn new(most: usize) -> Self {
        Self {
            most,
            label: None,
            tone: Tone::Neutral,
        }
    }

    /// What the magnitudes are.
    #[must_use]
    pub const fn label(mut self, label: &'a str) -> Self {
        self.label = Some(label);
        self
    }

    /// What the axis means.
    #[must_use]
    pub const fn tone(mut self, tone: Tone) -> Self {
        self.tone = tone;
        self
    }

    /// Whether there is an axis to read against.
    ///
    /// [`Meter::is_empty`]'s case: an axis running to zero is what an app with
    /// nothing to chart actually has, and saying so beats dividing by it.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.most == 0
    }
}

/// One magnitude in a [`Chart`], at its place on the axis.
///
/// Carries no axis of its own on purpose: a bar read against a maximum it
/// states itself is a meter, and a run of those is not a chart.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Bar<'a> {
    /// Where on the axis this sits: "Mar 3", "Week 12".
    ///
    /// The position's own name rather than an index, for the reason a pager's
    /// jump carries its page number: what a chart shows is a window over a
    /// series, and an index into that window is not the point it names.
    pub at: &'a str,
    /// The magnitude, in the chart's units, read against [`Chart::most`].
    pub value: usize,
    /// The magnitude as the app words it: "$42.10".
    ///
    /// Worded rather than derived because the units are the app's. A renderer
    /// handed 4210 cannot know it is money, let alone which money.
    pub reading: Option<&'a str>,
    /// A second fact about this bar, already worded: "3 sales".
    ///
    /// Worded for the reason [`reading`](Self::reading) is, plus one of its own:
    /// a count's noun inflects with the count, and that is language rather than
    /// drawing.
    pub note: Option<&'a str>,
}

impl<'a> Bar<'a> {
    /// A place on the axis, with no magnitude on it yet.
    ///
    /// The magnitude arrives through [`of`](Self::of) rather than as a second
    /// argument, and that is not stylistic: `quasi-declare` stages a
    /// constructor's plain arguments all one way or all the other, so a
    /// constructor taking a place AND a magnitude would have the place standing
    /// in as a number. Split, the place is a value and `of` is a count, which is
    /// the same split a pager's `of` makes and the reason it is spelled the
    /// same.
    #[must_use]
    pub const fn at(at: &'a str) -> Self {
        Self {
            at,
            value: 0,
            reading: None,
            note: None,
        }
    }

    /// How far up the axis this bar reaches.
    #[must_use]
    pub const fn of(mut self, value: usize) -> Self {
        self.value = value;
        self
    }

    /// How the app words this magnitude.
    #[must_use]
    pub const fn reading(mut self, reading: &'a str) -> Self {
        self.reading = Some(reading);
        self
    }

    /// A second fact about the bar, already worded.
    #[must_use]
    pub const fn note(mut self, note: &'a str) -> Self {
        self.note = Some(note);
        self
    }

    /// How far up the axis this bar reaches, 0.0 to 1.0, clamped.
    ///
    /// For drawing, which is what a clamped number is good for, and for the two
    /// renderers that draw in cells and pixels rather than in CSS. An empty axis
    /// reads as 0.0 rather than dividing by zero.
    ///
    /// A bar over [`Chart::most`] clamps, and unlike [`Meter`] that is not a
    /// fact being lost: `most` is the maximum of the bars, so a bar above it is
    /// an axis the app got wrong rather than an over-run worth drawing.
    #[must_use]
    pub fn fraction(&self, chart: &Chart<'_>) -> f32 {
        if chart.most == 0 {
            return 0.0;
        }
        (self.value as f64 / chart.most as f64).min(1.0) as f32
    }
}