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}