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 [Self::Image, Self::Audio, Self::Video]
578 .into_iter()
579 .find(|family| {
580 family
581 .wildcard()
582 .strip_suffix("/*")
583 .is_some_and(|t| t.eq_ignore_ascii_case(top))
584 })
585 }
586}
587
588/// What a field holds now, as a renderer reads it.
589///
590/// The one value type every renderer draws from. Absent from a field's
591/// description on purpose: the value is renderer state, and a description that
592/// carried it would be a second source of truth for what the user typed.
593///
594/// An enum rather than a bag of optional fields, for the reason [`Depth`] is
595/// one: a checkbox holding a string is unsayable here.
596///
597/// [`Depth`]: crate::Depth
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
599#[non_exhaustive]
600pub enum Value<'a> {
601 /// Nothing yet.
602 #[default]
603 Absent,
604 /// The value of anything that takes typed text, a select included: what a
605 /// select holds is the `value` of one of the field's options. The options
606 /// stay the field's, which is what keeps a `Chosen { options, value }`
607 /// variant from existing.
608 Text(&'a str),
609 /// A checkbox, on or off.
610 On(bool),
611 /// Both ends of a [`FieldKind::Interval`], lower first.
612 ///
613 /// Two values rather than one string with a separator: an interval submits
614 /// under two names, so it comes back as two values, and a delimiter chosen
615 /// here could appear inside either. Either end may be empty while the
616 /// other stands: "over 120 BPM" is an answer, not a half-filled form.
617 Between {
618 /// What the lower box holds now.
619 lower: &'a str,
620 /// What the upper box holds now.
621 upper: &'a str,
622 },
623}
624
625impl<'a> Value<'a> {
626 /// The value as text, for the kinds that submit one: the lower end of an
627 /// interval, and empty for a checkbox or nothing.
628 #[must_use]
629 pub const fn as_text(&self) -> &'a str {
630 match self {
631 Self::Text(text) | Self::Between { lower: text, .. } => text,
632 Self::Absent | Self::On(_) => "",
633 }
634 }
635
636 /// The upper end, for the one variant that has one.
637 #[must_use]
638 pub const fn upper_text(&self) -> &'a str {
639 match self {
640 Self::Between { upper, .. } => upper,
641 Self::Absent | Self::Text(_) | Self::On(_) => "",
642 }
643 }
644}