makeover-layout 0.44.2

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
474
475
476
477
478
479
480
// Names this module's prose links to, resolved for rustdoc.
#[allow(unused_imports)]
use crate::{Field, FieldKind, Unit};

/// One option offered by a field [`FieldKind::offers_options`] accepts.
///
/// Two strings, because the submitted value and the read label are different
/// facts and every renderer that has tried to collapse them has had to
/// un-collapse them later. `makeover-webview` invented this shape writing its
/// form emitter and it is taken here unchanged; moving it down rather than
/// re-deriving it is the point, since the second and third renderers were each
/// going to arrive at a near-miss of it.
/// `#[non_exhaustive]`, which every type here that a renderer matches or builds
/// carries. Without it a new member is a breaking change at every literal site
/// in the tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Choice<'a> {
    /// What is submitted.
    pub value: &'a str,
    /// What is read.
    pub label: &'a str,
    /// Why it cannot be picked right now, when it cannot.
    ///
    /// One member rather than an `available: bool` beside a reason, and the
    /// conflation is the point: an option greyed out with no explanation is a
    /// dead end the user cannot act on, and it is exactly the state the app
    /// that found this gap had to patch by hand with a line of prose under the
    /// control. Making the reason mandatory means the description cannot say
    /// the useless half.
    ///
    /// The option stays in the list. Dropping it is what an app does today, and
    /// it costs the user the knowledge that the thing exists at all —
    /// audiofiles' multi-sample mode appears on its own once a second sample is
    /// dropped, so a user who never sees it never learns what to drop.
    ///
    /// **Not [`Field::error`], and not [`Field::hint`].** An error is about the
    /// answer and a hint is standing help for the whole question; this is about
    /// one option among several, which is the level neither of those reaches.
    ///
    /// **Not disabled-the-state.** `State::Disabled` is about a whole field
    /// refusing to answer. This says the field is live and one of its answers
    /// is not available yet, which is a different sentence and the reason the
    /// tone rule matters here: the *other* options are still usable.
    pub unavailable: Option<&'a str>,
    /// The line under the label that says what picking this means.
    ///
    /// A choice between three plans is a choice nobody can make from three
    /// names, and until this existed the description had nowhere to put the
    /// sentence that made it makeable. What the corpus did instead is the
    /// tell: four of the six measured sites fold it into the label —
    /// `<strong>Public</strong>: Anyone can see this repository` in MNW's git
    /// settings, the same shape in its project-basics AI tier and its cart's
    /// currency conversion, and `Mislabeled (wrong AI tier or category)` in
    /// its report modal. The described screens do it too, in miniature: `Every
    /// 15 minutes (recommended)`, `Reference samples in place (loose-files
    /// mode)`. One fact, six spellings, no member.
    ///
    /// # Where it goes is the host's, and the rule already exists
    ///
    /// This is [`unavailable`](Self::unavailable)'s question met a third time
    /// and it takes the same answer, which is the strongest evidence one member
    /// is right rather than two. A radio group has room and gives the line its
    /// own element beside the label. A `<select>`'s option takes no elements,
    /// no second line and no title a keyboard reaches, so the line runs into
    /// the option's own text — exactly as a precondition does, and as a theme's
    /// contrast badge does in brackets. A terminal has rows and puts it on one
    /// under the option.
    ///
    /// # Not a price, and that is a measurement rather than a preference
    ///
    /// The site that asked for this is MNW's fee calculator, whose tier cards
    /// carry a name, a price *and* a description, so a second member for the
    /// price was on the table. It loses on the count: the tree's other three
    /// priced tier lists — `project.html`, `project_paywall.html`,
    /// `index.html` — are not option lists at all. Each card carries its own
    /// submit, which makes it a region with a heading, a fact and an act, and
    /// it is sayable already. So a price member would have exactly one
    /// consumer, and it would mean this crate growing a money type it does not
    /// have: [`Unit`] is a time axis, and every amount in the described tree is
    /// text.
    ///
    /// The price therefore leads the line: `$24/mo. 2GB/file, 100GB total.
    /// Fits audio, plugins, binaries.` What would reopen it is a **second**
    /// priced option list, not a judgement about how that reads.
    ///
    /// # What it is not
    ///
    /// Not [`unavailable`](Self::unavailable), which says the option cannot be
    /// picked. This says what it means to pick it, and the two are drawn
    /// together on an option that carries both: the description that says a
    /// tier is out of stock *and* what the tier is has said two things.
    ///
    /// Not [`Field::hint`], which is standing help for the whole question, and
    /// not markup. One line of plain text, for [`Candidate::detail`]'s reason:
    /// an option list is a place a renderer lays out, and a description that
    /// put a block in one would be handing every host a layout problem for the
    /// benefit of one.
    pub detail: Option<&'a str>,
    /// Whether this is the option currently chosen.
    ///
    /// The alternative, and what every renderer here did before this member
    /// existed, is to compare the field's current value against each option's
    /// own. That reads the same and is not the same: it states which option is
    /// marked ONCE, at the field, and leaves each option to work out whether
    /// the sentence is about it. A description whose data already knows per row
    /// -- a theme list where each theme carries `selected` -- then has to
    /// collapse that to one string for the renderer to re-derive, which is one
    /// fact stated twice.
    ///
    /// # Never both
    ///
    /// An option list either marks itself here or is matched against the
    /// field's value, and a description that does both has said one thing two
    /// ways, which is how the two drift. quasi-declare refuses the pair at
    /// compile time. This crate cannot: it is handed a list and a value with no
    /// record of which spelling built them, so a renderer marks an option whose
    /// `chosen` is set OR whose value matches, and a caller that sets both gets
    /// both marked.
    ///
    /// # What it buys, beyond saying it once
    ///
    /// It is the only form a compiled template can carry. A residual holds one
    /// body per loop, so "exactly one row differs" cannot be a property of the
    /// row body when the difference is decided by a comparison the body does
    /// not make. Said here it is a branch inside the row, which is a shape a
    /// residual has.
    pub chosen: bool,
}

