Skip to main content

omp_tui/color/
named.rs

1//! CSS color keywords: the full extended named-color set (CSS Color 4
2//! `<named-color>`, including `rebeccapurple`), the `transparent` and
3//! `currentcolor` specials, and the system colors with their deprecated
4//! aliases.
5
6use super::CssColor;
7use crate::{context::Theme, frame::Color};
8
9/// A CSS system color keyword, resolved against the [`Theme`]'s
10/// semantic palette rather than a fixed RGB value.
11///
12/// Deprecated keywords (`ActiveBorder`, `WindowText`, ...) parse to the
13/// modern keyword CSS Color 4 §6.5 maps them to.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum SystemColor {
16	/// Accent fill for selected or activated interface parts.
17	AccentColor,
18	/// Text painted over [`Self::AccentColor`] fills.
19	AccentColorText,
20	/// Text of active links.
21	ActiveText,
22	/// Border of push buttons.
23	ButtonBorder,
24	/// Face of push buttons.
25	ButtonFace,
26	/// Text on push buttons.
27	ButtonText,
28	/// The application background: the terminal's own default.
29	Canvas,
30	/// Text on the application background.
31	CanvasText,
32	/// Background of input fields.
33	Field,
34	/// Text inside input fields.
35	FieldText,
36	/// Disabled or de-emphasized text.
37	GrayText,
38	/// Background of selected text.
39	Highlight,
40	/// Selected text.
41	HighlightText,
42	/// Text of unvisited links.
43	LinkText,
44	/// Background of highlighter marks.
45	Mark,
46	/// Text inside highlighter marks.
47	MarkText,
48	/// Background of chosen items.
49	SelectedItem,
50	/// Text of chosen items.
51	SelectedItemText,
52	/// Text of visited links.
53	VisitedText,
54}
55
56impl SystemColor {
57	/// Case-insensitively parses a system color keyword, including the
58	/// deprecated aliases. Called by [`Theme::token`] so markup resolves
59	/// system colors through the same deferred path as theme tokens.
60	pub(crate) fn parse(name: &str) -> Option<Self> {
61		let (_, system) = SYSTEM
62			.iter()
63			.find(|(keyword, _)| keyword.eq_ignore_ascii_case(name))?;
64		Some(*system)
65	}
66
67	/// Resolves to the theme color playing this keyword's role.
68	pub(crate) const fn resolve(self, theme: &Theme) -> Color {
69		match self {
70			Self::Canvas => Color::Default,
71			Self::CanvasText | Self::ButtonText | Self::FieldText => theme.fg,
72			Self::AccentColor
73			| Self::ActiveText
74			| Self::Highlight
75			| Self::LinkText
76			| Self::SelectedItem => theme.accent,
77			Self::AccentColorText | Self::HighlightText | Self::MarkText | Self::SelectedItemText => {
78				theme.contrast
79			},
80			Self::ButtonFace | Self::Field => theme.surface,
81			Self::ButtonBorder | Self::GrayText | Self::VisitedText => theme.muted,
82			Self::Mark => theme.warn,
83		}
84	}
85}
86
87/// Longest keyword across every table: `lightgoldenrodyellow`.
88const LONGEST: usize = 20;
89
90/// Resolves a color keyword, case-insensitively.
91pub(super) fn parse(name: &str) -> Option<CssColor> {
92	if name.is_empty() || name.len() > LONGEST || !name.is_ascii() {
93		return None;
94	}
95	let mut lower = [0_u8; LONGEST];
96	for (slot, byte) in lower.iter_mut().zip(name.bytes()) {
97		*slot = byte.to_ascii_lowercase();
98	}
99	let needle = std::str::from_utf8(&lower[..name.len()]).ok()?;
100	if needle == "transparent" {
101		return Some(CssColor::Rgba(0, 0, 0, 0.0));
102	}
103	if needle == "currentcolor" {
104		return Some(CssColor::Current);
105	}
106	if let Ok(index) = SYSTEM.binary_search_by(|(keyword, _)| keyword.cmp(&needle)) {
107		return Some(CssColor::System(SYSTEM[index].1));
108	}
109	let index = NAMED
110		.binary_search_by(|(candidate, _)| candidate.cmp(&needle))
111		.ok()?;
112	let rgb = NAMED[index].1;
113	Some(CssColor::rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8))
114}
115
116/// System color keywords (current and CSS Color 4 §6.5 deprecated
117/// aliases), lowercase and sorted for binary search.
118const SYSTEM: &[(&str, SystemColor)] = &[
119	("accentcolor", SystemColor::AccentColor),
120	("accentcolortext", SystemColor::AccentColorText),
121	("activeborder", SystemColor::ButtonBorder),
122	("activecaption", SystemColor::Canvas),
123	("activetext", SystemColor::ActiveText),
124	("appworkspace", SystemColor::Canvas),
125	("background", SystemColor::Canvas),
126	("buttonborder", SystemColor::ButtonBorder),
127	("buttonface", SystemColor::ButtonFace),
128	("buttonhighlight", SystemColor::ButtonFace),
129	("buttonshadow", SystemColor::ButtonFace),
130	("buttontext", SystemColor::ButtonText),
131	("canvas", SystemColor::Canvas),
132	("canvastext", SystemColor::CanvasText),
133	("captiontext", SystemColor::CanvasText),
134	("field", SystemColor::Field),
135	("fieldtext", SystemColor::FieldText),
136	("graytext", SystemColor::GrayText),
137	("highlight", SystemColor::Highlight),
138	("highlighttext", SystemColor::HighlightText),
139	("inactiveborder", SystemColor::ButtonBorder),
140	("inactivecaption", SystemColor::Canvas),
141	("inactivecaptiontext", SystemColor::GrayText),
142	("infobackground", SystemColor::Canvas),
143	("infotext", SystemColor::CanvasText),
144	("linktext", SystemColor::LinkText),
145	("mark", SystemColor::Mark),
146	("marktext", SystemColor::MarkText),
147	("menu", SystemColor::Canvas),
148	("menutext", SystemColor::CanvasText),
149	("scrollbar", SystemColor::Canvas),
150	("selecteditem", SystemColor::SelectedItem),
151	("selecteditemtext", SystemColor::SelectedItemText),
152	("threeddarkshadow", SystemColor::ButtonBorder),
153	("threedface", SystemColor::ButtonFace),
154	("threedhighlight", SystemColor::ButtonBorder),
155	("threedlightshadow", SystemColor::ButtonBorder),
156	("threedshadow", SystemColor::ButtonBorder),
157	("visitedtext", SystemColor::VisitedText),
158	("window", SystemColor::Canvas),
159	("windowframe", SystemColor::ButtonBorder),
160	("windowtext", SystemColor::CanvasText),
161];
162
163/// Every CSS/HTML named color (CSS Color Module extended keywords),
164/// sorted for binary search.
165const NAMED: &[(&str, u32)] = &[
166	("aliceblue", 0x00f0_f8ff),
167	("antiquewhite", 0x00fa_ebd7),
168	("aqua", 0x0000_ffff),
169	("aquamarine", 0x007f_ffd4),
170	("azure", 0x00f0_ffff),
171	("beige", 0x00f5_f5dc),
172	("bisque", 0x00ff_e4c4),
173	("black", 0x0000_0000),
174	("blanchedalmond", 0x00ff_ebcd),
175	("blue", 0x0000_00ff),
176	("blueviolet", 0x008a_2be2),
177	("brown", 0x00a5_2a2a),
178	("burlywood", 0x00de_b887),
179	("cadetblue", 0x005f_9ea0),
180	("chartreuse", 0x007f_ff00),
181	("chocolate", 0x00d2_691e),
182	("coral", 0x00ff_7f50),
183	("cornflowerblue", 0x0064_95ed),
184	("cornsilk", 0x00ff_f8dc),
185	("crimson", 0x00dc_143c),
186	("cyan", 0x0000_ffff),
187	("darkblue", 0x0000_008b),
188	("darkcyan", 0x0000_8b8b),
189	("darkgoldenrod", 0x00b8_860b),
190	("darkgray", 0x00a9_a9a9),
191	("darkgreen", 0x0000_6400),
192	("darkgrey", 0x00a9_a9a9),
193	("darkkhaki", 0x00bd_b76b),
194	("darkmagenta", 0x008b_008b),
195	("darkolivegreen", 0x0055_6b2f),
196	("darkorange", 0x00ff_8c00),
197	("darkorchid", 0x0099_32cc),
198	("darkred", 0x008b_0000),
199	("darksalmon", 0x00e9_967a),
200	("darkseagreen", 0x008f_bc8f),
201	("darkslateblue", 0x0048_3d8b),
202	("darkslategray", 0x002f_4f4f),
203	("darkslategrey", 0x002f_4f4f),
204	("darkturquoise", 0x0000_ced1),
205	("darkviolet", 0x0094_00d3),
206	("deeppink", 0x00ff_1493),
207	("deepskyblue", 0x0000_bfff),
208	("dimgray", 0x0069_6969),
209	("dimgrey", 0x0069_6969),
210	("dodgerblue", 0x001e_90ff),
211	("firebrick", 0x00b2_2222),
212	("floralwhite", 0x00ff_faf0),
213	("forestgreen", 0x0022_8b22),
214	("fuchsia", 0x00ff_00ff),
215	("gainsboro", 0x00dc_dcdc),
216	("ghostwhite", 0x00f8_f8ff),
217	("gold", 0x00ff_d700),
218	("goldenrod", 0x00da_a520),
219	("gray", 0x0080_8080),
220	("green", 0x0000_8000),
221	("greenyellow", 0x00ad_ff2f),
222	("grey", 0x0080_8080),
223	("honeydew", 0x00f0_fff0),
224	("hotpink", 0x00ff_69b4),
225	("indianred", 0x00cd_5c5c),
226	("indigo", 0x004b_0082),
227	("ivory", 0x00ff_fff0),
228	("khaki", 0x00f0_e68c),
229	("lavender", 0x00e6_e6fa),
230	("lavenderblush", 0x00ff_f0f5),
231	("lawngreen", 0x007c_fc00),
232	("lemonchiffon", 0x00ff_facd),
233	("lightblue", 0x00ad_d8e6),
234	("lightcoral", 0x00f0_8080),
235	("lightcyan", 0x00e0_ffff),
236	("lightgoldenrodyellow", 0x00fa_fad2),
237	("lightgray", 0x00d3_d3d3),
238	("lightgreen", 0x0090_ee90),
239	("lightgrey", 0x00d3_d3d3),
240	("lightpink", 0x00ff_b6c1),
241	("lightsalmon", 0x00ff_a07a),
242	("lightseagreen", 0x0020_b2aa),
243	("lightskyblue", 0x0087_cefa),
244	("lightslategray", 0x0077_8899),
245	("lightslategrey", 0x0077_8899),
246	("lightsteelblue", 0x00b0_c4de),
247	("lightyellow", 0x00ff_ffe0),
248	("lime", 0x0000_ff00),
249	("limegreen", 0x0032_cd32),
250	("linen", 0x00fa_f0e6),
251	("magenta", 0x00ff_00ff),
252	("maroon", 0x0080_0000),
253	("mediumaquamarine", 0x0066_cdaa),
254	("mediumblue", 0x0000_00cd),
255	("mediumorchid", 0x00ba_55d3),
256	("mediumpurple", 0x0093_70db),
257	("mediumseagreen", 0x003c_b371),
258	("mediumslateblue", 0x007b_68ee),
259	("mediumspringgreen", 0x0000_fa9a),
260	("mediumturquoise", 0x0048_d1cc),
261	("mediumvioletred", 0x00c7_1585),
262	("midnightblue", 0x0019_1970),
263	("mintcream", 0x00f5_fffa),
264	("mistyrose", 0x00ff_e4e1),
265	("moccasin", 0x00ff_e4b5),
266	("navajowhite", 0x00ff_dead),
267	("navy", 0x0000_0080),
268	("oldlace", 0x00fd_f5e6),
269	("olive", 0x0080_8000),
270	("olivedrab", 0x006b_8e23),
271	("orange", 0x00ff_a500),
272	("orangered", 0x00ff_4500),
273	("orchid", 0x00da_70d6),
274	("palegoldenrod", 0x00ee_e8aa),
275	("palegreen", 0x0098_fb98),
276	("paleturquoise", 0x00af_eeee),
277	("palevioletred", 0x00db_7093),
278	("papayawhip", 0x00ff_efd5),
279	("peachpuff", 0x00ff_dab9),
280	("peru", 0x00cd_853f),
281	("pink", 0x00ff_c0cb),
282	("plum", 0x00dd_a0dd),
283	("powderblue", 0x00b0_e0e6),
284	("purple", 0x0080_0080),
285	("rebeccapurple", 0x0066_3399),
286	("red", 0x00ff_0000),
287	("rosybrown", 0x00bc_8f8f),
288	("royalblue", 0x0041_69e1),
289	("saddlebrown", 0x008b_4513),
290	("salmon", 0x00fa_8072),
291	("sandybrown", 0x00f4_a460),
292	("seagreen", 0x002e_8b57),
293	("seashell", 0x00ff_f5ee),
294	("sienna", 0x00a0_522d),
295	("silver", 0x00c0_c0c0),
296	("skyblue", 0x0087_ceeb),
297	("slateblue", 0x006a_5acd),
298	("slategray", 0x0070_8090),
299	("slategrey", 0x0070_8090),
300	("snow", 0x00ff_fafa),
301	("springgreen", 0x0000_ff7f),
302	("steelblue", 0x0046_82b4),
303	("tan", 0x00d2_b48c),
304	("teal", 0x0000_8080),
305	("thistle", 0x00d8_bfd8),
306	("tomato", 0x00ff_6347),
307	("turquoise", 0x0040_e0d0),
308	("violet", 0x00ee_82ee),
309	("wheat", 0x00f5_deb3),
310	("white", 0x00ff_ffff),
311	("whitesmoke", 0x00f5_f5f5),
312	("yellow", 0x00ff_ff00),
313	("yellowgreen", 0x009a_cd32),
314];
315
316#[cfg(test)]
317mod tests {
318	use super::*;
319
320	#[test]
321	fn tables_are_sorted_for_binary_search() {
322		assert!(NAMED.windows(2).all(|pair| pair[0].0 < pair[1].0));
323		assert!(SYSTEM.windows(2).all(|pair| pair[0].0 < pair[1].0));
324	}
325
326	#[test]
327	fn keywords_resolve_case_insensitively() {
328		assert_eq!(parse("rebeccapurple"), Some(CssColor::rgb(0x66, 0x33, 0x99)));
329		assert_eq!(parse("WHITE"), Some(CssColor::rgb(255, 255, 255)));
330		assert_eq!(parse("LightGoldenrodYellow"), Some(CssColor::rgb(0xfa, 0xfa, 0xd2)));
331	}
332
333	#[test]
334	fn special_keywords_keep_css_semantics() {
335		assert_eq!(parse("transparent"), Some(CssColor::Rgba(0, 0, 0, 0.0)));
336		assert_eq!(parse("CurrentColor"), Some(CssColor::Current));
337	}
338
339	#[test]
340	fn system_colors_parse_including_deprecated_aliases() {
341		assert_eq!(parse("Canvas"), Some(CssColor::System(SystemColor::Canvas)));
342		assert_eq!(parse("HIGHLIGHT"), Some(CssColor::System(SystemColor::Highlight)));
343		// Deprecated keywords map to their §6.5 modern equivalents.
344		assert_eq!(parse("WindowText"), Some(CssColor::System(SystemColor::CanvasText)));
345		assert_eq!(parse("ThreeDShadow"), Some(CssColor::System(SystemColor::ButtonBorder)));
346		assert_eq!(SystemColor::parse("InfoBackground"), Some(SystemColor::Canvas));
347	}
348
349	#[test]
350	fn system_colors_resolve_through_the_theme() {
351		let theme = Theme::default();
352		assert_eq!(SystemColor::Canvas.resolve(&theme), Color::Default);
353		assert_eq!(SystemColor::CanvasText.resolve(&theme), theme.fg);
354		assert_eq!(SystemColor::LinkText.resolve(&theme), theme.accent);
355		assert_eq!(SystemColor::Mark.resolve(&theme), theme.warn);
356		assert_eq!(SystemColor::GrayText.resolve(&theme), theme.muted);
357		assert_eq!(SystemColor::HighlightText.resolve(&theme), theme.contrast);
358		assert_eq!(SystemColor::Field.resolve(&theme), theme.surface);
359	}
360
361	#[test]
362	fn junk_names_are_rejected() {
363		assert_eq!(parse(""), None);
364		assert_eq!(parse("nosuchcolorname"), None);
365		assert_eq!(parse("lightgoldenrodyellowish"), None);
366		assert_eq!(parse("wh\u{ef}te"), None);
367	}
368}