makeover-layout 0.45.0

The renderer-agnostic half of the make-family design system: what a thing IS, named as intents and relationships and never as values. Colour defers to makeover, spacing to makeover-geometry; what is left is composition.
Documentation
// Names this module's prose links to, resolved for rustdoc.
#[allow(unused_imports)]
use crate::{Choice, Field};

/// Which ambient mode a theme is written for.
///
/// The vocabulary's own spelling of what `makeover` calls a theme's variant,
/// and the duplication is deliberate rather than an oversight. This crate has
/// no dependencies by charter — it emits nothing, reads nothing and resolves
/// nothing — so it cannot take the crate that owns the file format, and a
/// renderer that must group a picker needs the three groups as values.
///
/// The two are kept in step by the app that converts between them, which is a
/// three-arm `match` at each adopter and the price of the layering. If a fourth
/// mode is ever authored, this enum and `makeover::Variant` move together.
///
/// Three, not two: one shipped theme is high contrast, and an app matching on
/// light-or-dark alone files it under the wrong one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum ThemeVariant {
    /// Written for a light ambient mode.
    Light,
    /// Written for a dark ambient mode.
    Dark,
    /// Written to be legible before it is pretty.
    HighContrast,
}

impl ThemeVariant {
    /// The machine spelling, matching the theme file's own `meta.variant`.
    ///
    /// A data attribute, a stored value, a test assertion. Not a heading: what
    /// a group is *called* on screen is [`heading`](Self::heading).
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            ThemeVariant::Light => "light",
            ThemeVariant::Dark => "dark",
            ThemeVariant::HighContrast => "high-contrast",
        }
    }

    /// What the group of themes in this variant is called on screen.
    ///
    /// Here rather than at each renderer, which is the whole argument for the
    /// member existing: three renderers picking their own headings is one
    /// picker reading three ways, and the spellings below are the ones
    /// goingson's shipped picker used before it was described.
    #[must_use]
    pub const fn heading(self) -> &'static str {
        match self {
            ThemeVariant::Light => "Light",
            ThemeVariant::Dark => "Dark",
            ThemeVariant::HighContrast => "High Contrast",
        }
    }
}

impl std::fmt::Display for ThemeVariant {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// How legible a theme measured, as a picker reports it.
///
/// A measurement carried into the description, which is unusual here and is the
/// one case that earns it: the number comes off the theme's resolved colours,
/// so the layer that loaded the theme is the only party that has it, and an app
/// re-deriving it would be parsing every theme file a second time to learn what
/// was already known. What a renderer does with it is a badge beside the name.
///
/// Ordered worst-first, matching `makeover::ContrastTier`, so the two sort the
/// same way and an adopter's `match` cannot invert an ordering by accident.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Contrast {
    /// Muted text below the 3:1 floor for large text and UI parts.
    Low,
    /// Muted text clears 3:1 but not the 4.5:1 bar for normal text.
    Standard,
    /// Muted text meets WCAG AA on every panel ground.
    High,
}

impl Contrast {
    /// The machine spelling, for a data attribute or a test.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Contrast::Low => "low",
            Contrast::Standard => "standard",
            Contrast::High => "high",
        }
    }

    /// The short mark shown beside a theme's name.
    ///
    /// One spelling for the tree, for [`ThemeVariant::heading`]'s reason. These
    /// are the marks audiofiles shipped before its picker was described, which
    /// is the only implementation that ever drew them.
    ///
    /// [`Standard`](Self::Standard) is not the absence of a mark: a reader
    /// scanning a column of badges learns more from three marks than from two
    /// and a gap, and "OK" is the honest reading of a theme that clears the UI
    /// floor and misses the text one.
    #[must_use]
    pub const fn badge(self) -> &'static str {
        match self {
            Contrast::Low => "low",
            Contrast::Standard => "OK",
            Contrast::High => "AA",
        }
    }
}

impl std::fmt::Display for Contrast {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// One theme, as a picker offers it.
///
/// Four facts where a [`Choice`] has two, and the two extra ones are why this
/// is its own type rather than options with the variant folded into the label.
/// Both are facts the theme layer resolved and neither survives being written
/// into a string: a group is structure and a badge is a second column.
///
/// # No `unavailable`
///
/// [`Choice::unavailable`]'s counterpart is absent for its own sibling's
/// reason. A theme that is installed can be picked, and a theme that is not
/// installed is not in the list. There is no third state for a reason to
/// explain.
///
/// `#[non_exhaustive]` from birth, so a new member costs no call site.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ThemeChoice<'a> {
    /// What is submitted, and what the app stores.
    pub id: &'a str,
    /// What is read.
    pub name: &'a str,
    /// Which group it belongs to.
    pub variant: ThemeVariant,
    /// How legible its muted text measured.
    pub contrast: Contrast,
}

impl<'a> ThemeChoice<'a> {
    /// A theme, with everything a picker needs to place and mark it.
    ///
    /// Every fact is an argument and none is a builder, which is the opposite
    /// of [`Choice`]'s arrangement and is deliberate: a theme missing its
    /// variant has no group to sit in and a theme missing its tier has no badge
    /// to draw, so both are the control rather than embellishments on it. The
    /// same reasoning [`Field::range`] applies to its bounds.
    #[must_use]
    pub const fn new(
        id: &'a str,
        name: &'a str,
        variant: ThemeVariant,
        contrast: Contrast,
    ) -> Self {
        Self {
            id,
            name,
            variant,
            contrast,
        }
    }
}