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 /// Carried through the form and never shown.
334 Hidden,
335}
336
337/// The wire format a [`FieldKind::Date`] value takes: ISO 8601, `YYYY-MM-DD`.
338///
339/// A constant rather than a sentence in a doc comment, because the reason to
340/// name the format at all is that a host picking its own would fail silently
341/// against a server parsing another. A host that cannot emit the native control
342/// still has one spelling to meet, and can say which one it meant.
343pub const DATE_FORMAT: &str = "%Y-%m-%d";
344
345/// The wire format a [`FieldKind::DateTime`] value takes: `YYYY-MM-DDTHH:MM`,
346/// local, carrying no zone and no seconds.
347///
348/// [`DATE_FORMAT`]'s sibling and there for its reason. The absent zone is a
349/// property of the value rather than an omission: the moment is not fixed until
350/// something outside the description supplies one.
351pub const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M";
352
353impl FieldKind {
354 /// Whether the value the kind takes is a moment rather than a string.
355 ///
356 /// Named once here for the reason [`offers_options`](Self::offers_options)
357 /// is: two kinds answer yes, and a host that has to parse or format a value
358 /// needs to ask without spelling the pair out at each renderer. A third
359 /// temporal kind should land here and nowhere else.
360 ///
361 /// The format each one takes is [`DATE_FORMAT`] and [`DATETIME_FORMAT`].
362 #[must_use]
363 pub const fn temporal(self) -> bool {
364 matches!(self, Self::Date | Self::DateTime)
365 }
366
367 /// Whether the field is drawn at all.
368 #[must_use]
369 pub const fn visible(self) -> bool {
370 !matches!(self, Self::Hidden)
371 }
372
373 /// Whether the value must be kept out of logs and diagnostics.
374 #[must_use]
375 pub const fn confidential(self) -> bool {
376 matches!(self, Self::Secret)
377 }
378
379 /// Where the field's own label sits.
380 ///
381 /// A checkbox labels itself on the right of the box; everything else takes
382 /// a label above. Both webview apps already do this and both special-case
383 /// it inline, which is the tell that it belongs in the description.
384 ///
385 /// A [`Radio`](Self::Radio) is not one of them, and the near-miss is worth
386 /// naming: its *options* each label themselves, but the field still asks a
387 /// question above them, so the group takes a label like everything else.
388 #[must_use]
389 pub const fn labels_itself(self) -> bool {
390 matches!(self, Self::Checkbox)
391 }
392
393 /// Whether the kind reads a field's options.
394 ///
395 /// Three kinds do, so the set is named once here rather than spelled out at
396 /// each renderer and again in the options' own doc, where "every
397 /// kind but `Select`" was true for exactly one release.
398 /// [`Checklist`](Self::Checklist) is the third and landed here and nowhere
399 /// else, which is what this predicate is for.
400 #[must_use]
401 pub const fn offers_options(self) -> bool {
402 matches!(self, Self::Select | Self::Radio | Self::Checklist)
403 }
404
405 /// Whether the answer is a set of the options rather than one of them.
406 ///
407 /// One kind answers yes, and it gets a name for
408 /// [`takes_files`](Self::takes_files)'s reason: every renderer that walks
409 /// a field's options has to ask it before it decides whether an option is
410 /// marked by the field's value or by its own `chosen` alone, and whether
411 /// picking one clears the others.
412 #[must_use]
413 pub const fn takes_several(self) -> bool {
414 matches!(self, Self::Checklist)
415 }
416
417 /// Whether the kind reads a field's `themes` and `follows`.
418 ///
419 /// One member answers yes, and it gets a name for
420 /// [`takes_files`](Self::takes_files)'s reason rather than in spite of
421 /// being alone: four renderers ask it before they read either member, and
422 /// a `matches!` per renderer is where the next one goes missing.
423 ///
424 /// Deliberately not folded into
425 /// [`offers_options`](Self::offers_options). A theme picker offers no
426 /// options at all, so a renderer walking `options` for it walks an
427 /// empty slice and draws an empty control.
428 #[must_use]
429 pub const fn offers_themes(self) -> bool {
430 matches!(self, Self::Theme)
431 }
432
433 /// Whether the value runs to more than one line.
434 ///
435 /// Named once here for [`temporal`](Self::temporal)'s reason: two kinds
436 /// answer yes, every renderer has to ask it before it can size anything,
437 /// and a `matches!` per renderer is the pair drifting apart one member at a
438 /// time. What a host does with the markdown, if anything, it reads from the
439 /// kind itself; this is only whether one line is enough.
440 #[must_use]
441 pub const fn multiline(self) -> bool {
442 matches!(self, Self::Textarea | Self::Rich)
443 }
444
445 /// Whether the value is a file the host picks rather than a string typed
446 /// into a box.
447 ///
448 /// One member answers yes, which is [`visible`](Self::visible)'s and
449 /// [`confidential`](Self::confidential)'s footing rather than a departure
450 /// from it: the question gets a name because three renderers ask it before
451 /// they can read a field's accept list or `multiple`, and a `matches!`
452 /// per renderer is where a second file-taking kind would go missing.
453 #[must_use]
454 pub const fn takes_files(self) -> bool {
455 matches!(self, Self::File)
456 }
457
458 /// Whether the value is a quantity, so a field's `unit` means something.
459 ///
460 /// The numeric kinds and nothing else. A date is a quantity in the sense
461 /// that it is ordered, and it is not one in the sense that matters here:
462 /// its unit is fixed by the kind, so `Date` carrying `days` would be the
463 /// description restating what the kind already said.
464 ///
465 /// [`takes_files`](Self::takes_files)'s footing, and for its reason: the
466 /// renderers ask this before they decide where a unit goes, and a
467 /// `matches!` per renderer is where the next measurable kind goes missing.
468 ///
469 /// [`Interval`](Self::Interval) is measurable too: an axis is measured in
470 /// something and both its ends are in it.
471 #[must_use]
472 pub const fn measurable(self) -> bool {
473 matches!(self, Self::Number | Self::Range | Self::Interval)
474 }
475}
476
477/// A family of media a file can belong to.
478///
479/// Three members, because three is what a media type's own first segment offers
480/// that a renderer can do anything with. `text` and `application` are families
481/// too and neither buys a disclosure (there is no preview of an
482/// `application/octet-stream`) so naming them would be a member added for a
483/// case nobody has.
484///
485/// It is the answer to "which disclosure", not a validation rule.
486/// A field's accept list is what a host filters on.
487#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
488#[non_exhaustive]
489pub enum Family {
490 /// A still picture.
491 Image,
492 /// Sound.
493 Audio,
494 /// Moving pictures, with or without sound.
495 Video,
496}
497
498impl Family {
499 /// The wildcard media type that means the whole family.
500 ///
501 /// `image/*` and its two siblings, which is what the measured sites write
502 /// and what a webview puts in an `accept` attribute. Named here so the three
503 /// renderers do not each spell the star.
504 #[must_use]
505 pub const fn wildcard(self) -> &'static str {
506 match self {
507 Self::Image => "image/*",
508 Self::Audio => "audio/*",
509 Self::Video => "video/*",
510 }
511 }
512
513 /// The family a media type's first segment names, if it is one of these.
514 ///
515 /// Case-insensitive on the segment, because a media type is
516 /// case-insensitive and half the tree writes them lowercase by habit rather
517 /// than by rule.
518 #[must_use]
519 pub fn of_type(media_type: &str) -> Option<Self> {
520 let (top, _) = media_type.split_once('/')?;
521 if top.eq_ignore_ascii_case("image") {
522 Some(Self::Image)
523 } else if top.eq_ignore_ascii_case("audio") {
524 Some(Self::Audio)
525 } else if top.eq_ignore_ascii_case("video") {
526 Some(Self::Video)
527 } else {
528 None
529 }
530 }
531}
532
533/// What a field holds now, as a renderer reads it.
534///
535/// The one value type every renderer draws from. Absent from a field's
536/// description on purpose: the value is renderer state, and a description that
537/// carried it would be a second source of truth for what the user typed.
538///
539/// An enum rather than a bag of optional fields, for the reason [`Depth`] is
540/// one: a checkbox holding a string is unsayable here.
541///
542/// [`Depth`]: crate::Depth
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
544#[non_exhaustive]
545pub enum Value<'a> {
546 /// Nothing yet.
547 #[default]
548 Absent,
549 /// The value of anything that takes typed text, a select included: what a
550 /// select holds is the `value` of one of the field's options. The options
551 /// stay the field's, which is what keeps a `Chosen { options, value }`
552 /// variant from existing.
553 Text(&'a str),
554 /// A checkbox, on or off.
555 On(bool),
556 /// Both ends of a [`FieldKind::Interval`], lower first.
557 ///
558 /// Two values rather than one string with a separator: an interval submits
559 /// under two names, so it comes back as two values, and a delimiter chosen
560 /// here could appear inside either. Either end may be empty while the
561 /// other stands: "over 120 BPM" is an answer, not a half-filled form.
562 Between {
563 /// What the lower box holds now.
564 lower: &'a str,
565 /// What the upper box holds now.
566 upper: &'a str,
567 },
568}
569
570impl<'a> Value<'a> {
571 /// The value as text, for the kinds that submit one: the lower end of an
572 /// interval, and empty for a checkbox or nothing.
573 #[must_use]
574 pub const fn as_text(&self) -> &'a str {
575 match self {
576 Self::Text(text) | Self::Between { lower: text, .. } => text,
577 Self::Absent | Self::On(_) => "",
578 }
579 }
580
581 /// The upper end, for the one variant that has one.
582 #[must_use]
583 pub const fn upper_text(&self) -> &'a str {
584 match self {
585 Self::Between { upper, .. } => upper,
586 Self::Absent | Self::Text(_) | Self::On(_) => "",
587 }
588 }
589}