Skip to main content

makeover_layout/
theme_choice.rs

1/// Which ambient mode a theme is written for.
2///
3/// The vocabulary's own spelling of what `makeover` calls a theme's variant,
4/// and the duplication is deliberate rather than an oversight. This crate has
5/// no dependencies by charter (it emits nothing, reads nothing and resolves
6/// nothing) so it cannot take the crate that owns the file format, and a
7/// renderer that must group a picker needs the three groups as values.
8///
9/// The two are kept in step by the app that converts between them, which is a
10/// three-arm `match` at each adopter and the price of the layering. If a fourth
11/// mode is ever authored, this enum and `makeover::Variant` move together.
12///
13/// Three, not two: one shipped theme is high contrast, and an app matching on
14/// light-or-dark alone files it under the wrong one.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[non_exhaustive]
17pub enum ThemeVariant {
18    /// Written for a light ambient mode.
19    Light,
20    /// Written for a dark ambient mode.
21    Dark,
22    /// Written to be legible before it is pretty.
23    HighContrast,
24}
25
26impl ThemeVariant {
27    /// The machine spelling, matching the theme file's own `meta.variant`.
28    ///
29    /// A data attribute, a stored value, a test assertion. Not a heading: what
30    /// a group is *called* on screen is [`heading`](Self::heading).
31    #[must_use]
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            ThemeVariant::Light => "light",
35            ThemeVariant::Dark => "dark",
36            ThemeVariant::HighContrast => "high-contrast",
37        }
38    }
39
40    /// What the group of themes in this variant is called on screen.
41    ///
42    /// Here rather than at each renderer, which is the whole argument for the
43    /// member existing: three renderers picking their own headings is one
44    /// picker reading three ways, and the spellings below are the ones
45    /// goingson's shipped picker used before it was described.
46    #[must_use]
47    pub const fn heading(self) -> &'static str {
48        match self {
49            ThemeVariant::Light => "Light",
50            ThemeVariant::Dark => "Dark",
51            ThemeVariant::HighContrast => "High Contrast",
52        }
53    }
54}
55
56impl std::fmt::Display for ThemeVariant {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.write_str(self.as_str())
59    }
60}
61
62/// How legible a theme measured, as a picker reports it.
63///
64/// A measurement carried into the description, which is unusual here and is the
65/// one case that earns it: the number comes off the theme's resolved colours,
66/// so the layer that loaded the theme is the only party that has it, and an app
67/// re-deriving it would be parsing every theme file a second time to learn what
68/// was already known. What a renderer does with it is a badge beside the name.
69///
70/// Ordered worst-first, matching `makeover::ContrastTier`, so the two sort the
71/// same way and an adopter's `match` cannot invert an ordering by accident.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
73#[non_exhaustive]
74pub enum Contrast {
75    /// Muted text below the 3:1 floor for large text and UI parts.
76    Low,
77    /// Muted text clears 3:1 but not the 4.5:1 bar for normal text.
78    Standard,
79    /// Muted text meets WCAG AA on every panel ground.
80    High,
81}
82
83impl Contrast {
84    /// The machine spelling, for a data attribute or a test.
85    #[must_use]
86    pub const fn as_str(self) -> &'static str {
87        match self {
88            Contrast::Low => "low",
89            Contrast::Standard => "standard",
90            Contrast::High => "high",
91        }
92    }
93
94    /// The short mark shown beside a theme's name.
95    ///
96    /// One spelling for the tree, for [`ThemeVariant::heading`]'s reason. These
97    /// are the marks audiofiles shipped before its picker was described, which
98    /// is the only implementation that ever drew them.
99    ///
100    /// [`Standard`](Self::Standard) is not the absence of a mark: a reader
101    /// scanning a column of badges learns more from three marks than from two
102    /// and a gap, and "OK" is the honest reading of a theme that clears the UI
103    /// floor and misses the text one.
104    #[must_use]
105    pub const fn badge(self) -> &'static str {
106        match self {
107            Contrast::Low => "low",
108            Contrast::Standard => "OK",
109            Contrast::High => "AA",
110        }
111    }
112}
113
114impl std::fmt::Display for Contrast {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.write_str(self.as_str())
117    }
118}