// Names this module's prose links to, resolved for rustdoc.
#[allow(unused_imports)]
use crate::{Field, FieldKind, Unit};
/// One option offered by a field [`FieldKind::offers_options`] accepts.
///
/// Two strings, because the submitted value and the read label are different
/// facts and every renderer that has tried to collapse them has had to
/// un-collapse them later. `makeover-webview` invented this shape writing its
/// form emitter and it is taken here unchanged; moving it down rather than
/// re-deriving it is the point, since the second and third renderers were each
/// going to arrive at a near-miss of it.
/// `#[non_exhaustive]`, which every type here that a renderer matches or builds
/// carries. Without it a new member is a breaking change at every literal site
/// in the tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Choice<'a> {
/// What is submitted.
pub value: &'a str,
/// What is read.
pub label: &'a str,
/// Why it cannot be picked right now, when it cannot.
///
/// One member rather than an `available: bool` beside a reason, and the
/// conflation is the point: an option greyed out with no explanation is a
/// dead end the user cannot act on, and it is exactly the state the app
/// that found this gap had to patch by hand with a line of prose under the
/// control. Making the reason mandatory means the description cannot say
/// the useless half.
///
/// The option stays in the list. Dropping it is what an app does today, and
/// it costs the user the knowledge that the thing exists at all —
/// audiofiles' multi-sample mode appears on its own once a second sample is
/// dropped, so a user who never sees it never learns what to drop.
///
/// **Not [`Field::error`], and not [`Field::hint`].** An error is about the
/// answer and a hint is standing help for the whole question; this is about
/// one option among several, which is the level neither of those reaches.
///
/// **Not disabled-the-state.** `State::Disabled` is about a whole field
/// refusing to answer. This says the field is live and one of its answers
/// is not available yet, which is a different sentence and the reason the
/// tone rule matters here: the *other* options are still usable.
pub unavailable: Option<&'a str>,
/// The line under the label that says what picking this means.
///
/// A choice between three plans is a choice nobody can make from three
/// names, and until this existed the description had nowhere to put the
/// sentence that made it makeable. What the corpus did instead is the
/// tell: four of the six measured sites fold it into the label —
/// `Public : Anyone can see this repository` in MNW's git
/// settings, the same shape in its project-basics AI tier and its cart's
/// currency conversion, and `Mislabeled (wrong AI tier or category)` in
/// its report modal. The described screens do it too, in miniature: `Every
/// 15 minutes (recommended)`, `Reference samples in place (loose-files
/// mode)`. One fact, six spellings, no member.
///
/// # Where it goes is the host's, and the rule already exists
///
/// This is [`unavailable`](Self::unavailable)'s question met a third time
/// and it takes the same answer, which is the strongest evidence one member
/// is right rather than two. A radio group has room and gives the line its
/// own element beside the label. A ``'s option takes no elements,
/// no second line and no title a keyboard reaches, so the line runs into
/// the option's own text — exactly as a precondition does, and as a theme's
/// contrast badge does in brackets. A terminal has rows and puts it on one
/// under the option.
///
/// # Not a price, and that is a measurement rather than a preference
///
/// The site that asked for this is MNW's fee calculator, whose tier cards
/// carry a name, a price *and* a description, so a second member for the
/// price was on the table. It loses on the count: the tree's other three
/// priced tier lists — `project.html`, `project_paywall.html`,
/// `index.html` — are not option lists at all. Each card carries its own
/// submit, which makes it a region with a heading, a fact and an act, and
/// it is sayable already. So a price member would have exactly one
/// consumer, and it would mean this crate growing a money type it does not
/// have: [`Unit`] is a time axis, and every amount in the described tree is
/// text.
///
/// The price therefore leads the line: `$24/mo. 2GB/file, 100GB total.
/// Fits audio, plugins, binaries.` What would reopen it is a **second**
/// priced option list, not a judgement about how that reads.
///
/// # What it is not
///
/// Not [`unavailable`](Self::unavailable), which says the option cannot be
/// picked. This says what it means to pick it, and the two are drawn
/// together on an option that carries both: the description that says a
/// tier is out of stock *and* what the tier is has said two things.
///
/// Not [`Field::hint`], which is standing help for the whole question, and
/// not markup. One line of plain text, for [`Candidate::detail`]'s reason:
/// an option list is a place a renderer lays out, and a description that
/// put a block in one would be handing every host a layout problem for the
/// benefit of one.
pub detail: Option<&'a str>,
}
impl<'a> Choice<'a> {
/// An option whose submitted value is also its label.
#[must_use]
pub const fn plain(value: &'a str) -> Self {
Self::new(value, value)
}
/// An option that submits one string and reads as another.
///
/// A constructor rather than a literal, which is what `#[non_exhaustive]`
/// costs and buys: outside this crate the struct cannot be built by naming
/// its members, so every call site goes through here and the next member
/// added breaks none of them.
#[must_use]
pub const fn new(value: &'a str, label: &'a str) -> Self {
Self {
value,
label,
unavailable: None,
detail: None,
}
}
/// The same option, not pickable yet, and why.
///
/// Builder-shaped because the reason is the rare case: 39 of the 40 option
/// sites measured across the tree do not have one.
#[must_use]
pub const fn unless(mut self, reason: &'a str) -> Self {
self.unavailable = Some(reason);
self
}
/// The same option, with the line that says what picking it means.
///
/// Builder-shaped for [`unless`](Self::unless)'s reason, and it is the
/// commoner of the two: six measured sites want this and one wants a
/// precondition. See [`detail`](Self::detail).
#[must_use]
pub const fn detailing(mut self, detail: &'a str) -> Self {
self.detail = Some(detail);
self
}
/// Whether the option can be picked right now.
///
/// The predicate a renderer branches on, so that "unavailable" is read as
/// one condition in one place rather than as `unavailable.is_some()` at
/// three renderers, one of which will invert it.
#[must_use]
pub const fn available(&self) -> bool {
self.unavailable.is_none()
}
}
/// One entry in a field's suggestion list.
///
/// A suggestion-only type rather than a fourth member on [`Choice`], ruled by
/// Max. The two are near-identical and that is the accepted drift risk, so the
/// mitigation is written here: **an
/// option and a candidate are submitted the same way and read differently.**
/// An option is a thing you pick from a known set, and the set is the whole of
/// what there is. A candidate is a thing you are being *oriented* toward out of
/// a set nobody can see, which is why it carries [`detail`](Self::detail) and
/// an option does not.
///
/// This reverses a position quasi-router stated in its own doc, that a
/// candidate is [`Choice`] "because a candidate is submitted under one string
/// and read under another, which is what an option is". True and not
/// sufficient: how a thing is submitted was never the half that differed.
///
/// # Why the second string is not folded into the label
///
/// Because every renderer wants it separately, and the two measured sites both
/// draw it by hand today. The MNW server's tag box computes its context as the
/// parent path -- "the parent path orients an otherwise ambiguous leaf:
/// 'Format' appears under audio, software, writing, and video" -- and a list of
/// four identical rows reading "Format" is not a usable list. In a webview the
/// second string is styled differently, in a terminal it wants the remaining
/// columns rather than a dash, and in neither is it part of what the typed
/// value matches against. `Choice::new(slug, format!("{label} - {context}"))`
/// loses all three of those facts, which is the condition this type exists to
/// end.
///
/// # No `unavailable`
///
/// [`Choice::unavailable`] has no counterpart here, and the omission is the
/// implementer's call recorded rather than an oversight. A suggestion that
/// cannot be picked is arguably not a suggestion: an option list is a fixed set
/// a user is owed an explanation about, and a candidate list is whatever a
/// route decided to offer, so a route with nothing to say simply does not offer
/// the row. Add it if a measured site ever wants it.
///
/// # What it does not carry, and where that lives
///
/// What *happens* when a candidate is picked. Picking is local by default -- it
/// writes [`value`](Self::value) into the field that owns the list -- and a
/// candidate that does something else says so with an action. An action is not
/// a word this crate has, exactly as [`Field`] here has no `suggests` member,
/// so both live on the router's owned mirror of this type.
///
/// `#[non_exhaustive]` from birth. Non-negotiable: adding it later means a
/// breaking change at every literal site in the tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Candidate<'a> {
/// What is submitted, and what picking writes into the field.
pub value: &'a str,
/// What is read.
pub label: &'a str,
/// The second line: what orients this candidate among rows that read alike.
///
/// Optional because a candidate list whose labels are already distinct
/// wants nothing here, and a renderer given [`None`] draws one line rather
/// than an empty second one.
pub detail: Option<&'a str>,
}
impl<'a> Candidate<'a> {
/// A candidate whose submitted value is also its label.
#[must_use]
pub const fn plain(value: &'a str) -> Self {
Self::new(value, value)
}
/// A candidate that submits one string and reads as another.
///
/// A constructor rather than a literal, which is what `#[non_exhaustive]`
/// costs and buys: outside this crate the struct cannot be built by naming
/// its members, so every call site goes through here and the next member
/// added breaks none of them.
#[must_use]
pub const fn new(value: &'a str, label: &'a str) -> Self {
Self {
value,
label,
detail: None,
}
}
/// The same candidate, with the line that tells it from its neighbours.
#[must_use]
pub const fn detailed(mut self, detail: &'a str) -> Self {
self.detail = Some(detail);
self
}
}
/// One field of a form.
///
/// Borrowed rather than owned: a description is built, read once by a renderer,
/// and dropped. Nothing here outlives the screen it describes.
///
/// # What it carries, and what it does not
///
/// Stated here so the next renderer does not re-ask, which is what the first
/// two both did. It carries everything a renderer needs to *draw* the field:
/// its kind, what it is called, what it is asked for, its standing help, what
/// is wrong with it now, whether it is compulsory, whether it hides behind a
/// disclosure, its ghost text, and the options it offers.
///
/// It does not carry the **current value**, and it is not going to. That is the
/// one thing here that is genuinely renderer state: a webview reads it back out
/// of the DOM, an immediate-mode renderer holds a `&mut` to the app's own field
/// and writes through it, and a terminal keeps an edit buffer. A description
/// that carried the value would have to carry a way to write it back, at which
/// point it is a form model and no longer a description.
///
/// **Constraints** are here and enforcement is not, which is one line rather
/// than two. [`required`], [`max_length`], [`min`] and [`max`] are facts about
/// the *question*, so a renderer can emit its host's idiom for each — an HTML
/// attribute, a marked label, a clamped spinner — and the platform helps the
/// user before anything is submitted. Deciding that a value is wrong stays with
/// whoever validated, and [`error`] is that decision arriving back.
///
/// The set stops before `pattern`, and stops there on both tests at once. A
/// regex has an honest answer in a webview and none anywhere else: egui would
/// have to run it per keystroke and decide what a half-typed value means,
/// which is enforcement wearing description's clothes. And it is one site in
/// goingson and none in Balanced Breakfast, against 8 and 1 for `maxlength`.
///
/// [`error`]: Field::error
/// [`required`]: Field::required
/// [`max_length`]: Field::max_length
/// [`min`]: Field::min
/// [`max`]: Field::max
/// How a slider's position becomes its value, and how finely it moves.
///
/// **The data of a slider is a fraction and a function taking numbers to
/// numbers.** Stated by Max, and it is what [`min`](Field::min) and
/// [`max`](Field::max) are not: they were never the control's extent.
/// A slider's extent is always 0 to 1 — a thumb at 40% of a track — and the
/// bounds are `f(0)` and `f(1)`. Linear is the constant-slope case, which is
/// exactly why nobody noticed the function was there: when `f` is
/// `min + t * (max - min)` the extent and the bounds coincide numerically and
/// the mapping is invisible.
///
/// So this is not a scale flag bolted onto a range. Every range described
/// before it had a mapping, and four renderers each hard-coded the same one.
///
/// # Why a closed family and not a function
///
/// `fn(f64) -> f64` is the literal reading and it does not survive the
/// description boundary. A fn pointer cannot be emitted into a browser, and it
/// cannot be compared or hashed in a way that means anything, which this struct
/// needs. A named family is the same semantics with arbitrary closures given
/// up, and nothing measured wants one: the tree has a single non-linear shape
/// across five controls and no second shape at all.
///
/// # Why the step is here
///
/// Max, in the same breath: if the family is prescriptive anyway, the step
/// spacing belongs in it. On a slider the granularity and the mapping are one
/// decision — a curve chosen without saying how finely it moves is half an
/// answer — and holding them apart is what let a 0-to-1 threshold ship as a
/// two-position control, since the host default of 1 was applied to a mapping
/// nobody had named. It also un-overloads [`Field::step`], which stays as it
/// was for a *typed* value, where there is no mapping and the granularity is a
/// plain fact about the number.
///
/// A future curve carrying a fact of its own — an exponent, an inflection —
/// puts it in its own variant rather than on the struct, which is the second
/// reason this shape is right.
///
/// **The step is in the value's own units under every curve.** What a curve
/// changes is the mapping, not the units the granularity is measured in: a step
/// of `0.001` on an envelope time is three decimals whether the track is
/// logarithmic or not, and a renderer that reads the step for display precision
/// keeps reading it the same way.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Curve<'a> {
/// Constant slope: `f(t) = min + t * (max - min)`.
///
/// What every described range meant before this enum existed, and the
/// default, so a site that says nothing is correct unchanged.
Linear {
/// The granularity, in the value's own units. `None` is the host's own.
step: Option<&'a str>,
},
/// Constant ratio: `f(t) = min * (max / min).powf(t)`.
///
/// The mapping for a question whose extent spans orders of magnitude and
/// whose interesting half is the small end. audiofiles' envelope times run
/// 0.001 to 5 seconds, where a 5 ms attack and a 50 ms attack are audibly
/// different instruments and a linear track puts both inside its first one
/// percent.
///
/// # It needs positive bounds
///
/// A constant ratio is undefined across zero, so this asks for `min > 0`.
/// A range that does not have that is mapped [`Linear`](Self::Linear)ly
/// instead — see [`value_at`](Self::value_at). Stated rather than enforced,
/// the way every other constraint in this crate is, and it is not a
/// hypothetical: an envelope's sustain is a 0-to-1 level and is linear for
/// this reason rather than by oversight.
Logarithmic {
/// The granularity, in the value's own units. `None` is the host's own.
step: Option<&'a str>,
},
}
impl Default for Curve<'_> {
fn default() -> Self {
Self::Linear { step: None }
}
}
impl<'a> Curve<'a> {
/// The granularity this curve moves in, whichever curve it is.
///
/// Every variant carries one, so reading it does not need a match at each
/// of the four renderers.
#[must_use]
pub const fn step(self) -> Option<&'a str> {
// No wildcard: `#[non_exhaustive]` binds downstream, not here, so a
// curve added later has to answer this rather than fall through to a
// granularity nobody chose.
match self {
Self::Linear { step } | Self::Logarithmic { step } => step,
}
}
/// Whether this curve maps as a constant ratio *given these bounds*.
///
/// The bounds are the argument because [`Logarithmic`](Self::Logarithmic)
/// is a request rather than a guarantee: it needs `0 < min < max`, and a
/// range that does not have that is drawn linearly. A renderer asks this
/// instead of matching on the variant, so the fallback is decided in one
/// place rather than four.
#[must_use]
pub fn is_ratio(self, min: f64, max: f64) -> bool {
matches!(self, Self::Logarithmic { .. }) && min > 0.0 && max > min
}
/// The value at a position along the track, where `position` is 0 to 1.
///
/// `f`. The whole point of the type, and it lives here rather than in each
/// renderer so that a terminal's bar, an egui slider and a browser's input
/// cannot disagree about where a value sits.
///
/// A position outside 0 to 1 is clamped, and bounds that are equal or
/// inverted give `min` back: a track with no extent has one value on it.
#[must_use]
pub fn value_at(self, position: f64, min: f64, max: f64) -> f64 {
let position = position.clamp(0.0, 1.0);
// NaN named rather than fallen through: `max <= min` is false for a NaN
// bound, so without it a track with no numbers on it would be mapped as
// if it had two.
if max <= min || min.is_nan() || max.is_nan() {
return min;
}
if self.is_ratio(min, max) {
min * (max / min).powf(position)
} else {
position.mul_add(max - min, min)
}
}
/// The position a value sits at, where the answer is 0 to 1.
///
/// `f` inverted, which is what a renderer needs to *draw* a value it was
/// handed. Same clamping and the same degenerate answer as
/// [`value_at`](Self::value_at).
#[must_use]
pub fn position_of(self, value: f64, min: f64, max: f64) -> f64 {
if max <= min || min.is_nan() || max.is_nan() {
return 0.0;
}
let value = value.clamp(min, max);
let position = if self.is_ratio(min, max) {
(value / min).ln() / (max / min).ln()
} else {
(value - min) / (max - min)
};
position.clamp(0.0, 1.0)
}
}