impl<'a> Choice<'a> {
    /// An option whose submitted value is also its label.
    #[must_use]
    pub const fn plain(value: &'a str) -> Self {
        Self::new(value, value)
    }

    /// An option that submits one string and reads as another.
    ///
    /// A constructor rather than a literal, which is what `#[non_exhaustive]`
    /// costs and buys: outside this crate the struct cannot be built by naming
    /// its members, so every call site goes through here and the next member
    /// added breaks none of them.
    #[must_use]
    pub const fn new(value: &'a str, label: &'a str) -> Self {
        Self {
            value,
            label,
            unavailable: None,
            detail: None,
            chosen: false,
        }
    }

    /// The same option, not pickable yet, and why.
    ///
    /// Builder-shaped because the reason is the rare case: 39 of the 40 option
    /// sites measured across the tree do not have one.
    #[must_use]
    pub const fn unless(mut self, reason: &'a str) -> Self {
        self.unavailable = Some(reason);
        self
    }

    /// The same option, with the line that says what picking it means.
    ///
    /// Builder-shaped for [`unless`](Self::unless)'s reason, and it is the
    /// commoner of the two: six measured sites want this and one wants a
    /// precondition. See [`detail`](Self::detail).
    #[must_use]
    pub const fn detailing(mut self, detail: &'a str) -> Self {
        self.detail = Some(detail);
        self
    }

    /// The same option, marked as the one currently chosen.
    ///
    /// See [`chosen`](Self::chosen). Builder-shaped like the other two, and for
    /// the same reason: the marked option is one row of a list where every
    /// other row is not.
    #[must_use]
    pub const fn chosen(mut self) -> Self {
        self.chosen = true;
        self
    }

