Skip to main content

makeover_layout/
field.rs

1// Names this module's prose links to, resolved for rustdoc.
2#[allow(unused_imports)]
3use crate::{Awaiting, Fill, ThemeVariant};
4
5/// What kind of value a form field takes.
6///
7/// The union of the two vocabularies that diverged, which is what triggered
8/// this crate. They have since converged on their own: both apps now have a
9/// `renderFormField` emitting the same anatomy, and what is left differing is
10/// the kind set, the error shape, and whether the return is a string or a node.
11///
12/// Validation is deliberately absent. Neither app has a shared story (goingson
13/// validates after collecting the form data, with per-field transform hooks;
14/// Balanced Breakfast has `required` and nothing else), and a schema that
15/// describes fields but not constraints acquires a constraint layer per app,
16/// which is exactly how the current divergence started. Naming it absent is a
17/// decision; leaving it unmentioned would not be.
18/// `#[non_exhaustive]` for the reason [`Fill`] is: renderers match on this and
19/// the set keeps growing, so growth must not be a lockstep event.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum FieldKind {
23    /// A single line of text.
24    Text,
25    /// A single line of text that must never be echoed, logged or round-tripped
26    /// through anything that might persist it.
27    Secret,
28    /// A number.
29    Number,
30    /// A number inside bounds the user drags across, where the range being
31    /// visible is the point.
32    ///
33    /// Not [`Number`](Self::Number) with a field's `min` and `max`, which is
34    /// the reading to resist and is the same resistance [`Radio`](Self::Radio)
35    /// needed against `Select`. A bounded number and a validated number are
36    /// different *questions*. A validated
37    /// number is typed and can be wrong: the bounds are a rule the answer is
38    /// checked against, and being told "must be at least 1" afterwards is the
39    /// normal course of it. A range cannot be out of range at all, because the
40    /// bounds are the control's extent rather than a rule, and the two ends are
41    /// what the question means, audiofiles asks for a classifier threshold
42    /// between 0 and 1, where 0 is never and 1 is only-on-certainty, and a typed
43    /// 0.72 says nothing without both ends on screen beside it.
44    ///
45    /// A renderer cannot infer which one is meant from `min`/`max` alone, which
46    /// is why this is a kind and not an inference: goingson's `min="1"` duration
47    /// is a validated number and would become a slider.
48    ///
49    /// The membership test passes without stretching: a webview emits
50    /// `<input type="range">`, egui has `Slider`, a terminal draws a bar and
51    /// takes arrow keys, a CLI takes a bounded argument.
52    ///
53    /// # It owes its bounds
54    ///
55    /// A field's `min` and `max` are optional for every other kind and are
56    /// **required** here, in the sense the description can require anything:
57    /// the field's `bounded` is the check, and a range missing one has no
58    /// extent for a renderer to draw. What a renderer does with an unbounded
59    /// range is its own call and both answers are honest (fall back to a typed
60    /// number, or pick a host default) so this is stated rather than enforced,
61    /// the way every other constraint here is.
62    ///
63    /// The granularity is the third fact and is genuinely optional: absent, the
64    /// host's own stands.
65    Range,
66    /// One question with two ends: a lower value and an upper one, submitted
67    /// under two names.
68    ///
69    /// "Show me samples between 90 and 130 BPM" has a single answer with two
70    /// ends, and the ends constrain each other: a minimum above the maximum is
71    /// not a wrong value, it is an empty result nobody asked for. Described as
72    /// two [`Number`](Self::Number) fields that is unsayable, nothing says they
73    /// are one question, so a renderer draws two controls with two labels and no
74    /// relationship, and a field's error can only be attached to one side of a
75    /// fault that belongs to both.
76    ///
77    /// Not [`Range`](Self::Range), which was the reading to resist and the
78    /// resistance is the same one `Range` itself needed against `Number`. A
79    /// range describes *one* value inside an extent; this describes two, and the
80    /// extent is a bound on each rather than the question's meaning. The two
81    /// come apart in the answer: a range has a value, an interval has a pair,
82    /// and either end may be absent while the other stands.
83    ///
84    /// # It states both names
85    ///
86    /// The field's `name` is the lower end and its `upper_name` is the upper
87    /// one, stated rather than derived. One member instead of a naming
88    /// convention this crate would then own forever.
89    ///
90    /// Direction is carried by which member the name sits in, so nothing
91    /// separate says which end is which.
92    ///
93    /// # What it does not enforce
94    ///
95    /// The crossing rule. A lower end above the upper one is describable here
96    /// and always was, exactly as a number below its `min` is: this
97    /// crate carries constraints and never checks them, and deciding a value is
98    /// wrong stays with whoever validated. What the description buys is that the
99    /// fault now has one place to be reported rather than two.
100    ///
101    /// # Both ends take the same facts
102    ///
103    /// `min`, `max`, `step` and `unit` describe the axis rather than one end of
104    /// it, so they are read once and applied to both. Six of audiofiles' filter axes
105    /// are exactly this: one extent, one unit, one granularity, two ends.
106    ///
107    /// The bounds are optional here, unlike `Range`. They are a rule the answer
108    /// is checked against rather than the control's extent, which is
109    /// [`Number`](Self::Number)'s arrangement and not a slider's.
110    Interval,
111    /// An email address.
112    ///
113    /// Distinct from [`Text`](Self::Text) because the distinction is not
114    /// decoration: a webview renderer emits `type="email"`, which on a touch
115    /// device changes the keyboard that appears and turns on the platform's own
116    /// validation. goingson ships to iOS, so collapsing this into text costs a
117    /// keyboard with no `@` on it.
118    Email,
119    /// A URL. Same reasoning as [`Email`](Self::Email).
120    Url,
121    /// A telephone number. Same reasoning as [`Email`](Self::Email), and the
122    /// clearest case of it: the keyboard is a numeric pad rather than letters.
123    Tel,
124    /// A calendar day, with no time of day in it.
125    ///
126    /// [`Email`](Self::Email)'s argument, and it carries further: a webview
127    /// emits `type="date"`, which is a native picker, the platform's own
128    /// validation, and on a touch device the date keyboard. Described as
129    /// [`Text`](Self::Text) with a hint reading "YYYY-MM-DD", all three are
130    /// lost and the hint is doing the platform's job in prose.
131    ///
132    /// The membership test passes on every host without stretching: a webview
133    /// and a Tauri app emit the input, egui has a date picker, a terminal
134    /// prompts for a day and can validate it, a CLI takes an argument.
135    ///
136    /// # The value is ISO 8601, `YYYY-MM-DD`
137    ///
138    /// Named here rather than left to each host, because a host that picks
139    /// differently sends a server something it parses differently, and the
140    /// failure is silent and per-host. It is `<input type="date">`'s own wire
141    /// format, so the webview renderer owes nothing to honour it and the other
142    /// hosts have one spelling to meet. [`DATE_FORMAT`] is the constant, and a
143    /// test asserts this doc and that constant agree.
144    Date,
145    /// A calendar day and a time of day together.
146    ///
147    /// Apart from [`Date`](Self::Date) because the question is different rather
148    /// than more precise: "which day does this expire" and "at what moment does
149    /// this publish" are asked by different screens and answered by different
150    /// controls. A webview emits `type="datetime-local"` for one and
151    /// `type="date"` for the other, and a host that collapsed them would ask
152    /// half the tree for a precision it does not want.
153    ///
154    /// Both arrived together on measurement rather than on symmetry: 13 sites
155    /// of each across the MNW server and goingson, and **zero** of `time`,
156    /// `month` or `week`, which is why those are not here. A member added for a
157    /// case nobody has is a member designed against nothing, which is
158    /// [`File`](Self::File)'s reasoning about `accept` applied to a whole
159    /// member.
160    ///
161    /// # The value is `YYYY-MM-DDTHH:MM`, local, with no zone
162    ///
163    /// `<input type="datetime-local">`'s own format, and the "local" is the
164    /// load-bearing half: the value carries no offset and no `Z`, so the moment
165    /// it names is only fixed once something supplies a zone. That is the app's
166    /// business and not the description's. Seconds are absent, which is the
167    /// browser's own default and is left as the rule rather than restated as a
168    /// constraint. [`DATETIME_FORMAT`] is the constant.
169    ///
170    /// A field's `min` and `max` already take "the host's own spelling of a
171    /// bound", so a floor of *not in the past* needs nothing new here: it
172    /// is a string in this same format.
173    DateTime,
174    /// Several lines of text.
175    Textarea,
176    /// Several lines of text the user writes markdown in.
177    ///
178    /// The editing counterpart of prose a description carries as markdown
179    /// source, and the reason it can exist at all is the same one that lets the
180    /// source be carried: editing markdown is editing text, so a terminal, an
181    /// immediate-mode host and a webview all have an honest answer, and none of
182    /// them has to refuse. A kind that meant "rich text" in the WYSIWYG sense
183    /// would have been a document model, and two of the three hosts would have
184    /// had to draw something they cannot.
185    ///
186    /// What the mark buys over [`Textarea`](Self::Textarea) is that a renderer
187    /// may offer the affordances markdown has and plain text does not (a
188    /// preview, a syntax pass, a monospaced face for the source) and that a
189    /// host reading the value back knows what it is holding. A renderer with
190    /// none of that draws a textarea, which is why this is additive rather than
191    /// a second control.
192    ///
193    /// It says nothing about **when** the value is saved. Autosave is a clock,
194    /// clocks are not described here, and the four MNW editors this was measured
195    /// against each keep their own.
196    ///
197    /// Sanitising stays where it already is for markdown that is only displayed:
198    /// with the renderer, at the point markup is produced. Being described is
199    /// not a safety property, and a host with its own sanitiser and its own
200    /// content-security posture still owns both.
201    Rich,
202    /// One of a fixed set, offered behind a control that shows one at a time.
203    Select,
204    /// One of a fixed set, with every option on screen at once.
205    ///
206    /// Not a presentation of [`Select`](Self::Select), which is the reading to
207    /// resist: what differs is a property of the *question*. A choice that is
208    /// consequential or irreversible has to be readable without opening
209    /// anything, because a closed control shows one option and hides the rest,
210    /// and the one it shows is whichever was current before the user had read
211    /// the alternatives. audiofiles asks whether a library copies samples into
212    /// its store or references them where they lie (which cannot be changed
213    /// afterwards) and had already promoted that out of a checkbox by hand,
214    /// with a comment giving this reason, before the description could say it.
215    ///
216    /// Everything here is an `<input type=...>`, a `<select>` or a
217    /// `<textarea>`, and the way this enum grows is by a site being measured
218    /// rather than by a list being completed. No member is ever "the last one".
219    Radio,
220    /// Any number of a fixed set, with every option on screen at once.
221    ///
222    /// [`Radio`](Self::Radio)'s shape with the answer widened from one option
223    /// to a set, and not a row of [`Checkbox`](Self::Checkbox)es, which is the
224    /// reading to resist. A row of checkboxes is several questions, each with
225    /// its own name, label and error; this is one question whose answer has
226    /// several parts. The difference shows the moment anything is wrong: "pick
227    /// at least one" belongs to the set, and a row of checkboxes has nowhere to
228    /// put it but under one box.
229    ///
230    /// Four measured sites, all in the MNW server and all written by hand. A
231    /// project's features are asked three times, in its settings and in both
232    /// project wizards. The fourth is the item wizard's bundle picker, which
233    /// joins the ticked ids with commas into a hidden input: the delimiter this
234    /// member exists so that no host has to invent.
235    ///
236    /// # The ticked set is marked on the options
237    ///
238    /// Each ticked option is marked `chosen`. A set is not one string,
239    /// and a value holding one would need a separator that any option's value
240    /// could contain. `chosen` is already how an option list marks itself, so
241    /// a checklist marks as many options as are ticked and needs no second
242    /// spelling.
243    ///
244    /// # It submits the name once per ticked option
245    ///
246    /// HTML's own wire format for a set of checkboxes sharing a name, so a
247    /// webview owes nothing to honour it. An empty set submits nothing under
248    /// the name, and a handler reads an absent name as the empty answer rather
249    /// than as a missing one.
250    Checklist,
251    /// On or off.
252    Checkbox,
253    /// A file the user picks from wherever the host keeps files.
254    ///
255    /// It was filed as a router finding (a control whose destination is a
256    /// host capability rather than an address) and splitting it is what made
257    /// it two answers instead of one member satisfying neither. *Opening* a
258    /// file is a one-way handoff and needs no new API. *Picking* one returns a
259    /// value into a write, which is a form concern, which is this.
260    ///
261    /// The membership test passes on every host and not by a stretch: a Tauri
262    /// app opens a native picker, a server renders `<input type="file">`, a
263    /// terminal prompts for a path, a CLI takes an argument. That is closer to
264    /// [`Email`](Self::Email), which exists because it changes the keyboard,
265    /// than to anything bespoke.
266    ///
267    /// # The four things an upload says, and where each of them lives
268    ///
269    /// | axis | where |
270    /// |---|---|
271    /// | what it accepts | the field's accept list, [`Family`] for a media family |
272    /// | one file or several | the field's `multiple` |
273    /// | where the bytes go | the router's action, not here |
274    /// | how far along it is | [`Awaiting`] on that action |
275    ///
276    /// Only the first two are this crate's, and that split is the answer to
277    /// "describe an upload in full" rather than a gap in it. A destination is an
278    /// address and this crate holds no addresses; progress is a live number and
279    /// a description is built once, so the number is the renderer's to observe
280    /// against the size [`Awaiting::amount`] carried before the transfer began.
281    ///
282    /// # How the file is handed over is the host's
283    ///
284    /// A drop area, a button opening a native picker, a path typed at a prompt:
285    /// all three are the same field, and every measured site has the first. It
286    /// is not described for the reason no gesture is, this crate owns no
287    /// coordinates and no pointer, and a terminal that cannot be dropped on
288    /// would be refusing a description it can otherwise honour completely.
289    ///
290    /// The accept list and `multiple` are measured rather than deferred. A
291    /// member designed against nothing is the rule to keep: count the sites
292    /// before adding one.
293    File,
294    /// Which theme the app wears.
295    ///
296    /// The one member here that names a *subject* rather than a shape of
297    /// answer, and it is worth saying why that is not the door it looks like.
298    /// Every other kind is a question a screen might ask about anything; this
299    /// one is a specific question every app in the family asks, once, on its
300    /// settings screen, and three of them wrote the same control by hand.
301    ///
302    /// # It is furniture, and the measurement is what says so
303    ///
304    /// The reading to resist is that this is [`Select`](Self::Select) with a
305    /// grouped option list. Option lists that group barely occur; the thing
306    /// that recurs is this picker.
307    ///
308    /// # What it carries that a select cannot
309    ///
310    /// A list of themes rather than a list of options, because a theme is four
311    /// facts and an option is two. The two extra facts are the ones no
312    /// app can supply without redoing work the theme layer has already done:
313    /// which [`ThemeVariant`] group a theme is in, and how legible its muted
314    /// text measured. An option labelled `format!("{name} ({variant})")` is
315    /// what the three apps had, and it flattens the group into prose and loses the
316    /// tier entirely.
317    ///
318    /// The field's `follows` carries the entry that is not a theme.
319    ///
320    /// # The cost, stated rather than discovered later
321    ///
322    /// This puts one screen's shape into a vocabulary that otherwise holds
323    /// none, which was the objection raised against it and accepted going in.
324    /// The mitigation is narrowness: this describes a theme picker, not a
325    /// general "list the host resolved" mechanism. A second host-resolved list
326    /// is when that generalisation gets measured, and not before.
327    ///
328    /// A renderer that has not heard of it draws a select over the themes'
329    /// names and loses the grouping, which is the state
330    /// every app was in before this member. Degrading to the status quo ante
331    /// is the floor the member is designed against.
332    Theme,
333    /// Which density this device draws at: what it detects, or the reader's
334    /// choice of [`Density::Pointer`] or [`Density::Touch`].
335    ///
336    /// # The device holds the answer, not the app
337    ///
338    /// Density is a claim about what is pointing at this screen, so the
339    /// choice is the device's the way detection is. A laptop and a tablet
340    /// signed into one account want different answers, and an app storing
341    /// one value would hand the tablet the laptop's. So the field carries no
342    /// value and submits none: the host reads its own store to show the
343    /// current choice, writes it there on a pick, and applies it at once.
344    /// The spelling it stores is [`Density::attribute_value`], so every host
345    /// reading one store agrees, and absence is "follow detection".
346    ///
347    /// A webview keeps it in the browser and writes
348    /// [`DENSITY_ATTRIBUTE`](makeover_geometry::DENSITY_ATTRIBUTE) on the
349    /// root; an egui host keeps it with its own persisted state; a terminal
350    /// has no contact patch to size for and does not draw the field.
351    ///
352    /// Furniture for [`Theme`](Self::Theme)'s reason and on its terms: it
353    /// names one picker and is not a general "setting the host keeps"
354    /// mechanism. A second device-held setting is when that gets measured.
355    ///
356    /// [`Density::Pointer`]: makeover_geometry::Density::Pointer
357    /// [`Density::Touch`]: makeover_geometry::Density::Touch
358    /// [`Density::attribute_value`]: makeover_geometry::Density::attribute_value
359    Density,
360    /// Carried through the form and never shown.
361    Hidden,
362}
363
364/// What a [`FieldKind::Density`] picker offers, in order, and what each entry
365/// is called.
366///
367/// `None` is "follow detection", which is also what an empty store means.
368/// Here rather than in each host because the words are the description's:
369/// a webview's select and an egui combo box offering the same three things
370/// under different names would be two pickers. "Mouse and trackpad" rather
371/// than "Pointer" because the reader is choosing by what is in their hand,
372/// not by the preset's name.
373pub const DENSITY_CHOICES: [(Option<makeover_geometry::Density>, &str); 3] = [
374    (None, "Automatic"),
375    (
376        Some(makeover_geometry::Density::Pointer),
377        "Mouse and trackpad",
378    ),
379    (Some(makeover_geometry::Density::Touch), "Touch"),
380];
381
382/// The wire format a [`FieldKind::Date`] value takes: ISO 8601, `YYYY-MM-DD`.
383///
384/// A constant rather than a sentence in a doc comment, because the reason to
385/// name the format at all is that a host picking its own would fail silently
386/// against a server parsing another. A host that cannot emit the native control
387/// still has one spelling to meet, and can say which one it meant.
388pub const DATE_FORMAT: &str = "%Y-%m-%d";
389
390/// The wire format a [`FieldKind::DateTime`] value takes: `YYYY-MM-DDTHH:MM`,
391/// local, carrying no zone and no seconds.
392///
393/// [`DATE_FORMAT`]'s sibling and there for its reason. The absent zone is a
394/// property of the value rather than an omission: the moment is not fixed until
395/// something outside the description supplies one.
396pub const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M";
397
398impl FieldKind {
399    /// Whether the value the kind takes is a moment rather than a string.
400    ///
401    /// Named once here for the reason [`offers_options`](Self::offers_options)
402    /// is: two kinds answer yes, and a host that has to parse or format a value
403    /// needs to ask without spelling the pair out at each renderer. A third
404    /// temporal kind should land here and nowhere else.
405    ///
406    /// The format each one takes is [`DATE_FORMAT`] and [`DATETIME_FORMAT`].
407    #[must_use]
408    pub const fn temporal(self) -> bool {
409        matches!(self, Self::Date | Self::DateTime)
410    }
411
412    /// Whether the field is drawn at all.
413    #[must_use]
414    pub const fn visible(self) -> bool {
415        !matches!(self, Self::Hidden)
416    }
417
418    /// Whether the value must be kept out of logs and diagnostics.
419    #[must_use]
420    pub const fn confidential(self) -> bool {
421        matches!(self, Self::Secret)
422    }
423
424    /// Where the field's own label sits.
425    ///
426    /// A checkbox labels itself on the right of the box; everything else takes
427    /// a label above. Both webview apps already do this and both special-case
428    /// it inline, which is the tell that it belongs in the description.
429    ///
430    /// A [`Radio`](Self::Radio) is not one of them, and the near-miss is worth
431    /// naming: its *options* each label themselves, but the field still asks a
432    /// question above them, so the group takes a label like everything else.
433    #[must_use]
434    pub const fn labels_itself(self) -> bool {
435        matches!(self, Self::Checkbox)
436    }
437
438    /// Whether the kind reads a field's options.
439    ///
440    /// Three kinds do, so the set is named once here rather than spelled out at
441    /// each renderer and again in the options' own doc, where "every
442    /// kind but `Select`" was true for exactly one release.
443    /// [`Checklist`](Self::Checklist) is the third and landed here and nowhere
444    /// else, which is what this predicate is for.
445    #[must_use]
446    pub const fn offers_options(self) -> bool {
447        matches!(self, Self::Select | Self::Radio | Self::Checklist)
448    }
449
450    /// Whether the answer is a set of the options rather than one of them.
451    ///
452    /// One kind answers yes, and it gets a name for
453    /// [`takes_files`](Self::takes_files)'s reason: every renderer that walks
454    /// a field's options has to ask it before it decides whether an option is
455    /// marked by the field's value or by its own `chosen` alone, and whether
456    /// picking one clears the others.
457    #[must_use]
458    pub const fn takes_several(self) -> bool {
459        matches!(self, Self::Checklist)
460    }
461
462    /// Whether the kind reads a field's `themes` and `follows`.
463    ///
464    /// One member answers yes, and it gets a name for
465    /// [`takes_files`](Self::takes_files)'s reason rather than in spite of
466    /// being alone: four renderers ask it before they read either member, and
467    /// a `matches!` per renderer is where the next one goes missing.
468    ///
469    /// Deliberately not folded into
470    /// [`offers_options`](Self::offers_options). A theme picker offers no
471    /// options at all, so a renderer walking `options` for it walks an
472    /// empty slice and draws an empty control.
473    #[must_use]
474    pub const fn offers_themes(self) -> bool {
475        matches!(self, Self::Theme)
476    }
477
478    /// Whether the host keeps the value rather than the app.
479    ///
480    /// One member answers yes, named for [`takes_files`](Self::takes_files)'s
481    /// reason: a renderer has to ask before it writes a `name`, fills a value
482    /// or submits one, and a field the device holds does none of the three.
483    /// See [`Density`](Self::Density).
484    #[must_use]
485    pub const fn held_by_device(self) -> bool {
486        matches!(self, Self::Density)
487    }
488
489    /// Whether the value runs to more than one line.
490    ///
491    /// Named once here for [`temporal`](Self::temporal)'s reason: two kinds
492    /// answer yes, every renderer has to ask it before it can size anything,
493    /// and a `matches!` per renderer is the pair drifting apart one member at a
494    /// time. What a host does with the markdown, if anything, it reads from the
495    /// kind itself; this is only whether one line is enough.
496    #[must_use]
497    pub const fn multiline(self) -> bool {
498        matches!(self, Self::Textarea | Self::Rich)
499    }
500
501    /// Whether the value is a file the host picks rather than a string typed
502    /// into a box.
503    ///
504    /// One member answers yes, which is [`visible`](Self::visible)'s and
505    /// [`confidential`](Self::confidential)'s footing rather than a departure
506    /// from it: the question gets a name because three renderers ask it before
507    /// they can read a field's accept list or `multiple`, and a `matches!`
508    /// per renderer is where a second file-taking kind would go missing.
509    #[must_use]
510    pub const fn takes_files(self) -> bool {
511        matches!(self, Self::File)
512    }
513
514    /// Whether the value is a quantity, so a field's `unit` means something.
515    ///
516    /// The numeric kinds and nothing else. A date is a quantity in the sense
517    /// that it is ordered, and it is not one in the sense that matters here:
518    /// its unit is fixed by the kind, so `Date` carrying `days` would be the
519    /// description restating what the kind already said.
520    ///
521    /// [`takes_files`](Self::takes_files)'s footing, and for its reason: the
522    /// renderers ask this before they decide where a unit goes, and a
523    /// `matches!` per renderer is where the next measurable kind goes missing.
524    ///
525    /// [`Interval`](Self::Interval) is measurable too: an axis is measured in
526    /// something and both its ends are in it.
527    #[must_use]
528    pub const fn measurable(self) -> bool {
529        matches!(self, Self::Number | Self::Range | Self::Interval)
530    }
531}
532
533/// A family of media a file can belong to.
534///
535/// Three members, because three is what a media type's own first segment offers
536/// that a renderer can do anything with. `text` and `application` are families
537/// too and neither buys a disclosure (there is no preview of an
538/// `application/octet-stream`) so naming them would be a member added for a
539/// case nobody has.
540///
541/// It is the answer to "which disclosure", not a validation rule.
542/// A field's accept list is what a host filters on.
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
544#[non_exhaustive]
545pub enum Family {
546    /// A still picture.
547    Image,
548    /// Sound.
549    Audio,
550    /// Moving pictures, with or without sound.
551    Video,
552}
553
554impl Family {
555    /// The wildcard media type that means the whole family.
556    ///
557    /// `image/*` and its two siblings, which is what the measured sites write
558    /// and what a webview puts in an `accept` attribute. Named here so the three
559    /// renderers do not each spell the star.
560    #[must_use]
561    pub const fn wildcard(self) -> &'static str {
562        match self {
563            Self::Image => "image/*",
564            Self::Audio => "audio/*",
565            Self::Video => "video/*",
566        }
567    }
568
569    /// The family a media type's first segment names, if it is one of these.
570    ///
571    /// Case-insensitive on the segment, because a media type is
572    /// case-insensitive and half the tree writes them lowercase by habit rather
573    /// than by rule.
574    #[must_use]
575    pub fn of_type(media_type: &str) -> Option<Self> {
576        let (top, _) = media_type.split_once('/')?;
577        if top.eq_ignore_ascii_case("image") {
578            Some(Self::Image)
579        } else if top.eq_ignore_ascii_case("audio") {
580            Some(Self::Audio)
581        } else if top.eq_ignore_ascii_case("video") {
582            Some(Self::Video)
583        } else {
584            None
585        }
586    }
587}
588
589/// What a field holds now, as a renderer reads it.
590///
591/// The one value type every renderer draws from. Absent from a field's
592/// description on purpose: the value is renderer state, and a description that
593/// carried it would be a second source of truth for what the user typed.
594///
595/// An enum rather than a bag of optional fields, for the reason [`Depth`] is
596/// one: a checkbox holding a string is unsayable here.
597///
598/// [`Depth`]: crate::Depth
599#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
600#[non_exhaustive]
601pub enum Value<'a> {
602    /// Nothing yet.
603    #[default]
604    Absent,
605    /// The value of anything that takes typed text, a select included: what a
606    /// select holds is the `value` of one of the field's options. The options
607    /// stay the field's, which is what keeps a `Chosen { options, value }`
608    /// variant from existing.
609    Text(&'a str),
610    /// A checkbox, on or off.
611    On(bool),
612    /// Both ends of a [`FieldKind::Interval`], lower first.
613    ///
614    /// Two values rather than one string with a separator: an interval submits
615    /// under two names, so it comes back as two values, and a delimiter chosen
616    /// here could appear inside either. Either end may be empty while the
617    /// other stands: "over 120 BPM" is an answer, not a half-filled form.
618    Between {
619        /// What the lower box holds now.
620        lower: &'a str,
621        /// What the upper box holds now.
622        upper: &'a str,
623    },
624}
625
626impl<'a> Value<'a> {
627    /// The value as text, for the kinds that submit one: the lower end of an
628    /// interval, and empty for a checkbox or nothing.
629    #[must_use]
630    pub const fn as_text(&self) -> &'a str {
631        match self {
632            Self::Text(text) | Self::Between { lower: text, .. } => text,
633            Self::Absent | Self::On(_) => "",
634        }
635    }
636
637    /// The upper end, for the one variant that has one.
638    #[must_use]
639    pub const fn upper_text(&self) -> &'a str {
640        match self {
641            Self::Between { upper, .. } => upper,
642            Self::Absent | Self::Text(_) | Self::On(_) => "",
643        }
644    }
645}