1use std::sync::Arc;
13
14use laterite_core::i18n::Text;
15
16use crate::AssetUrls;
17
18pub const COMPACT_STYLESHEET: &str = "skins/compact.css";
20
21#[derive(Debug, Clone)]
32pub struct Skin {
33 pub code: &'static str,
36 pub label: Text,
38 pub stylesheet: Option<&'static str>,
41}
42
43#[derive(Debug, Clone)]
46pub struct Active {
47 pub code: Arc<str>,
48 pub stylesheet: Option<String>,
49}
50
51impl Default for Active {
52 fn default() -> Self {
53 Self {
54 code: Arc::from("standard"),
55 stylesheet: None,
56 }
57 }
58}
59
60pub fn builtin() -> Vec<Skin> {
62 vec![
63 Skin {
64 code: "standard",
65 label: Text::new("Standard"),
66 stylesheet: None,
67 },
68 Skin {
69 code: "compact",
70 label: Text::new("Compact"),
71 stylesheet: Some(COMPACT_STYLESHEET),
72 },
73 ]
74}
75
76pub(crate) fn resolve(code: &str, skins: &[Skin], base: &str, urls: &AssetUrls) -> Active {
79 let code = if code.trim().is_empty() {
80 "standard"
81 } else {
82 code.trim()
83 };
84 let Some(skin) = skins.iter().find(|s| s.code == code) else {
85 let known: Vec<&str> = skins.iter().map(|s| s.code).collect();
86 panic!(
87 "unknown admin skin `{code}` (registered: {})",
88 known.join(", ")
89 );
90 };
91 let stylesheet = skin.stylesheet.map(|key| {
92 let Some(path) = urls.get(key) else {
93 panic!("admin skin `{code}` names stylesheet `{key}`, which no module registered");
94 };
95 format!("{base}/assets/{path}")
96 });
97 Active {
98 code: Arc::from(skin.code),
99 stylesheet,
100 }
101}
102
103pub(crate) fn check(skins: &[Skin]) {
105 for (i, skin) in skins.iter().enumerate() {
106 if skins[..i].iter().any(|s| s.code == skin.code) {
107 panic!("duplicate admin skin `{}`", skin.code);
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 fn urls() -> AssetUrls {
117 let mut urls = AssetUrls::new();
118 urls.insert(COMPACT_STYLESHEET, "skins/compact.abc123.css".to_string());
119 urls
120 }
121
122 #[test]
123 fn standard_has_no_stylesheet_and_blank_means_standard() {
124 let active = resolve("", &builtin(), "/admin", &urls());
125 assert_eq!(&*active.code, "standard");
126 assert!(active.stylesheet.is_none());
127 }
128
129 #[test]
130 fn compact_resolves_to_its_fingerprinted_stylesheet() {
131 let active = resolve("compact", &builtin(), "/manage", &urls());
132 assert_eq!(&*active.code, "compact");
133 assert_eq!(
134 active.stylesheet.as_deref(),
135 Some("/manage/assets/skins/compact.abc123.css")
136 );
137 }
138
139 #[test]
140 #[should_panic(expected = "unknown admin skin `dense`")]
141 fn an_unknown_code_aborts() {
142 resolve("dense", &builtin(), "/admin", &urls());
143 }
144
145 #[test]
146 #[should_panic(expected = "which no module registered")]
147 fn a_missing_stylesheet_aborts() {
148 resolve("compact", &builtin(), "/admin", &AssetUrls::new());
149 }
150
151 #[test]
152 #[should_panic(expected = "duplicate admin skin `compact`")]
153 fn a_duplicate_code_aborts() {
154 let mut skins = builtin();
155 skins.push(Skin {
156 code: "compact",
157 label: Text::new("Again"),
158 stylesheet: None,
159 });
160 check(&skins);
161 }
162}