    /// Whether the option can be picked right now.
    ///
    /// The predicate a renderer branches on, so that "unavailable" is read as
    /// one condition in one place rather than as `unavailable.is_some()` at
    /// three renderers, one of which will invert it.
    #[must_use]
    pub const fn available(&self) -> bool {
        self.unavailable.is_none()
    }
}

/// One entry in a field's suggestion list.
///
/// A suggestion-only type rather than a fourth member on [`Choice`], ruled by
/// Max. The two are near-identical and that is the accepted drift risk, so the
/// mitigation is written here: **an
/// option and a candidate are submitted the same way and read differently.**
/// An option is a thing you pick from a known set, and the set is the whole of
/// what there is. A candidate is a thing you are being *oriented* toward out of
/// a set nobody can see, which is why it carries [`detail`](Self::detail) and
/// an option does not.
///
/// This reverses a position quasi-router stated in its own doc, that a
/// candidate is [`Choice`] "because a candidate is submitted under one string
/// and read under another, which is what an option is". True and not
/// sufficient: how a thing is submitted was never the half that differed.
///
/// # Why the second string is not folded into the label
///
/// Because every renderer wants it separately, and the two measured sites both
/// draw it by hand today. The MNW server's tag box computes its context as the
/// parent path -- "the parent path orients an otherwise ambiguous leaf:
/// 'Format' appears under audio, software, writing, and video" -- and a list of
/// four identical rows reading "Format" is not a usable list. In a webview the
/// second string is styled differently, in a terminal it wants the remaining
/// columns rather than a dash, and in neither is it part of what the typed
/// value matches against. `Choice::new(slug, format!("{label} - {context}"))`
/// loses all three of those facts, which is the condition this type exists to
/// end.
///
/// # No `unavailable`
///
/// [`Choice::unavailable`] has no counterpart here, and the omission is the
/// implementer's call recorded rather than an oversight. A suggestion that
/// cannot be picked is arguably not a suggestion: an option list is a fixed set
/// a user is owed an explanation about, and a candidate list is whatever a
/// route decided to offer, so a route with nothing to say simply does not offer
/// the row. Add it if a measured site ever wants it.
///
/// # What it does not carry, and where that lives
///
/// What *happens* when a candidate is picked. Picking is local by default -- it
/// writes [`value`](Self::value) into the field that owns the list -- and a
/// candidate that does something else says so with an action. An action is not
/// a word this crate has, exactly as [`Field`] here has no `suggests` member,
/// so both live on the router's owned mirror of this type.
///
/// `#[non_exhaustive]` from birth. Non-negotiable: adding it later means a
/// breaking change at every literal site in the tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Candidate<'a> {
    /// What is submitted, and what picking writes into the field.
    pub value: &'a str,
    /// What is read.
    pub label: &'a str,
    /// The second line: what orients this candidate among rows that read alike.
    ///
    /// Optional because a candidate list whose labels are already distinct
    /// wants nothing here, and a renderer given [`None`] draws one line rather
    /// than an empty second one.
    pub detail: Option<&'a str>,
}

impl<'a> Candidate<'a> {
    /// A candidate whose submitted value is also its label.
    #[must_use]
    pub const fn plain(value: &'a str) -> Self {
        Self::new(value, value)
    }

    /// A candidate that submits one string and reads as another.
    ///
    /// A constructor rather than a literal, which is what `#[non_exhaustive]`
    /// costs and buys: outside this crate the struct cannot be built by naming
    /// its members, so every call site goes through here and the next member
    /// added breaks none of them.
    #[must_use]
    pub const fn new(value: &'a str, label: &'a str) -> Self {
        Self {
            value,
            label,
            detail: None,
        }
    }

    /// The same candidate, with the line that tells it from its neighbours.
    #[must_use]
    pub const fn detailed(mut self, detail: &'a str) -> Self {
        self.detail = Some(detail);
        self
    }
}

