1use std::borrow::Cow;
8
9use iced::theme::Palette;
10use iced::theme::palette::{Extended, Pair, Secondary, deviate};
11use iced::{Color, Theme, color};
12
13pub mod radius {
15 pub const SM: f32 = 4.0;
16 pub const MD: f32 = 6.0;
17 pub const LG: f32 = 8.0;
18 pub const FULL: f32 = 9999.0;
19}
20
21pub mod space {
23 pub const XS: f32 = 4.0;
24 pub const SM: f32 = 8.0;
25 pub const MD: f32 = 12.0;
26 pub const LG: f32 = 16.0;
27 pub const XL: f32 = 24.0;
28}
29
30pub mod text_size {
32 pub const XS: f32 = 12.0;
33 pub const SM: f32 = 14.0;
34 pub const MD: f32 = 16.0;
35 pub const LG: f32 = 18.0;
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct Tokens {
41 pub background: Color,
42 pub foreground: Color,
43 pub muted: Color,
44 pub muted_foreground: Color,
45 pub accent: Color,
46 pub accent_foreground: Color,
47 pub border: Color,
48 pub ring: Color,
49 pub primary: Color,
50 pub primary_foreground: Color,
51 pub secondary: Color,
52 pub secondary_foreground: Color,
53 pub destructive: Color,
54 pub destructive_foreground: Color,
55 pub success: Color,
56 pub warning: Color,
57 pub is_dark: bool,
58}
59
60impl Tokens {
61 pub fn of(theme: &Theme) -> Self {
63 let ext = theme.extended_palette();
64 let background = ext.background.base.color;
65 let foreground = ext.background.base.text;
66
67 Self {
68 background,
69 foreground,
70 muted: ext.background.weak.color,
71 muted_foreground: mix(foreground, background, 0.4),
72 accent: ext.secondary.weak.color,
73 accent_foreground: on(ext.secondary.weak.color),
74 border: ext.background.strong.color,
75 ring: Color {
76 a: 0.5,
77 ..ext.primary.base.color
78 },
79 primary: ext.primary.base.color,
80 primary_foreground: on(ext.primary.base.color),
81 secondary: ext.secondary.base.color,
82 secondary_foreground: on(ext.secondary.base.color),
83 destructive: ext.danger.base.color,
84 destructive_foreground: on(ext.danger.base.color),
85 success: ext.success.base.color,
86 warning: ext.warning.base.color,
87 is_dark: ext.is_dark,
88 }
89 }
90}
91
92pub fn light() -> Theme {
94 Config::light().build()
95}
96
97pub fn dark() -> Theme {
99 Config::dark().build()
100}
101
102#[derive(Debug, Clone, PartialEq)]
119pub struct Config {
120 name: Cow<'static, str>,
121 background: Color,
122 foreground: Color,
123 primary: Color,
124 secondary: Option<Color>,
125 accent: Option<Color>,
126 destructive: Color,
127 success: Color,
128 warning: Color,
129 border: Option<Color>,
130}
131
132impl Config {
133 pub fn light() -> Self {
135 Self {
136 name: Cow::Borrowed("Cube Light"),
137 background: color!(0xffffff),
138 foreground: color!(0x09090b),
139 primary: color!(0x18181b),
140 secondary: None,
141 accent: None,
142 destructive: color!(0xdc2626),
143 success: color!(0x16a34a),
144 warning: color!(0xd97706),
145 border: None,
146 }
147 }
148
149 pub fn dark() -> Self {
151 Self {
152 name: Cow::Borrowed("Cube Dark"),
153 background: color!(0x09090b),
154 foreground: color!(0xfafafa),
155 primary: color!(0xfafafa),
156 secondary: None,
157 accent: None,
158 destructive: color!(0xdc2626),
159 success: color!(0x22c55e),
160 warning: color!(0xf59e0b),
161 border: None,
162 }
163 }
164
165 pub fn name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
167 self.name = name.into();
168 self
169 }
170
171 pub fn primary(mut self, colour: Color) -> Self {
173 self.primary = colour;
174 self
175 }
176
177 pub fn secondary(mut self, colour: Color) -> Self {
179 self.secondary = Some(colour);
180 self
181 }
182
183 pub fn accent(mut self, colour: Color) -> Self {
186 self.accent = Some(colour);
187 self
188 }
189
190 pub fn destructive(mut self, colour: Color) -> Self {
192 self.destructive = colour;
193 self
194 }
195
196 pub fn success(mut self, colour: Color) -> Self {
197 self.success = colour;
198 self
199 }
200
201 pub fn warning(mut self, colour: Color) -> Self {
202 self.warning = colour;
203 self
204 }
205
206 pub fn background(mut self, colour: Color) -> Self {
209 self.background = colour;
210 self
211 }
212
213 pub fn foreground(mut self, colour: Color) -> Self {
215 self.foreground = colour;
216 self
217 }
218
219 pub fn border(mut self, colour: Color) -> Self {
222 self.border = Some(colour);
223 self
224 }
225
226 pub fn palette(&self) -> Palette {
228 Palette {
229 background: self.background,
230 text: self.foreground,
231 primary: self.primary,
232 success: self.success,
233 warning: self.warning,
234 danger: self.destructive,
235 }
236 }
237
238 pub fn extended(&self) -> Extended {
242 let mut extended = Extended::generate(self.palette());
243 let shade = extended.background.weak.color;
244 let secondary = self.secondary.unwrap_or(shade);
245 let accent = self.accent.unwrap_or(shade);
246 let strong = deviate(secondary, 0.1);
247
248 extended.secondary = Secondary {
249 base: Pair::new(secondary, on(secondary)),
250 weak: Pair::new(accent, on(accent)),
251 strong: Pair::new(strong, on(strong)),
252 };
253 if let Some(border) = self.border {
254 extended.background.strong = Pair::new(border, self.foreground);
255 }
256 extended
257 }
258
259 pub fn build(&self) -> Theme {
261 let extended = self.extended();
262 Theme::custom_with_fn(self.name.clone(), self.palette(), move |_| extended)
263 }
264}
265
266pub fn mix(a: Color, b: Color, t: f32) -> Color {
268 let t = t.clamp(0.0, 1.0);
269 Color {
270 r: a.r + (b.r - a.r) * t,
271 g: a.g + (b.g - a.g) * t,
272 b: a.b + (b.b - a.b) * t,
273 a: a.a + (b.a - a.a) * t,
274 }
275}
276
277pub fn on(background: Color) -> Color {
279 let luminance = 0.2126 * background.r + 0.7152 * background.g + 0.0722 * background.b;
280 if luminance > 0.55 {
281 color!(0x09090b)
282 } else {
283 color!(0xfafafa)
284 }
285}
286
287pub fn fade(color: Color, alpha: f32) -> Color {
289 Color {
290 a: color.a * alpha.clamp(0.0, 1.0),
291 ..color
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn mix_endpoints_return_inputs() {
301 let a = Color::from_rgb(0.0, 0.2, 0.4);
302 let b = Color::from_rgb(1.0, 0.8, 0.6);
303 assert_eq!(mix(a, b, 0.0), a);
304 assert_eq!(mix(a, b, 1.0), b);
305 }
306
307 #[test]
308 fn mix_clamps_out_of_range_factor() {
309 let a = Color::BLACK;
310 let b = Color::WHITE;
311 assert_eq!(mix(a, b, -1.0), a);
312 assert_eq!(mix(a, b, 2.0), b);
313 }
314
315 #[test]
316 fn fade_scales_alpha_only() {
317 let faded = fade(Color::from_rgba(0.1, 0.2, 0.3, 0.8), 0.5);
318 assert_eq!((faded.r, faded.g, faded.b), (0.1, 0.2, 0.3));
319 assert!((faded.a - 0.4).abs() < f32::EPSILON);
320 }
321
322 #[test]
323 fn on_picks_dark_text_for_light_backgrounds() {
324 assert_eq!(on(Color::WHITE), color!(0x09090b));
325 assert_eq!(on(Color::BLACK), color!(0xfafafa));
326 assert_eq!(on(color!(0xdc2626)), color!(0xfafafa));
327 }
328
329 #[test]
330 fn light_and_dark_report_their_mode() {
331 assert!(!Tokens::of(&light()).is_dark);
332 assert!(Tokens::of(&dark()).is_dark);
333 }
334
335 #[test]
336 fn primary_foreground_contrasts_with_primary() {
337 for theme in [light(), dark()] {
338 let tokens = Tokens::of(&theme);
339 assert_ne!(tokens.primary, tokens.primary_foreground);
340 assert_ne!(tokens.background, tokens.foreground);
341 }
342 }
343
344 #[test]
345 fn defaults_match_the_original_palettes() {
346 let light = Tokens::of(&light());
347 assert_eq!(light.background, color!(0xffffff));
348 assert_eq!(light.foreground, color!(0x09090b));
349 assert_eq!(light.primary, color!(0x18181b));
350 assert_eq!(light.destructive, color!(0xdc2626));
351 assert_eq!(light.secondary, light.muted);
352 assert_eq!(light.accent, light.muted);
353 assert_eq!(light.secondary_foreground, light.foreground);
354 assert_eq!(light.accent_foreground, light.foreground);
355
356 let dark = Tokens::of(&dark());
357 assert_eq!(dark.background, color!(0x09090b));
358 assert_eq!(dark.primary, color!(0xfafafa));
359 assert_eq!(dark.success, color!(0x22c55e));
360 assert_eq!(dark.secondary, dark.muted);
361 assert_eq!(dark.secondary_foreground, dark.foreground);
362 assert_eq!(dark.accent_foreground, dark.foreground);
363 }
364
365 #[test]
366 fn defaults_keep_the_generated_border() {
367 for config in [Config::light(), Config::dark()] {
368 let generated = Extended::generate(config.palette());
369 let tokens = Tokens::of(&config.build());
370 assert_eq!(tokens.border, generated.background.strong.color);
371 assert_eq!(tokens.muted, generated.background.weak.color);
372 }
373 }
374
375 #[test]
376 fn config_colours_reach_tokens() {
377 let theme = Config::light()
378 .name("Brand")
379 .primary(color!(0x4f46e5))
380 .secondary(color!(0xe0e7ff))
381 .accent(color!(0xfef3c7))
382 .destructive(color!(0xbe123c))
383 .success(color!(0x059669))
384 .warning(color!(0xca8a04))
385 .background(color!(0xfafaf9))
386 .foreground(color!(0x1c1917))
387 .border(color!(0xd6d3d1))
388 .build();
389 let tokens = Tokens::of(&theme);
390
391 assert_eq!(theme.to_string(), "Brand");
392 assert_eq!(tokens.primary, color!(0x4f46e5));
393 assert_eq!(tokens.primary_foreground, on(color!(0x4f46e5)));
394 assert_eq!(tokens.secondary, color!(0xe0e7ff));
395 assert_eq!(tokens.secondary_foreground, on(color!(0xe0e7ff)));
396 assert_eq!(tokens.accent, color!(0xfef3c7));
397 assert_eq!(tokens.destructive, color!(0xbe123c));
398 assert_eq!(tokens.success, color!(0x059669));
399 assert_eq!(tokens.warning, color!(0xca8a04));
400 assert_eq!(tokens.background, color!(0xfafaf9));
401 assert_eq!(tokens.foreground, color!(0x1c1917));
402 assert_eq!(tokens.border, color!(0xd6d3d1));
403 }
404
405 #[test]
406 fn unset_surfaces_follow_a_new_background() {
407 let background = color!(0xf5f5f4);
408 let tokens = Tokens::of(&Config::light().background(background).build());
409 let shade = Extended::generate(Palette {
410 background,
411 ..Config::light().palette()
412 })
413 .background
414 .weak
415 .color;
416 assert_eq!(tokens.secondary, shade);
417 assert_eq!(tokens.accent, shade);
418 }
419
420 #[test]
421 fn dark_mode_follows_the_background() {
422 assert!(!Tokens::of(&Config::light().build()).is_dark);
423 assert!(Tokens::of(&Config::dark().build()).is_dark);
424 let night = Config::light()
425 .background(color!(0x0f172a))
426 .foreground(color!(0xf8fafc))
427 .build();
428 assert!(Tokens::of(&night).is_dark);
429 let paper = Config::dark()
430 .background(color!(0xfffbeb))
431 .foreground(color!(0x292524))
432 .build();
433 assert!(!Tokens::of(&paper).is_dark);
434 }
435
436 #[test]
437 fn tokens_resolve_for_builtin_iced_themes() {
438 for theme in Theme::ALL {
439 let tokens = Tokens::of(theme);
440 assert_ne!(tokens.background, tokens.foreground, "{theme}");
441 }
442 }
443}