Skip to main content

makeover_layout/
field.rs

1use crate::{Choice, Curve, ThemeChoice, Tone};
2
3// Names this module's prose links to, resolved for rustdoc.
4#[allow(unused_imports)]
5use crate::{Awaiting, Contrast, Fill, ThemeVariant};
6
7/// What kind of value a form field takes.
8///
9/// The union of the two vocabularies that diverged, which is what triggered
10/// this crate. They have since converged on their own: both apps now have a
11/// `renderFormField` emitting the same anatomy, and what is left differing is
12/// the kind set, the error shape, and whether the return is a string or a node.
13///
14/// Validation is deliberately absent. Neither app has a shared story (goingson
15/// validates after collecting the form data, with per-field transform hooks;
16/// Balanced Breakfast has `required` and nothing else), and a schema that
17/// describes fields but not constraints acquires a constraint layer per app,
18/// which is exactly how the current divergence started. Naming it absent is a
19/// decision; leaving it unmentioned would not be.
20/// `#[non_exhaustive]` for the reason [`Fill`] is: renderers match on this and
21/// the set keeps growing, so growth must not be a lockstep event.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum FieldKind {
25    /// A single line of text.
26    Text,
27    /// A single line of text that must never be echoed, logged or round-tripped
28    /// through anything that might persist it.
29    Secret,
30    /// A number.
31    Number,
32    /// A number inside bounds the user drags across, where the range being
33    /// visible is the point.
34    ///
35    /// Not [`Number`](Self::Number) with [`min`](Field::min) and
36    /// [`max`](Field::max), which is the reading to resist and is the same
37    /// resistance [`Radio`](Self::Radio) needed against `Select`. A bounded
38    /// number and a validated number are different *questions*. A validated
39    /// number is typed and can be wrong: the bounds are a rule the answer is
40    /// checked against, and being told "must be at least 1" afterwards is the
41    /// normal course of it. A range cannot be out of range at all, because the
42    /// bounds are the control's extent rather than a rule, and the two ends are
43    /// what the question means, audiofiles asks for a classifier threshold
44    /// between 0 and 1, where 0 is never and 1 is only-on-certainty, and a typed
45    /// 0.72 says nothing without both ends on screen beside it.
46    ///
47    /// A renderer cannot infer which one is meant from `min`/`max` alone, which
48    /// is why this is a kind and not an inference: goingson's `min="1"` duration
49    /// is a validated number and would become a slider.
50    ///
51    /// The membership test passes without stretching: a webview emits
52    /// `<input type="range">`, egui has `Slider`, a terminal draws a bar and
53    /// takes arrow keys, a CLI takes a bounded argument.
54    ///
55    /// # It owes its bounds
56    ///
57    /// [`min`](Field::min) and [`max`](Field::max) are `Option` for every other
58    /// kind and are **required** here, in the sense the description can require
59    /// anything: [`Field::bounded`] is the check, and a range missing one has no
60    /// extent for a renderer to draw. What a renderer does with an unbounded
61    /// range is its own call and both answers are honest (fall back to a typed
62    /// number, or pick a host default) so this is stated rather than enforced,
63    /// the way every other constraint here is.
64    ///
65    /// [`Field::step`] is the third fact and is genuinely optional: absent, the
66    /// host's own granularity stands.
67    Range,
68    /// One question with two ends: a lower value and an upper one, submitted
69    /// under two names.
70    ///
71    /// "Show me samples between 90 and 130 BPM" has a single answer with two
72    /// ends, and the ends constrain each other: a minimum above the maximum is
73    /// not a wrong value, it is an empty result nobody asked for. Described as
74    /// two [`Number`](Self::Number) fields that is unsayable, nothing says they
75    /// are one question, so a renderer draws two controls with two labels and no
76    /// relationship, and [`Field::error`] can only be attached to one side of a
77    /// fault that belongs to both.
78    ///
79    /// Not [`Range`](Self::Range), which was the reading to resist and the
80    /// resistance is the same one `Range` itself needed against `Number`. A
81    /// range describes *one* value inside an extent; this describes two, and the
82    /// extent is a bound on each rather than the question's meaning. The two
83    /// come apart in the answer: a range has a value, an interval has a pair,
84    /// and either end may be absent while the other stands.
85    ///
86    /// # It states both names
87    ///
88    /// [`Field::name`] is the lower end and [`Field::upper_name`] is the upper
89    /// one, stated rather than derived. One member instead of a naming
90    /// convention this crate would then own forever.
91    ///
92    /// Direction is carried by which member the name sits in, so nothing
93    /// separate says which end is which.
94    ///
95    /// # What it does not enforce
96    ///
97    /// The crossing rule. A lower end above the upper one is describable here
98    /// and always was, exactly as an out-of-[`min`](Field::min) number is: this
99    /// crate carries constraints and never checks them, and deciding a value is
100    /// wrong stays with whoever validated. What the description buys is that the
101    /// fault now has one place to be reported rather than two.
102    ///
103    /// # Both ends take the same facts
104    ///
105    /// [`min`](Field::min), [`max`](Field::max), [`step`](Field::step) and
106    /// [`unit`](Field::unit) describe the axis rather than one end of it, so
107    /// they are read once and applied to both. Six of audiofiles' filter axes
108    /// are exactly this: one extent, one unit, one granularity, two ends.
109    ///
110    /// The bounds are optional here, unlike `Range`. They are a rule the answer
111    /// is checked against rather than the control's extent, which is
112    /// [`Number`](Self::Number)'s arrangement and not a slider's.
113    Interval,
114    /// An email address.
115    ///
116    /// Distinct from [`Text`](Self::Text) because the distinction is not
117    /// decoration: a webview renderer emits `type="email"`, which on a touch
118    /// device changes the keyboard that appears and turns on the platform's own
119    /// validation. goingson ships to iOS, so collapsing this into text costs a
120    /// keyboard with no `@` on it.
121    Email,
122    /// A URL. Same reasoning as [`Email`](Self::Email).
123    Url,
124    /// A telephone number. Same reasoning as [`Email`](Self::Email), and the
125    /// clearest case of it: the keyboard is a numeric pad rather than letters.
126    Tel,
127    /// A calendar day, with no time of day in it.
128    ///
129    /// [`Email`](Self::Email)'s argument, and it carries further: a webview
130    /// emits `type="date"`, which is a native picker, the platform's own
131    /// validation, and on a touch device the date keyboard. Described as
132    /// [`Text`](Self::Text) with a hint reading "YYYY-MM-DD", all three are
133    /// lost and the hint is doing the platform's job in prose.
134    ///
135    /// The membership test passes on every host without stretching: a webview
136    /// and a Tauri app emit the input, egui has a date picker, a terminal
137    /// prompts for a day and can validate it, a CLI takes an argument.
138    ///
139    /// # The value is ISO 8601, `YYYY-MM-DD`
140    ///
141    /// Named here rather than left to each host, because a host that picks
142    /// differently sends a server something it parses differently, and the
143    /// failure is silent and per-host. It is `<input type="date">`'s own wire
144    /// format, so the webview renderer owes nothing to honour it and the other
145    /// hosts have one spelling to meet. [`DATE_FORMAT`] is the constant, and a
146    /// test asserts this doc and that constant agree.
147    Date,
148    /// A calendar day and a time of day together.
149    ///
150    /// Apart from [`Date`](Self::Date) because the question is different rather
151    /// than more precise: "which day does this expire" and "at what moment does
152    /// this publish" are asked by different screens and answered by different
153    /// controls. A webview emits `type="datetime-local"` for one and
154    /// `type="date"` for the other, and a host that collapsed them would ask
155    /// half the tree for a precision it does not want.
156    ///
157    /// Both arrived together on measurement rather than on symmetry: 13 sites
158    /// of each across the MNW server and goingson, and **zero** of `time`,
159    /// `month` or `week`, which is why those are not here. A member added for a
160    /// case nobody has is a member designed against nothing, which is
161    /// [`File`](Self::File)'s reasoning about `accept` applied to a whole
162    /// member.
163    ///
164    /// # The value is `YYYY-MM-DDTHH:MM`, local, with no zone
165    ///
166    /// `<input type="datetime-local">`'s own format, and the "local" is the
167    /// load-bearing half: the value carries no offset and no `Z`, so the moment
168    /// it names is only fixed once something supplies a zone. That is the app's
169    /// business and not the description's. Seconds are absent, which is the
170    /// browser's own default and is left as the rule rather than restated as a
171    /// constraint. [`DATETIME_FORMAT`] is the constant.
172    ///
173    /// [`Field::min`] and [`Field::max`] already take "the host's own spelling
174    /// of a bound", so a floor of *not in the past* needs nothing new here: it
175    /// is a string in this same format.
176    DateTime,
177    /// Several lines of text.
178    Textarea,
179    /// Several lines of text the user writes markdown in.
180    ///
181    /// The editing counterpart of prose a description carries as markdown
182    /// source, and the reason it can exist at all is the same one that lets the
183    /// source be carried: editing markdown is editing text, so a terminal, an
184    /// immediate-mode host and a webview all have an honest answer, and none of
185    /// them has to refuse. A kind that meant "rich text" in the WYSIWYG sense
186    /// would have been a document model, and two of the three hosts would have
187    /// had to draw something they cannot.
188    ///
189    /// What the mark buys over [`Textarea`](Self::Textarea) is that a renderer
190    /// may offer the affordances markdown has and plain text does not (a
191    /// preview, a syntax pass, a monospaced face for the source) and that a
192    /// host reading the value back knows what it is holding. A renderer with
193    /// none of that draws a textarea, which is why this is additive rather than
194    /// a second control.
195    ///
196    /// It says nothing about **when** the value is saved. Autosave is a clock,
197    /// clocks are not described here, and the four MNW editors this was measured
198    /// against each keep their own.
199    ///
200    /// Sanitising stays where it already is for markdown that is only displayed:
201    /// with the renderer, at the point markup is produced. Being described is
202    /// not a safety property, and a host with its own sanitiser and its own
203    /// content-security posture still owns both.
204    Rich,
205    /// One of a fixed set, offered behind a control that shows one at a time.
206    Select,
207    /// One of a fixed set, with every option on screen at once.
208    ///
209    /// Not a presentation of [`Select`](Self::Select), which is the reading to
210    /// resist: what differs is a property of the *question*. A choice that is
211    /// consequential or irreversible has to be readable without opening
212    /// anything, because a closed control shows one option and hides the rest,
213    /// and the one it shows is whichever was current before the user had read
214    /// the alternatives. audiofiles asks whether a library copies samples into
215    /// its store or references them where they lie (which cannot be changed
216    /// afterwards) and had already promoted that out of a checkbox by hand,
217    /// with a comment giving this reason, before the description could say it.
218    ///
219    /// Everything here is an `<input type=...>`, a `<select>` or a
220    /// `<textarea>`, and the way this enum grows is by a site being measured
221    /// rather than by a list being completed. No member is ever "the last one".
222    Radio,
223    /// Any number of a fixed set, with every option on screen at once.
224    ///
225    /// [`Radio`](Self::Radio)'s shape with the answer widened from one option
226    /// to a set, and not a row of [`Checkbox`](Self::Checkbox)es, which is the
227    /// reading to resist. A row of checkboxes is several questions, each with
228    /// its own name, label and error; this is one question whose answer has
229    /// several parts. The difference shows the moment anything is wrong: "pick
230    /// at least one" belongs to the set, and a row of checkboxes has nowhere to
231    /// put it but under one box.
232    ///
233    /// Four measured sites, all in the MNW server and all written by hand. A
234    /// project's features are asked three times, in its settings and in both
235    /// project wizards. The fourth is the item wizard's bundle picker, which
236    /// joins the ticked ids with commas into a hidden input: the delimiter this
237    /// member exists so that no host has to invent.
238    ///
239    /// # The ticked set is marked on the options
240    ///
241    /// Each ticked option carries [`Choice::chosen`]. A set is not one string,
242    /// and a value holding one would need a separator that any option's value
243    /// could contain. `chosen` is already how an option list marks itself, so
244    /// a checklist marks as many options as are ticked and needs no second
245    /// spelling.
246    ///
247    /// # It submits the name once per ticked option
248    ///
249    /// HTML's own wire format for a set of checkboxes sharing a name, so a
250    /// webview owes nothing to honour it. An empty set submits nothing under
251    /// the name, and a handler reads an absent name as the empty answer rather
252    /// than as a missing one.
253    Checklist,
254    /// On or off.
255    Checkbox,
256    /// A file the user picks from wherever the host keeps files.
257    ///
258    /// It was filed as a router finding (a control whose destination is a
259    /// host capability rather than an address) and splitting it is what made
260    /// it two answers instead of one member satisfying neither. *Opening* a
261    /// file is a one-way handoff and needs no new API. *Picking* one returns a
262    /// value into a write, which is a form concern, which is this.
263    ///
264    /// The membership test passes on every host and not by a stretch: a Tauri
265    /// app opens a native picker, a server renders `<input type="file">`, a
266    /// terminal prompts for a path, a CLI takes an argument. That is closer to
267    /// [`Email`](Self::Email), which exists because it changes the keyboard,
268    /// than to anything bespoke.
269    ///
270    /// # The four things an upload says, and where each of them lives
271    ///
272    /// | axis | where |
273    /// |---|---|
274    /// | what it accepts | [`Field::accept`] |
275    /// | one file or several | [`Field::multiple`] |
276    /// | where the bytes go | the router's action, not here |
277    /// | how far along it is | [`Awaiting`] on that action |
278    ///
279    /// Only the first two are this crate's, and that split is the answer to
280    /// "describe an upload in full" rather than a gap in it. A destination is an
281    /// address and this crate holds no addresses; progress is a live number and
282    /// a description is built once, so the number is the renderer's to observe
283    /// against the size [`Awaiting::amount`] carried before the transfer began.
284    ///
285    /// # How the file is handed over is the host's
286    ///
287    /// A drop area, a button opening a native picker, a path typed at a prompt:
288    /// all three are the same field, and every measured site has the first. It
289    /// is not described for the reason no gesture is, this crate owns no
290    /// coordinates and no pointer, and a terminal that cannot be dropped on
291    /// would be refusing a description it can otherwise honour completely.
292    ///
293    /// [`Field::accept`] and [`Field::multiple`] are measured rather than
294    /// deferred. A member designed against nothing is the rule to keep: count
295    /// the sites before adding one.
296    File,
297    /// Which theme the app wears.
298    ///
299    /// The one member here that names a *subject* rather than a shape of
300    /// answer, and it is worth saying why that is not the door it looks like.
301    /// Every other kind is a question a screen might ask about anything; this
302    /// one is a specific question every app in the family asks, once, on its
303    /// settings screen, and three of them wrote the same control by hand.
304    ///
305    /// # It is furniture, and the measurement is what says so
306    ///
307    /// The reading to resist is that this is [`Select`](Self::Select) with a
308    /// grouped option list. Option lists that group barely occur; the thing
309    /// that recurs is this picker.
310    ///
311    /// # What it carries that a select cannot
312    ///
313    /// [`Field::themes`] rather than [`Field::options`], because a theme is
314    /// four facts and an option is two. The two extra facts are the ones no
315    /// app can supply without redoing work the theme layer has already done:
316    /// which [`ThemeVariant`] group a theme is in, and how legible its muted
317    /// text measured. `Choice::new(id, format!("{name} ({variant})"))` is what
318    /// the three apps had, and it flattens the group into prose and loses the
319    /// tier entirely.
320    ///
321    /// [`Field::follows`] carries the entry that is not a theme.
322    ///
323    /// # The cost, stated rather than discovered later
324    ///
325    /// This puts one screen's shape into a vocabulary that otherwise holds
326    /// none, which was the objection raised against it and accepted going in.
327    /// The mitigation is narrowness: this describes a theme picker, not a
328    /// general "list the host resolved" mechanism. A second host-resolved list
329    /// is when that generalisation gets measured, and not before.
330    ///
331    /// A renderer that has not heard of it draws a select over
332    /// [`Field::themes`]' names and loses the grouping, which is the state
333    /// every app was in before this member. Degrading to the status quo ante
334    /// is the floor the member is designed against.
335    Theme,
336    /// Carried through the form and never shown.
337    Hidden,
338}
339
340/// The wire format a [`FieldKind::Date`] value takes: ISO 8601, `YYYY-MM-DD`.
341///
342/// A constant rather than a sentence in a doc comment, because the reason to
343/// name the format at all is that a host picking its own would fail silently
344/// against a server parsing another. A host that cannot emit the native control
345/// still has one spelling to meet, and can say which one it meant.
346pub const DATE_FORMAT: &str = "%Y-%m-%d";
347
348/// The wire format a [`FieldKind::DateTime`] value takes: `YYYY-MM-DDTHH:MM`,
349/// local, carrying no zone and no seconds.
350///
351/// [`DATE_FORMAT`]'s sibling and there for its reason. The absent zone is a
352/// property of the value rather than an omission: the moment is not fixed until
353/// something outside the description supplies one.
354pub const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M";
355
356impl FieldKind {
357    /// Whether the value the kind takes is a moment rather than a string.
358    ///
359    /// Named once here for the reason [`offers_options`](Self::offers_options)
360    /// is: two kinds answer yes, and a host that has to parse or format a value
361    /// needs to ask without spelling the pair out at each renderer. A third
362    /// temporal kind should land here and nowhere else.
363    ///
364    /// The format each one takes is [`DATE_FORMAT`] and [`DATETIME_FORMAT`].
365    #[must_use]
366    pub const fn temporal(self) -> bool {
367        matches!(self, Self::Date | Self::DateTime)
368    }
369
370    /// Whether the field is drawn at all.
371    #[must_use]
372    pub const fn visible(self) -> bool {
373        !matches!(self, Self::Hidden)
374    }
375
376    /// Whether the value must be kept out of logs and diagnostics.
377    #[must_use]
378    pub const fn confidential(self) -> bool {
379        matches!(self, Self::Secret)
380    }
381
382    /// Where the field's own label sits.
383    ///
384    /// A checkbox labels itself on the right of the box; everything else takes
385    /// a label above. Both webview apps already do this and both special-case
386    /// it inline, which is the tell that it belongs in the description.
387    ///
388    /// A [`Radio`](Self::Radio) is not one of them, and the near-miss is worth
389    /// naming: its *options* each label themselves, but the field still asks a
390    /// question above them, so the group takes a label like everything else.
391    #[must_use]
392    pub const fn labels_itself(self) -> bool {
393        matches!(self, Self::Checkbox)
394    }
395
396    /// Whether the kind reads [`Field::options`].
397    ///
398    /// Three kinds do, so the set is named once here rather than spelled out at
399    /// each renderer and again in [`Field::options`]' own doc, where "every
400    /// kind but `Select`" was true for exactly one release.
401    /// [`Checklist`](Self::Checklist) is the third and landed here and nowhere
402    /// else, which is what this predicate is for.
403    #[must_use]
404    pub const fn offers_options(self) -> bool {
405        matches!(self, Self::Select | Self::Radio | Self::Checklist)
406    }
407
408    /// Whether the answer is a set of the options rather than one of them.
409    ///
410    /// One kind answers yes, and it gets a name for
411    /// [`takes_files`](Self::takes_files)'s reason: every renderer that walks
412    /// [`Field::options`] has to ask it before it decides whether an option is
413    /// marked by the field's value or by [`Choice::chosen`] alone, and whether
414    /// picking one clears the others.
415    #[must_use]
416    pub const fn takes_several(self) -> bool {
417        matches!(self, Self::Checklist)
418    }
419
420    /// Whether the kind reads [`Field::themes`] and [`Field::follows`].
421    ///
422    /// One member answers yes, and it gets a name for
423    /// [`takes_files`](Self::takes_files)'s reason rather than in spite of
424    /// being alone: four renderers ask it before they read either member, and
425    /// a `matches!` per renderer is where the next one goes missing.
426    ///
427    /// Deliberately not folded into
428    /// [`offers_options`](Self::offers_options). A theme picker offers no
429    /// [`Choice`]es at all, so a renderer walking `options` for it walks an
430    /// empty slice and draws an empty control.
431    #[must_use]
432    pub const fn offers_themes(self) -> bool {
433        matches!(self, Self::Theme)
434    }
435
436    /// Whether the value runs to more than one line.
437    ///
438    /// Named once here for [`temporal`](Self::temporal)'s reason: two kinds
439    /// answer yes, every renderer has to ask it before it can size anything,
440    /// and a `matches!` per renderer is the pair drifting apart one member at a
441    /// time. What a host does with the markdown, if anything, it reads from the
442    /// kind itself; this is only whether one line is enough.
443    #[must_use]
444    pub const fn multiline(self) -> bool {
445        matches!(self, Self::Textarea | Self::Rich)
446    }
447
448    /// Whether the value is a file the host picks rather than a string typed
449    /// into a box.
450    ///
451    /// One member answers yes, which is [`visible`](Self::visible)'s and
452    /// [`confidential`](Self::confidential)'s footing rather than a departure
453    /// from it: the question gets a name because three renderers ask it before
454    /// they can read [`Field::accept`] or [`Field::multiple`], and a `matches!`
455    /// per renderer is where a second file-taking kind would go missing.
456    #[must_use]
457    pub const fn takes_files(self) -> bool {
458        matches!(self, Self::File)
459    }
460
461    /// Whether the value is a quantity, so [`Field::unit`] means something.
462    ///
463    /// The numeric kinds and nothing else. A date is a quantity in the sense
464    /// that it is ordered, and it is not one in the sense that matters here:
465    /// its unit is fixed by the kind, so `Date` carrying `days` would be the
466    /// description restating what [`kind`](Field::kind) already said.
467    ///
468    /// [`takes_files`](Self::takes_files)'s footing, and for its reason: the
469    /// renderers ask this before they decide where a unit goes, and a
470    /// `matches!` per renderer is where the next measurable kind goes missing.
471    ///
472    /// [`Interval`](Self::Interval) is measurable too: an axis is measured in
473    /// something and both its ends are in it.
474    #[must_use]
475    pub const fn measurable(self) -> bool {
476        matches!(self, Self::Number | Self::Range | Self::Interval)
477    }
478}
479
480/// A family of media a file can belong to.
481///
482/// Three members, because three is what a media type's own first segment offers
483/// that a renderer can do anything with. `text` and `application` are families
484/// too and neither buys a disclosure (there is no preview of an
485/// `application/octet-stream`) so naming them would be a member added for a
486/// case nobody has.
487///
488/// It is the answer to "which disclosure", not a validation rule.
489/// [`Field::accept`] is what a host filters on.
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
491#[non_exhaustive]
492pub enum Family {
493    /// A still picture.
494    Image,
495    /// Sound.
496    Audio,
497    /// Moving pictures, with or without sound.
498    Video,
499}
500
501impl Family {
502    /// The wildcard media type that means the whole family.
503    ///
504    /// `image/*` and its two siblings, which is what the measured sites write
505    /// and what a webview puts in an `accept` attribute. Named here so the three
506    /// renderers do not each spell the star.
507    #[must_use]
508    pub const fn wildcard(self) -> &'static str {
509        match self {
510            Self::Image => "image/*",
511            Self::Audio => "audio/*",
512            Self::Video => "video/*",
513        }
514    }
515
516    /// The family a media type's first segment names, if it is one of these.
517    ///
518    /// Case-insensitive on the segment, because a media type is
519    /// case-insensitive and half the tree writes them lowercase by habit rather
520    /// than by rule.
521    #[must_use]
522    pub fn of_type(media_type: &str) -> Option<Self> {
523        let (top, _) = media_type.split_once('/')?;
524        if top.eq_ignore_ascii_case("image") {
525            Some(Self::Image)
526        } else if top.eq_ignore_ascii_case("audio") {
527            Some(Self::Audio)
528        } else if top.eq_ignore_ascii_case("video") {
529            Some(Self::Video)
530        } else {
531            None
532        }
533    }
534}
535
536/// One entry in a file field's accept list.
537///
538/// Three shapes rather than a string, and all three are in the measured sites:
539/// the MNW server writes `image/*`, `image/jpeg,image/png,image/webp`,
540/// `.zip,.dmg,.exe,.appimage,.deb,.tar.gz,.clap,.vst3` and, in one place,
541/// `.csv,text/csv`. A single string would carry all of them and answer nothing
542/// about any of them.
543///
544/// # Why the list is not just a filter
545///
546/// It is read twice. Once to decide what the picker offers, which any of the
547/// three shapes serves, and once to decide **which disclosure** the field gets:
548/// a preview for a picture, a duration or a waveform for a sound. There is one
549/// upload shape and a media upload is that shape with more of it shown, so the
550/// accept list is what says which more. [`family`](Self::family) is that
551/// question answered once here instead of a media-type parser in each renderer.
552///
553/// # A suffix names no family, on purpose
554///
555/// `.mp3` is audio in fact, and nothing here says so. A suffix-to-family table
556/// in a published crate is a mapping that goes stale, disagrees with the host's
557/// own idea of what a file is, and is wrong the first time somebody hands it a
558/// container. A call site that wants a picture's preview writes
559/// [`Family::Image`] or `image/jpeg`; a call site listing installer suffixes
560/// wants no disclosure anyway, which is the measured case.
561#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
562#[non_exhaustive]
563pub enum Accepted<'a> {
564    /// Every file of a family: `image/*` and its siblings.
565    Family(Family),
566    /// One media type, written the way a media type is written:
567    /// `image/jpeg`, `text/csv`.
568    Type(&'a str),
569    /// One file-name suffix, written with its leading dot: `.zip`, `.tar.gz`.
570    ///
571    /// A suffix and not an extension, because `.tar.gz` is a measured site and
572    /// is two dots.
573    Suffix(&'a str),
574}
575
576impl<'a> Accepted<'a> {
577    /// The family this entry belongs to, when it names one.
578    ///
579    /// [`None`] for a [`Suffix`](Self::Suffix) and for any media type outside
580    /// the three families, which is the honest answer rather than a missing
581    /// one: the description did not say.
582    #[must_use]
583    pub fn family(self) -> Option<Family> {
584        match self {
585            Self::Family(family) => Some(family),
586            Self::Type(media_type) => Family::of_type(media_type),
587            Self::Suffix(_) => None,
588        }
589    }
590
591    /// How a host that wants one string writes this entry.
592    ///
593    /// A webview's `accept` attribute takes exactly these spellings, and a
594    /// terminal listing what it will take reads the same words.
595    #[must_use]
596    pub const fn as_str(self) -> &'a str {
597        match self {
598            Self::Family(family) => family.wildcard(),
599            Self::Type(text) | Self::Suffix(text) => text,
600        }
601    }
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
605#[non_exhaustive]
606pub struct Field<'a> {
607    /// What kind of value it takes.
608    pub kind: FieldKind,
609    /// The name the value is submitted under.
610    ///
611    /// The *lower* end's name for a [`FieldKind::Interval`], whose upper end is
612    /// [`upper_name`](Self::upper_name). Every other kind submits one value and
613    /// this is the whole of it.
614    pub name: &'a str,
615    /// The name a [`FieldKind::Interval`]'s upper end is submitted under.
616    ///
617    /// [`None`] for every other kind, and sayable-and-ignored there the way
618    /// [`options`](Self::options) is on a kind that offers none.
619    ///
620    /// Stated rather than derived from [`name`](Self::name), and
621    /// [`FieldKind::Interval`] carries the measurement that decided it: the two
622    /// sites in this tree disagree about affix order, so a derived rule would
623    /// rename one of them. Which member a name sits in is also what says which
624    /// end it is, so nothing separate carries the direction.
625    ///
626    /// An interval missing it is an interval with one end that can be submitted,
627    /// which is a description a renderer may draw honestly and no better than
628    /// that. [`Field::interval`] is what makes forgetting it unsayable, on the
629    /// same footing as [`Field::range`] and its bounds.
630    pub upper_name: Option<&'a str>,
631    /// What the user is asked for.
632    pub label: &'a str,
633    /// Standing help, shown whether or not anything is wrong.
634    pub hint: Option<&'a str>,
635    /// What is currently wrong with the value.
636    pub error: Option<&'a str>,
637    /// A consequence of the answer the user has given, carrying its own tone.
638    ///
639    /// The third message channel, between [`hint`](Self::hint) and
640    /// [`error`](Self::error) and overlapping neither. A hint is standing help
641    /// that does not depend on the value; an error says the value is not
642    /// acceptable. A note is the case in the middle: the value is perfectly
643    /// acceptable and choosing it costs something the user should know about.
644    ///
645    /// The first consumer is audiofiles' export Format field, where choosing
646    /// WAV or AIFF over Original re-encodes and silently drops embedded BWF,
647    /// iXML, loop points, cue markers and ID3. That is not a validation
648    /// failure and it is not standing help (it is true of one answer to one
649    /// question) and it was hand-drawn in the app's own draw callback for
650    /// want of anywhere to say it.
651    ///
652    /// The tone is carried rather than fixed at [`Tone::Warning`] because the
653    /// channel is not only for warnings: the same slot says "this is the
654    /// recommended one" ([`Tone::Success`]) and "this is what that setting
655    /// implies" ([`Tone::Info`]). A renderer gets the announcement behaviour
656    /// off the tone for free, makeover-webview emits `data-tone` and treats
657    /// Warning and Danger as assertive for `aria-live`.
658    ///
659    /// It does **not** make the field invalid. [`invalid`](Self::invalid) stays
660    /// `error.is_some()`, so a note never marks the group as a problem.
661    ///
662    /// # Precedence, for a renderer with room for one
663    ///
664    /// Error, then note, then hint. A renderer that shows every message shows
665    /// them in that order too. makeover-tui is the one with room for exactly
666    /// one line, and it is why the order is decided here rather than three
667    /// times: what is wrong outranks what it costs, which outranks how it
668    /// works.
669    pub note: Option<(Tone, &'a str)>,
670    /// Ghost text shown while the field is empty.
671    ///
672    /// User-facing text, and it sits with `label` and `hint` rather than with
673    /// the value because it is a property of the *question* and not of the
674    /// answer.
675    ///
676    /// Not a substitute for a label. A field labelled only by its placeholder
677    /// loses its label the moment anything is typed, and no renderer here can
678    /// make that not happen, so the description keeps both.
679    pub placeholder: Option<&'a str>,
680    /// The options offered, in the order they are offered.
681    ///
682    /// Empty for every kind [`FieldKind::offers_options`] rejects. A field
683    /// described with no options is sayable on purpose: it is what an app with
684    /// an unfinished-loading option list actually has, and a renderer showing
685    /// an empty control says so on screen rather than in a log.
686    ///
687    /// Which option is *current* is not here. That is the value, and the value
688    /// is renderer state.
689    pub options: &'a [Choice<'a>],
690    /// The themes offered, in the order they are offered.
691    ///
692    /// Empty for every kind [`FieldKind::offers_themes`] rejects, and sayable
693    /// as empty for the one that accepts it: an app whose theme directories
694    /// hold nothing has a picker offering only [`follows`](Self::follows),
695    /// which is a true description of that machine.
696    ///
697    /// **The order is the grouping.** Entries arrive sorted by
698    /// [`ThemeVariant`] and then by [`Contrast`] within each variant, so a
699    /// renderer that draws headings walks the run of one variant and a renderer
700    /// that cannot still gets the useful order. Handing back groups would force
701    /// the second renderer to flatten what the first wanted.
702    ///
703    /// Nothing here sorts. The description carries the order it was given, and
704    /// the sort belongs with whoever measured the tiers, `makeover::theme_options`
705    /// is what produces it, and re-sorting here would be this crate deciding a
706    /// question it cannot see the inputs to.
707    ///
708    /// Which theme is *current* is not here. That is the value, and the value
709    /// is renderer state, exactly as it is for [`options`](Self::options).
710    pub themes: &'a [ThemeChoice<'a>],
711    /// The entry that follows the ambient mode instead of naming a theme.
712    ///
713    /// [`None`] for a picker that does not offer one, which is a real answer:
714    /// an app whose host has no ambient mode to follow should not offer a row
715    /// that does nothing.
716    ///
717    /// A [`Choice`] rather than a bare label, because the *value* is the app's.
718    /// Every store in the family spells it `system` today and none of them is
719    /// obliged to; a description that hardcoded the spelling would be this
720    /// crate holding a fact about somebody else's config table.
721    ///
722    /// It is not a [`ThemeChoice`] with an absent variant. Following is a
723    /// standing instruction that resolves differently as the desktop flips, and
724    /// a theme id is an answer that does not, which is the distinction
725    /// `makeover::ThemeSelection` exists to hold, carried here rather than
726    /// blurred.
727    pub follows: Option<Choice<'a>>,
728    /// What a file field takes, in the order a host offering the list shows it.
729    ///
730    /// Empty for every kind [`FieldKind::takes_files`] rejects, and empty is
731    /// also a real answer for one that accepts it: a field that takes any file
732    /// says so by listing nothing, which is what an `<input type="file">` with
733    /// no `accept` does and what most of the measured sites are.
734    ///
735    /// It is a filter and it is the disclosure cue, and [`Accepted`]'s doc
736    /// carries which reading is which. Nothing here validates: a host may hand
737    /// back a file the list does not cover, exactly as a browser does when the
738    /// user switches the picker to "All Files", and deciding a value is wrong
739    /// stays with whoever validated.
740    pub accept: &'a [Accepted<'a>],
741    /// Whether more than one file may be picked at once.
742    ///
743    /// Only [`FieldKind::takes_files`] reads it. A multi-valued answer to any
744    /// other question is a different shape (a set of options, a repeated
745    /// group) and neither is this flag with a different kind beside it.
746    ///
747    /// False is the common case: 4 of the MNW server's 16 file inputs carry it.
748    pub multiple: bool,
749    /// Whether the form refuses to submit without it.
750    pub required: bool,
751    /// The longest the value may be, in characters.
752    pub max_length: Option<u32>,
753    /// The lowest value accepted, as the host would write it.
754    ///
755    /// Text rather than a number, because the bound is only a number for some
756    /// of the kinds that take one. goingson's own sites are `min="1"` on a
757    /// duration and `min="2026-08-09T14:30"` on a datetime, and a numeric member
758    /// could say the first and not the second. The [`kind`](Self::kind) already
759    /// says how to read it, the same way it does for the value.
760    pub min: Option<&'a str>,
761    /// The highest value accepted, as the host would write it. See
762    /// [`min`](Self::min).
763    pub max: Option<&'a str>,
764    /// The granularity the value moves in, as the host would write it.
765    ///
766    /// Text for [`min`](Self::min)'s reason, and it earns it twice over: the
767    /// step of a date is a day and the step of a threshold is 0.01, and a
768    /// numeric member could say one of them.
769    ///
770    /// Absent means the host's own granularity, which is the honest default
771    /// rather than a missing value: a webview's `<input>` steps by 1 unless told
772    /// otherwise, and that is the browser's rule and not this crate's to
773    /// restate.
774    ///
775    /// # It is the granularity of a *typed* value
776    ///
777    /// [`FieldKind::Range`] reads its own from [`curve`](Self::curve) and
778    /// ignores this. On a slider the granularity and the mapping are one
779    /// decision, and on a typed number there is no mapping to decide with. See
780    /// [`Curve`], "Why the step is here".
781    pub step: Option<&'a str>,
782    /// How a slider's position becomes its value, and how finely it moves.
783    ///
784    /// [`FieldKind::Range`]'s, and nothing else reads it: a typed number has a
785    /// granularity but no mapping, and takes [`step`](Self::step) instead.
786    ///
787    /// Defaults to [`Curve::Linear`] with no step, which is what an
788    /// undescribed range means.
789    pub curve: Curve<'a>,
790    /// What the number is measured in: `s`, `ms`, `dB`, `GiB`.
791    ///
792    /// A fact about the value, not part of the question's name, and that
793    /// distinction is the whole reason it is a member. The two readings come
794    /// apart the moment anything reads a field back rather than drawing it: a
795    /// [`max`](Self::max) of `-96` and a bound of `-96 dBFS` are the same number
796    /// and not the same answer, and under the convention this replaces the unit
797    /// could only be recovered by parsing it back out of a label.
798    ///
799    /// # Where a renderer draws it
800    ///
801    /// Beside the value, wherever that host puts a value. Not in the label: a
802    /// label is the sentence above the control, so unit-in-label reads the same
803    /// on every host and is wrong on any host with somewhere better. egui puts
804    /// it inside
805    /// the slider where the readout already is, a terminal appends it to the
806    /// value in the edit line, a webview sets it adjacent to the input.
807    ///
808    /// # Which kinds read it
809    ///
810    /// [`FieldKind::measurable`] answers, and it is
811    /// [`takes_files`](FieldKind::takes_files)'s footing: three renderers ask
812    /// before they can decide whether to draw this, and a `matches!` per
813    /// renderer is where the next measurable kind goes missing. A unit on a kind
814    /// that rejects it is sayable and ignored, the same way
815    /// [`options`](Self::options) is on a kind that offers none.
816    ///
817    /// # Why a string
818    ///
819    /// The measured sites are `GiB`, `dBFS`, `s` and `ms`. An enum would have to
820    /// grow a member for every unit any consumer ever wants, and this crate does
821    /// not know them; it knows that a number has one.
822    ///
823    /// Written as the symbol alone, with no brackets and no leading space. The
824    /// spacing is the renderer's, because a slider's readout and a sentence want
825    /// different answers.
826    pub unit: Option<&'a str>,
827    /// Whether the field lives behind a "more options" disclosure.
828    pub extended: bool,
829    /// Whether this local wall-clock value is submitted as an absolute instant.
830    ///
831    /// [`FieldKind::DateTime`] asks for a time the way a person says one --
832    /// "the 14th at half past two", and that names a different moment in
833    /// Denver than it does in Berlin. A route that stores an instant needs the
834    /// moment, so somebody has to convert. This member says the description
835    /// wants that conversion; it does not say how.
836    ///
837    /// # The conversion belongs to the renderer
838    ///
839    /// Because the renderer is the only party that knows what "your computer's
840    /// time zone" means for its host. A browser has one and the user is sitting
841    /// in it; a TUI reads the host clock; an egui app reads the same clock a
842    /// different way. Nothing above the renderer can answer it, and the
843    /// alternatives all try: a hidden IANA-zone field needs a host capability
844    /// for reading the zone that three hosts answer differently, plus a kind
845    /// that does not exist, plus a wire-contract change; a timezone on the
846    /// user's profile is a product decision wearing a bug's clothes. Say it
847    /// here, and the next reader does not propose them again.
848    ///
849    /// # What a renderer does
850    ///
851    /// Draws the same control it always did (the flag changes what is
852    /// *submitted*, not what is shown) and converts the local value to an
853    /// absolute instant on the way out. A renderer that cannot convert submits
854    /// the local value unchanged, which is what every renderer did before this
855    /// existed.
856    ///
857    /// No wire contract moves when a site adopts it: the route was already
858    /// receiving an instant. What changes is who computed it.
859    ///
860    /// # Which kinds read it
861    ///
862    /// [`FieldKind::DateTime`]'s. `Date` and `Time` are each half a moment and
863    /// cannot name one on their own, so the flag is sayable and ignored there,
864    /// the way [`options`](Self::options) is on a kind that offers none.
865    pub as_instant: bool,
866}
867
868impl<'a> Field<'a> {
869    /// A plain required-nothing field of the given kind.
870    #[must_use]
871    pub const fn new(kind: FieldKind, name: &'a str, label: &'a str) -> Self {
872        Self {
873            kind,
874            name,
875            upper_name: None,
876            label,
877            hint: None,
878            error: None,
879            note: None,
880            placeholder: None,
881            options: &[],
882            themes: &[],
883            follows: None,
884            accept: &[],
885            multiple: false,
886            required: false,
887            max_length: None,
888            min: None,
889            max: None,
890            step: None,
891            curve: Curve::Linear { step: None },
892            unit: None,
893            extended: false,
894            as_instant: false,
895        }
896    }
897
898    /// A bounded number the user drags across its whole extent.
899    ///
900    /// The third under-described kind, and it gets a constructor for
901    /// [`select`](Self::select)'s reason: a range is the one kind whose bounds
902    /// are not a rule but the control itself, so a call site that forgot them
903    /// has a slider with nothing to slide across. Taking them as arguments is
904    /// what makes that unsayable.
905    ///
906    /// The granularity stays a field rather than a fourth argument, and it is
907    /// [`curve`](Self::curve)'s: it is genuinely optional, since the host's own
908    /// is a real answer, and the two bounds are not.
909    #[must_use]
910    pub const fn range(name: &'a str, label: &'a str, min: &'a str, max: &'a str) -> Self {
911        Self {
912            min: Some(min),
913            max: Some(max),
914            ..Self::new(FieldKind::Range, name, label)
915        }
916    }
917
918    /// One question with two ends, taking the name each end submits under.
919    ///
920    /// A constructor for [`range`](Self::range)'s reason inverted: a range's
921    /// bounds are what a call site cannot forget, and an interval's second name
922    /// is. An interval built through [`new`](Self::new) has an upper end with
923    /// nowhere to be submitted, and nothing downstream can invent one, so taking
924    /// it as an argument is what makes that unsayable.
925    ///
926    /// The extent, the granularity and the unit stay members. They describe the
927    /// axis rather than either end and they are genuinely optional, which is
928    /// [`FieldKind::Number`]'s arrangement and the one an interval takes.
929    #[must_use]
930    pub const fn interval(name: &'a str, upper_name: &'a str, label: &'a str) -> Self {
931        Self {
932            upper_name: Some(upper_name),
933            ..Self::new(FieldKind::Interval, name, label)
934        }
935    }
936
937    /// A file field, taking the given accept list.
938    ///
939    /// The fourth under-described kind and it gets a constructor for
940    /// [`range`](Self::range)'s reason rather than [`select`](Self::select)'s:
941    /// a file field with no accept list is not broken, it is a field that takes
942    /// anything, and the hazard is the opposite one. A call site that meant to
943    /// restrict and forgot has a picker offering every file on the machine and
944    /// a server refusing the upload afterwards, which is the failure the list
945    /// exists to move forward. Taking it as an argument is what makes an
946    /// accidental omission a deliberate `&[]`.
947    ///
948    /// [`multiple`](Self::multiple) stays a field. One file is the common case
949    /// and the honest default; several is the thing worth saying.
950    #[must_use]
951    pub const fn upload(name: &'a str, label: &'a str, accept: &'a [Accepted<'a>]) -> Self {
952        Self {
953            accept,
954            ..Self::new(FieldKind::File, name, label)
955        }
956    }
957
958    /// A select offering the given options.
959    ///
960    /// One of the two kinds under-described by [`Field::new`], so it gets a
961    /// constructor rather than leaving every call site to remember that a
962    /// select with an empty `options` renders as an empty select.
963    #[must_use]
964    pub const fn select(name: &'a str, label: &'a str, options: &'a [Choice<'a>]) -> Self {
965        Self::offering(FieldKind::Select, name, label, options)
966    }
967
968    /// A radio group offering the given options.
969    ///
970    /// The other. Same hazard as [`select`](Self::select) and a worse one: a
971    /// radio group with no options draws nothing at all, so a call site that
972    /// forgot them has an empty rectangle rather than a visibly empty control.
973    #[must_use]
974    pub const fn radio(name: &'a str, label: &'a str, options: &'a [Choice<'a>]) -> Self {
975        Self::offering(FieldKind::Radio, name, label, options)
976    }
977
978    /// A checklist offering the given options, the ticked ones marked
979    /// [`chosen`](Choice::chosen).
980    ///
981    /// A constructor for [`radio`](Self::radio)'s reason, and the hazard is
982    /// the same one: a checklist with no options draws nothing at all.
983    #[must_use]
984    pub const fn checklist(name: &'a str, label: &'a str, options: &'a [Choice<'a>]) -> Self {
985        Self::offering(FieldKind::Checklist, name, label, options)
986    }
987
988    /// A theme picker over the themes the host resolved.
989    ///
990    /// A constructor for [`select`](Self::select)'s reason and one of its own.
991    /// The shared reason: a theme picker built through [`new`](Self::new) has
992    /// an empty [`themes`](Self::themes) list and draws an empty control. Its
993    /// own: the list is the *only* thing this kind takes that a call site
994    /// cannot get wrong by omission and can get wrong by substitution, since
995    /// [`options`](Self::options) is right there and reads as if it would work.
996    ///
997    /// [`following`](Self::following) is the builder rather than a fourth
998    /// argument, because a picker with no follow-the-system row is a real
999    /// picker and every renderer draws it honestly.
1000    #[must_use]
1001    pub const fn theme(name: &'a str, label: &'a str, themes: &'a [ThemeChoice<'a>]) -> Self {
1002        Self {
1003            themes,
1004            ..Self::new(FieldKind::Theme, name, label)
1005        }
1006    }
1007
1008    /// The same picker, offering a row that tracks the ambient mode.
1009    ///
1010    /// The [`Choice`] carries the value the app's own store spells it with.
1011    #[must_use]
1012    pub const fn following(mut self, follow: Choice<'a>) -> Self {
1013        self.follows = Some(follow);
1014        self
1015    }
1016
1017    /// The name the upper end of an interval submits under. See [`upper_name`](Self::upper_name).
1018    #[must_use]
1019    pub const fn upper_name(mut self, upper_name: &'a str) -> Self {
1020        self.upper_name = Some(upper_name);
1021        self
1022    }
1023
1024    /// Standing help. See [`hint`](Self::hint).
1025    #[must_use]
1026    pub const fn hint(mut self, hint: &'a str) -> Self {
1027        self.hint = Some(hint);
1028        self
1029    }
1030
1031    /// What is wrong with the value. See [`error`](Self::error).
1032    #[must_use]
1033    pub const fn error(mut self, error: &'a str) -> Self {
1034        self.error = Some(error);
1035        self
1036    }
1037
1038    /// Ghost text while empty. See [`placeholder`](Self::placeholder).
1039    #[must_use]
1040    pub const fn placeholder(mut self, placeholder: &'a str) -> Self {
1041        self.placeholder = Some(placeholder);
1042        self
1043    }
1044
1045    /// The options offered. See [`options`](Self::options).
1046    #[must_use]
1047    pub const fn options(mut self, options: &'a [Choice<'a>]) -> Self {
1048        self.options = options;
1049        self
1050    }
1051
1052    /// The themes offered. See [`themes`](Self::themes).
1053    #[must_use]
1054    pub const fn themes(mut self, themes: &'a [ThemeChoice<'a>]) -> Self {
1055        self.themes = themes;
1056        self
1057    }
1058
1059    /// What a file field takes. See [`accept`](Self::accept).
1060    #[must_use]
1061    pub const fn accept(mut self, accept: &'a [Accepted<'a>]) -> Self {
1062        self.accept = accept;
1063        self
1064    }
1065
1066    /// Whether several files may be picked. See [`multiple`](Self::multiple).
1067    #[must_use]
1068    pub const fn multiple(mut self, multiple: bool) -> Self {
1069        self.multiple = multiple;
1070        self
1071    }
1072
1073    /// Whether the form refuses to submit without it.
1074    #[must_use]
1075    pub const fn required(mut self, required: bool) -> Self {
1076        self.required = required;
1077        self
1078    }
1079
1080    /// The longest the value may be, in characters.
1081    #[must_use]
1082    pub const fn max_length(mut self, max_length: u32) -> Self {
1083        self.max_length = Some(max_length);
1084        self
1085    }
1086
1087    /// The lowest value accepted. See [`min`](Self::min).
1088    #[must_use]
1089    pub const fn min(mut self, min: &'a str) -> Self {
1090        self.min = Some(min);
1091        self
1092    }
1093
1094    /// The highest value accepted. See [`min`](Self::min).
1095    #[must_use]
1096    pub const fn max(mut self, max: &'a str) -> Self {
1097        self.max = Some(max);
1098        self
1099    }
1100
1101    /// The granularity a typed value moves in. See [`step`](Self::step).
1102    #[must_use]
1103    pub const fn step(mut self, step: &'a str) -> Self {
1104        self.step = Some(step);
1105        self
1106    }
1107
1108    /// How a slider maps position to value. See [`curve`](Self::curve).
1109    #[must_use]
1110    pub const fn curve(mut self, curve: Curve<'a>) -> Self {
1111        self.curve = curve;
1112        self
1113    }
1114
1115    /// What the number is measured in. See [`unit`](Self::unit).
1116    #[must_use]
1117    pub const fn unit(mut self, unit: &'a str) -> Self {
1118        self.unit = Some(unit);
1119        self
1120    }
1121
1122    /// Whether it lives behind a "more options" disclosure.
1123    #[must_use]
1124    pub const fn extended(mut self, extended: bool) -> Self {
1125        self.extended = extended;
1126        self
1127    }
1128
1129    /// Whether a date-time is submitted as an absolute instant. See [`as_instant`](Self::as_instant).
1130    #[must_use]
1131    pub const fn as_instant(mut self, as_instant: bool) -> Self {
1132        self.as_instant = as_instant;
1133        self
1134    }
1135
1136    /// A consequence of the answer, carrying its own tone. See
1137    /// [`note`](Self::note).
1138    #[must_use]
1139    pub const fn note(mut self, tone: Tone, note: &'a str) -> Self {
1140        self.note = Some((tone, note));
1141        self
1142    }
1143
1144    /// The unit, where the kind reads one: [`unit`](Self::unit) filtered by
1145    /// [`FieldKind::measurable`]. What every renderer draws beside the value.
1146    #[must_use]
1147    pub fn shown_unit(&self) -> Option<&'a str> {
1148        self.unit.filter(|_| self.kind.measurable())
1149    }
1150
1151    /// The label, with `marker` after it when the field is
1152    /// [`required`](Self::required). The marker is the renderer's (a
1153    /// terminal and a painter each spell their own), the rule is this one.
1154    #[must_use]
1155    pub fn marked_label(&self, marker: &str) -> std::borrow::Cow<'a, str> {
1156        if self.required {
1157            std::borrow::Cow::Owned(format!("{} {marker}", self.label))
1158        } else {
1159            std::borrow::Cow::Borrowed(self.label)
1160        }
1161    }
1162
1163    /// The shared body of the two constructors that take options.
1164    ///
1165    /// Private, and keyed on the kind rather than exposed, because the two
1166    /// public names are the point: a call site says which question it is
1167    /// asking, not which flag it is setting.
1168    const fn offering(
1169        kind: FieldKind,
1170        name: &'a str,
1171        label: &'a str,
1172        options: &'a [Choice<'a>],
1173    ) -> Self {
1174        Self {
1175            options,
1176            ..Self::new(kind, name, label)
1177        }
1178    }
1179
1180    /// Whether the field is currently reporting a problem.
1181    ///
1182    /// Read this rather than testing `error.is_some()` at each renderer: the
1183    /// error state has to mark the field's whole group and not only the
1184    /// message, because a renderer with no descendant selectors (egui, a
1185    /// terminal) cannot find the group from the message. goingson already marks
1186    /// the group and Balanced Breakfast does not, so goingson's shape is the
1187    /// one taken here.
1188    ///
1189    /// [`note`](Self::note) is deliberately not consulted. A note says the
1190    /// answer costs something, not that it is unacceptable, and a field the
1191    /// user may submit as it stands is not invalid.
1192    #[must_use]
1193    pub const fn invalid(&self) -> bool {
1194        self.error.is_some()
1195    }
1196
1197    /// Whether the field carries both ends of its extent.
1198    ///
1199    /// Only [`FieldKind::Range`] owes them, and it owes them absolutely: a
1200    /// slider with one end missing has no extent to draw. Named here rather
1201    /// than left to each renderer to test `min.is_some() && max.is_some()`,
1202    /// which is three renderers arriving at the same condition and one of them
1203    /// getting it wrong, and named as a question about the *field* rather than
1204    /// about the kind because the kind cannot see the bounds.
1205    ///
1206    /// It is a check and not a guarantee. Nothing here refuses to build an
1207    /// unbounded range ([`Field::range`] is what makes the bounded one easy)
1208    /// so a renderer asks this and falls back to whatever its host does
1209    /// honestly with a number.
1210    #[must_use]
1211    pub const fn bounded(&self) -> bool {
1212        self.min.is_some() && self.max.is_some()
1213    }
1214
1215    /// Whether anything in [`accept`](Self::accept) names a media family.
1216    ///
1217    /// The question a renderer asks before it decides to keep room for a
1218    /// preview, and it is deliberately the *whole list* rather than one entry:
1219    /// the media dropzone this was measured against takes `image/*,video/*`, so
1220    /// there is no single family to return and there is still a disclosure to
1221    /// offer. Which one it turns out to be is known once a file is picked, which
1222    /// is renderer-side and after the description is gone.
1223    ///
1224    /// False for an empty list, for a list of suffixes, and for `text/csv`. A
1225    /// renderer that wants the family of a particular entry reads
1226    /// [`Accepted::family`].
1227    #[must_use]
1228    pub fn accepts_media(&self) -> bool {
1229        self.accept.iter().any(|one| one.family().is_some())
1230    }
1231}
1232
1233/// What a field holds now, as a renderer reads it.
1234///
1235/// The one value type every renderer draws from. Absent from [`Field`] on
1236/// purpose: the value is renderer state, and a description that carried it
1237/// would be a second source of truth for what the user typed.
1238///
1239/// An enum rather than a bag of optional fields, for the reason [`Depth`] is
1240/// one: a checkbox holding a string is unsayable here.
1241///
1242/// [`Depth`]: crate::Depth
1243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1244#[non_exhaustive]
1245pub enum Value<'a> {
1246    /// Nothing yet.
1247    #[default]
1248    Absent,
1249    /// The value of anything that takes typed text, a select included: what a
1250    /// select holds is the `value` of one of [`Field::options`]'s choices. The
1251    /// options stay the field's, which is what keeps a `Chosen { options,
1252    /// value }` variant from existing.
1253    Text(&'a str),
1254    /// A checkbox, on or off.
1255    On(bool),
1256    /// Both ends of a [`FieldKind::Interval`], lower first.
1257    ///
1258    /// Two values rather than one string with a separator: an interval submits
1259    /// under two names, so it comes back as two values, and a delimiter chosen
1260    /// here could appear inside either. Either end may be empty while the
1261    /// other stands: "over 120 BPM" is an answer, not a half-filled form.
1262    Between {
1263        /// What the lower box holds now.
1264        lower: &'a str,
1265        /// What the upper box holds now.
1266        upper: &'a str,
1267    },
1268}
1269
1270impl<'a> Value<'a> {
1271    /// The value as text, for the kinds that submit one: the lower end of an
1272    /// interval, and empty for a checkbox or nothing.
1273    #[must_use]
1274    pub const fn as_text(&self) -> &'a str {
1275        match self {
1276            Self::Text(text) | Self::Between { lower: text, .. } => text,
1277            Self::Absent | Self::On(_) => "",
1278        }
1279    }
1280
1281    /// The upper end, for the one variant that has one.
1282    #[must_use]
1283    pub const fn upper_text(&self) -> &'a str {
1284        match self {
1285            Self::Between { upper, .. } => upper,
1286            Self::Absent | Self::Text(_) | Self::On(_) => "",
1287        }
1288    }
1289}