/// One field of a form.
///
/// Borrowed rather than owned: a description is built, read once by a renderer,
/// and dropped. Nothing here outlives the screen it describes.
///
/// # What it carries, and what it does not
///
/// Stated here so the next renderer does not re-ask, which is what the first
/// two both did. It carries everything a renderer needs to *draw* the field:
/// its kind, what it is called, what it is asked for, its standing help, what
/// is wrong with it now, whether it is compulsory, whether it hides behind a
/// disclosure, its ghost text, and the options it offers.
///
/// It does not carry the **current value**, and it is not going to. That is the
/// one thing here that is genuinely renderer state: a webview reads it back out
/// of the DOM, an immediate-mode renderer holds a `&mut` to the app's own field
/// and writes through it, and a terminal keeps an edit buffer. A description
/// that carried the value would have to carry a way to write it back, at which
/// point it is a form model and no longer a description.
///
/// **Constraints** are here and enforcement is not, which is one line rather
/// than two. [`required`], [`max_length`], [`min`] and [`max`] are facts about
/// the *question*, so a renderer can emit its host's idiom for each — an HTML
/// attribute, a marked label, a clamped spinner — and the platform helps the
/// user before anything is submitted. Deciding that a value is wrong stays with
/// whoever validated, and [`error`] is that decision arriving back.
///
/// The set stops before `pattern`, and stops there on both tests at once. A
/// regex has an honest answer in a webview and none anywhere else: egui would
/// have to run it per keystroke and decide what a half-typed value means,
/// which is enforcement wearing description's clothes. And it is one site in
/// goingson and none in Balanced Breakfast, against 8 and 1 for `maxlength`.
///
/// [`error`]: Field::error
/// [`required`]: Field::required
/// [`max_length`]: Field::max_length
/// [`min`]: Field::min
/// [`max`]: Field::max
/// How a slider's position becomes its value, and how finely it moves.
///
/// **The data of a slider is a fraction and a function taking numbers to
/// numbers.** Stated by Max, and it is what [`min`](Field::min) and
/// [`max`](Field::max) are not: they were never the control's extent.
/// A slider's extent is always 0 to 1 — a thumb at 40% of a track — and the
/// bounds are `f(0)` and `f(1)`. Linear is the constant-slope case, which is
/// exactly why nobody noticed the function was there: when `f` is
/// `min + t * (max - min)` the extent and the bounds coincide numerically and
/// the mapping is invisible.
///
/// So this is not a scale flag bolted onto a range. Every range described
/// before it had a mapping, and four renderers each hard-coded the same one.
///
/// # Why a closed family and not a function
///
/// `fn(f64) -> f64` is the literal reading and it does not survive the
/// description boundary. A fn pointer cannot be emitted into a browser, and it
/// cannot be compared or hashed in a way that means anything, which this struct
/// needs. A named family is the same semantics with arbitrary closures given
/// up, and nothing measured wants one: the tree has a single non-linear shape
/// across five controls and no second shape at all.
///
/// # Why the step is here
///
/// Max, in the same breath: if the family is prescriptive anyway, the step
/// spacing belongs in it. On a slider the granularity and the mapping are one
/// decision — a curve chosen without saying how finely it moves is half an
/// answer — and holding them apart is what let a 0-to-1 threshold ship as a
/// two-position control, since the host default of 1 was applied to a mapping
/// nobody had named. It also un-overloads [`Field::step`], which stays as it
/// was for a *typed* value, where there is no mapping and the granularity is a
/// plain fact about the number.
///
/// A future curve carrying a fact of its own — an exponent, an inflection —
/// puts it in its own variant rather than on the struct, which is the second
/// reason this shape is right.
///
/// **The step is in the value's own units under every curve.** What a curve
/// changes is the mapping, not the units the granularity is measured in: a step
/// of `0.001` on an envelope time is three decimals whether the track is
/// logarithmic or not, and a renderer that reads the step for display precision
/// keeps reading it the same way.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Curve<'a> {
    /// Constant slope: `f(t) = min + t * (max - min)`.
    ///
    /// What every described range meant before this enum existed, and the
    /// default, so a site that says nothing is correct unchanged.
    Linear {
        /// The granularity, in the value's own units. `None` is the host's own.
        step: Option<&'a str>,
    },
    /// Constant ratio: `f(t) = min * (max / min).powf(t)`.
    ///
    /// The mapping for a question whose extent spans orders of magnitude and
    /// whose interesting half is the small end. audiofiles' envelope times run
    /// 0.001 to 5 seconds, where a 5 ms attack and a 50 ms attack are audibly
    /// different instruments and a linear track puts both inside its first one
    /// percent.
    ///
    /// # It needs positive bounds
    ///
    /// A constant ratio is undefined across zero, so this asks for `min > 0`.
    /// A range that does not have that is mapped [`Linear`](Self::Linear)ly
    /// instead — see [`value_at`](Self::value_at). Stated rather than enforced,
    /// the way every other constraint in this crate is, and it is not a
    /// hypothetical: an envelope's sustain is a 0-to-1 level and is linear for
    /// this reason rather than by oversight.
    Logarithmic {
        /// The granularity, in the value's own units. `None` is the host's own.
        step: Option<&'a str>,
    },
}

