Skip to main content

max / makeover-layout

6.3 KB · 175 lines History Blame Raw
1 // Names this module's prose links to, resolved for rustdoc.
2 #[allow(unused_imports)]
3 use crate::{Choice, Field};
4
5 /// Which ambient mode a theme is written for.
6 ///
7 /// The vocabulary's own spelling of what `makeover` calls a theme's variant,
8 /// and the duplication is deliberate rather than an oversight. This crate has
9 /// no dependencies by charter — it emits nothing, reads nothing and resolves
10 /// nothing — so it cannot take the crate that owns the file format, and a
11 /// renderer that must group a picker needs the three groups as values.
12 ///
13 /// The two are kept in step by the app that converts between them, which is a
14 /// three-arm `match` at each adopter and the price of the layering. If a fourth
15 /// mode is ever authored, this enum and `makeover::Variant` move together.
16 ///
17 /// Three, not two: one shipped theme is high contrast, and an app matching on
18 /// light-or-dark alone files it under the wrong one.
19 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20 #[non_exhaustive]
21 pub enum ThemeVariant {
22 /// Written for a light ambient mode.
23 Light,
24 /// Written for a dark ambient mode.
25 Dark,
26 /// Written to be legible before it is pretty.
27 HighContrast,
28 }
29
30 impl ThemeVariant {
31 /// The machine spelling, matching the theme file's own `meta.variant`.
32 ///
33 /// A data attribute, a stored value, a test assertion. Not a heading: what
34 /// a group is *called* on screen is [`heading`](Self::heading).
35 #[must_use]
36 pub const fn as_str(self) -> &'static str {
37 match self {
38 ThemeVariant::Light => "light",
39 ThemeVariant::Dark => "dark",
40 ThemeVariant::HighContrast => "high-contrast",
41 }
42 }
43
44 /// What the group of themes in this variant is called on screen.
45 ///
46 /// Here rather than at each renderer, which is the whole argument for the
47 /// member existing: three renderers picking their own headings is one
48 /// picker reading three ways, and the spellings below are the ones
49 /// goingson's shipped picker used before it was described.
50 #[must_use]
51 pub const fn heading(self) -> &'static str {
52 match self {
53 ThemeVariant::Light => "Light",
54 ThemeVariant::Dark => "Dark",
55 ThemeVariant::HighContrast => "High Contrast",
56 }
57 }
58 }
59
60 impl std::fmt::Display for ThemeVariant {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(self.as_str())
63 }
64 }
65
66 /// How legible a theme measured, as a picker reports it.
67 ///
68 /// A measurement carried into the description, which is unusual here and is the
69 /// one case that earns it: the number comes off the theme's resolved colours,
70 /// so the layer that loaded the theme is the only party that has it, and an app
71 /// re-deriving it would be parsing every theme file a second time to learn what
72 /// was already known. What a renderer does with it is a badge beside the name.
73 ///
74 /// Ordered worst-first, matching `makeover::ContrastTier`, so the two sort the
75 /// same way and an adopter's `match` cannot invert an ordering by accident.
76 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
77 #[non_exhaustive]
78 pub enum Contrast {
79 /// Muted text below the 3:1 floor for large text and UI parts.
80 Low,
81 /// Muted text clears 3:1 but not the 4.5:1 bar for normal text.
82 Standard,
83 /// Muted text meets WCAG AA on every panel ground.
84 High,
85 }
86
87 impl Contrast {
88 /// The machine spelling, for a data attribute or a test.
89 #[must_use]
90 pub const fn as_str(self) -> &'static str {
91 match self {
92 Contrast::Low => "low",
93 Contrast::Standard => "standard",
94 Contrast::High => "high",
95 }
96 }
97
98 /// The short mark shown beside a theme's name.
99 ///
100 /// One spelling for the tree, for [`ThemeVariant::heading`]'s reason. These
101 /// are the marks audiofiles shipped before its picker was described, which
102 /// is the only implementation that ever drew them.
103 ///
104 /// [`Standard`](Self::Standard) is not the absence of a mark: a reader
105 /// scanning a column of badges learns more from three marks than from two
106 /// and a gap, and "OK" is the honest reading of a theme that clears the UI
107 /// floor and misses the text one.
108 #[must_use]
109 pub const fn badge(self) -> &'static str {
110 match self {
111 Contrast::Low => "low",
112 Contrast::Standard => "OK",
113 Contrast::High => "AA",
114 }
115 }
116 }
117
118 impl std::fmt::Display for Contrast {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.write_str(self.as_str())
121 }
122 }
123
124 /// One theme, as a picker offers it.
125 ///
126 /// Four facts where a [`Choice`] has two, and the two extra ones are why this
127 /// is its own type rather than options with the variant folded into the label.
128 /// Both are facts the theme layer resolved and neither survives being written
129 /// into a string: a group is structure and a badge is a second column.
130 ///
131 /// # No `unavailable`
132 ///
133 /// [`Choice::unavailable`]'s counterpart is absent for its own sibling's
134 /// reason. A theme that is installed can be picked, and a theme that is not
135 /// installed is not in the list. There is no third state for a reason to
136 /// explain.
137 ///
138 /// `#[non_exhaustive]` from birth, so a new member costs no call site.
139 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
140 #[non_exhaustive]
141 pub struct ThemeChoice<'a> {
142 /// What is submitted, and what the app stores.
143 pub id: &'a str,
144 /// What is read.
145 pub name: &'a str,
146 /// Which group it belongs to.
147 pub variant: ThemeVariant,
148 /// How legible its muted text measured.
149 pub contrast: Contrast,
150 }
151
152 impl<'a> ThemeChoice<'a> {
153 /// A theme, with everything a picker needs to place and mark it.
154 ///
155 /// Every fact is an argument and none is a builder, which is the opposite
156 /// of [`Choice`]'s arrangement and is deliberate: a theme missing its
157 /// variant has no group to sit in and a theme missing its tier has no badge
158 /// to draw, so both are the control rather than embellishments on it. The
159 /// same reasoning [`Field::range`] applies to its bounds.
160 #[must_use]
161 pub const fn new(
162 id: &'a str,
163 name: &'a str,
164 variant: ThemeVariant,
165 contrast: Contrast,
166 ) -> Self {
167 Self {
168 id,
169 name,
170 variant,
171 contrast,
172 }
173 }
174 }
175