// 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, } } }