impl Default for Curve<'_> {
    fn default() -> Self {
        Self::Linear { step: None }
    }
}

impl<'a> Curve<'a> {
    /// The granularity this curve moves in, whichever curve it is.
    ///
    /// Every variant carries one, so reading it does not need a match at each
    /// of the four renderers.
    #[must_use]
    pub const fn step(self) -> Option<&'a str> {
        // No wildcard: `#[non_exhaustive]` binds downstream, not here, so a
        // curve added later has to answer this rather than fall through to a
        // granularity nobody chose.
        match self {
            Self::Linear { step } | Self::Logarithmic { step } => step,
        }
    }

    /// Whether this curve maps as a constant ratio *given these bounds*.
    ///
    /// The bounds are the argument because [`Logarithmic`](Self::Logarithmic)
    /// is a request rather than a guarantee: it needs `0 < min < max`, and a
    /// range that does not have that is drawn linearly. A renderer asks this
    /// instead of matching on the variant, so the fallback is decided in one
    /// place rather than four.
    #[must_use]
    pub fn is_ratio(self, min: f64, max: f64) -> bool {
        matches!(self, Self::Logarithmic { .. }) && min > 0.0 && max > min
    }

    /// The value at a position along the track, where `position` is 0 to 1.
    ///
    /// `f`. The whole point of the type, and it lives here rather than in each
    /// renderer so that a terminal's bar, an egui slider and a browser's input
    /// cannot disagree about where a value sits.
    ///
    /// A position outside 0 to 1 is clamped, and bounds that are equal or
    /// inverted give `min` back: a track with no extent has one value on it.
    #[must_use]
    pub fn value_at(self, position: f64, min: f64, max: f64) -> f64 {
        let position = position.clamp(0.0, 1.0);
        // NaN named rather than fallen through: `max <= min` is false for a NaN
        // bound, so without it a track with no numbers on it would be mapped as
        // if it had two.
        if max <= min || min.is_nan() || max.is_nan() {
            return min;
        }
        if self.is_ratio(min, max) {
            min * (max / min).powf(position)
        } else {
            position.mul_add(max - min, min)
        }
    }

    /// The position a value sits at, where the answer is 0 to 1.
    ///
    /// `f` inverted, which is what a renderer needs to *draw* a value it was
    /// handed. Same clamping and the same degenerate answer as
    /// [`value_at`](Self::value_at).
    #[must_use]
    pub fn position_of(self, value: f64, min: f64, max: f64) -> f64 {
        if max <= min || min.is_nan() || max.is_nan() {
            return 0.0;
        }
        let value = value.clamp(min, max);
        let position = if self.is_ratio(min, max) {
            (value / min).ln() / (max / min).ln()
        } else {
            (value - min) / (max - min)
        };
        position.clamp(0.0, 1.0)
    }
}