use crate::{Choice, Curve, ThemeChoice, Tone}; // Names this module's prose links to, resolved for rustdoc. #[allow(unused_imports)] use crate::{Awaiting, Contrast, Fill, ThemeVariant}; /// What kind of value a form field takes. /// /// The union of the two vocabularies that diverged, which is what triggered /// this crate. They have since converged on their own: both apps now have a /// `renderFormField` emitting the same anatomy, and what is left differing is /// the kind set, the error shape, and whether the return is a string or a node. /// /// Validation is deliberately absent. Neither app has a shared story (goingson /// validates after collecting the form data, with per-field transform hooks; /// Balanced Breakfast has `required` and nothing else), and a schema that /// describes fields but not constraints acquires a constraint layer per app, /// which is exactly how the current divergence started. Naming it absent is a /// decision; leaving it unmentioned would not be. /// `#[non_exhaustive]` for the reason [`Fill`] is: renderers match on this and /// the set keeps growing, so growth must not be a lockstep event. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum FieldKind { /// A single line of text. Text, /// A single line of text that must never be echoed, logged or round-tripped /// through anything that might persist it. Secret, /// A number. Number, /// A number inside bounds the user drags across, where the range being /// visible is the point. /// /// Not [`Number`](Self::Number) with [`min`](Field::min) and /// [`max`](Field::max), which is the reading to resist and is the same /// resistance [`Radio`](Self::Radio) needed against `Select`. A bounded /// number and a validated number are different *questions*. A validated /// number is typed and can be wrong: the bounds are a rule the answer is /// checked against, and being told "must be at least 1" afterwards is the /// normal course of it. A range cannot be out of range at all, because the /// bounds are the control's extent rather than a rule, and the two ends are /// what the question means — audiofiles asks for a classifier threshold /// between 0 and 1, where 0 is never and 1 is only-on-certainty, and a typed /// 0.72 says nothing without both ends on screen beside it. /// /// A renderer cannot infer which one is meant from `min`/`max` alone, which /// is why this is a kind and not an inference: goingson's `min="1"` duration /// is a validated number and would become a slider. /// /// The membership test passes without stretching: a webview emits /// ``, egui has `Slider`, a terminal draws a bar and /// takes arrow keys, a CLI takes a bounded argument. /// /// # It owes its bounds /// /// [`min`](Field::min) and [`max`](Field::max) are `Option` for every other /// kind and are **required** here, in the sense the description can require /// anything: [`Field::bounded`] is the check, and a range missing one has no /// extent for a renderer to draw. What a renderer does with an unbounded /// range is its own call and both answers are honest — fall back to a typed /// number, or pick a host default — so this is stated rather than enforced, /// the way every other constraint here is. /// /// [`Field::step`] is the third fact and is genuinely optional: absent, the /// host's own granularity stands. Range, /// One question with two ends: a lower value and an upper one, submitted /// under two names. /// /// "Show me samples between 90 and 130 BPM" has a single answer with two /// ends, and the ends constrain each other: a minimum above the maximum is /// not a wrong value, it is an empty result nobody asked for. Described as /// two [`Number`](Self::Number) fields that is unsayable — nothing says they /// are one question, so a renderer draws two controls with two labels and no /// relationship, and [`Field::error`] can only be attached to one side of a /// fault that belongs to both. /// /// Not [`Range`](Self::Range), which was the reading to resist and the /// resistance is the same one `Range` itself needed against `Number`. A /// range describes *one* value inside an extent; this describes two, and the /// extent is a bound on each rather than the question's meaning. The two /// come apart in the answer: a range has a value, an interval has a pair, /// and either end may be absent while the other stands. /// /// # It states both names /// /// [`Field::name`] is the lower end and [`Field::upper_name`] is the upper /// one, stated rather than derived. One member instead of a naming /// convention this crate would then own forever. /// /// Direction is carried by which member the name sits in, so nothing /// separate says which end is which. /// /// # What it does not enforce /// /// The crossing rule. A lower end above the upper one is describable here /// and always was, exactly as an out-of-[`min`](Field::min) number is: this /// crate carries constraints and never checks them, and deciding a value is /// wrong stays with whoever validated. What the description buys is that the /// fault now has one place to be reported rather than two. /// /// # Both ends take the same facts /// /// [`min`](Field::min), [`max`](Field::max), [`step`](Field::step) and /// [`unit`](Field::unit) describe the axis rather than one end of it, so /// they are read once and applied to both. Six of audiofiles' filter axes /// are exactly this: one extent, one unit, one granularity, two ends. /// /// The bounds are optional here, unlike `Range`. They are a rule the answer /// is checked against rather than the control's extent, which is /// [`Number`](Self::Number)'s arrangement and not a slider's. Interval, /// An email address. /// /// Distinct from [`Text`](Self::Text) because the distinction is not /// decoration: a webview renderer emits `type="email"`, which on a touch /// device changes the keyboard that appears and turns on the platform's own /// validation. goingson ships to iOS, so collapsing this into text costs a /// keyboard with no `@` on it. Email, /// A URL. Same reasoning as [`Email`](Self::Email). Url, /// A telephone number. Same reasoning as [`Email`](Self::Email), and the /// clearest case of it: the keyboard is a numeric pad rather than letters. Tel, /// A calendar day, with no time of day in it. /// /// [`Email`](Self::Email)'s argument, and it carries further: a webview /// emits `type="date"`, which is a native picker, the platform's own /// validation, and on a touch device the date keyboard. Described as /// [`Text`](Self::Text) with a hint reading "YYYY-MM-DD", all three are /// lost and the hint is doing the platform's job in prose. /// /// The membership test passes on every host without stretching: a webview /// and a Tauri app emit the input, egui has a date picker, a terminal /// prompts for a day and can validate it, a CLI takes an argument. /// /// # The value is ISO 8601, `YYYY-MM-DD` /// /// Named here rather than left to each host, because a host that picks /// differently sends a server something it parses differently, and the /// failure is silent and per-host. It is ``'s own wire /// format, so the webview renderer owes nothing to honour it and the other /// hosts have one spelling to meet. [`DATE_FORMAT`] is the constant, and a /// test asserts this doc and that constant agree. Date, /// A calendar day and a time of day together. /// /// Apart from [`Date`](Self::Date) because the question is different rather /// than more precise: "which day does this expire" and "at what moment does /// this publish" are asked by different screens and answered by different /// controls. A webview emits `type="datetime-local"` for one and /// `type="date"` for the other, and a host that collapsed them would ask /// half the tree for a precision it does not want. /// /// Both arrived together on measurement rather than on symmetry: 13 sites /// of each across the MNW server and goingson, and **zero** of `time`, /// `month` or `week`, which is why those are not here. A member added for a /// case nobody has is a member designed against nothing, which is /// [`File`](Self::File)'s reasoning about `accept` applied to a whole /// member. /// /// # The value is `YYYY-MM-DDTHH:MM`, local, with no zone /// /// ``'s own format, and the "local" is the /// load-bearing half: the value carries no offset and no `Z`, so the moment /// it names is only fixed once something supplies a zone. That is the app's /// business and not the description's. Seconds are absent, which is the /// browser's own default and is left as the rule rather than restated as a /// constraint. [`DATETIME_FORMAT`] is the constant. /// /// [`Field::min`] and [`Field::max`] already take "the host's own spelling /// of a bound", so a floor of *not in the past* needs nothing new here: it /// is a string in this same format. DateTime, /// Several lines of text. Textarea, /// Several lines of text the user writes markdown in. /// /// The editing counterpart of prose a description carries as markdown /// source, and the reason it can exist at all is the same one that lets the /// source be carried: editing markdown is editing text, so a terminal, an /// immediate-mode host and a webview all have an honest answer, and none of /// them has to refuse. A kind that meant "rich text" in the WYSIWYG sense /// would have been a document model, and two of the three hosts would have /// had to draw something they cannot. /// /// What the mark buys over [`Textarea`](Self::Textarea) is that a renderer /// may offer the affordances markdown has and plain text does not — a /// preview, a syntax pass, a monospaced face for the source — and that a /// host reading the value back knows what it is holding. A renderer with /// none of that draws a textarea, which is why this is additive rather than /// a second control. /// /// It says nothing about **when** the value is saved. Autosave is a clock, /// clocks are not described here, and the four MNW editors this was measured /// against each keep their own. /// /// Sanitising stays where it already is for markdown that is only displayed: /// with the renderer, at the point markup is produced. Being described is /// not a safety property, and a host with its own sanitiser and its own /// content-security posture still owns both. Rich, /// One of a fixed set, offered behind a control that shows one at a time. Select, /// One of a fixed set, with every option on screen at once. /// /// Not a presentation of [`Select`](Self::Select), which is the reading to /// resist: what differs is a property of the *question*. A choice that is /// consequential or irreversible has to be readable without opening /// anything, because a closed control shows one option and hides the rest, /// and the one it shows is whichever was current before the user had read /// the alternatives. audiofiles asks whether a library copies samples into /// its store or references them where they lie — which cannot be changed /// afterwards — and had already promoted that out of a checkbox by hand, /// with a comment giving this reason, before the description could say it. /// /// Everything here is an ``, a `