ailloli_ui_core/style/theme.rs
1//! Built-in dark/orange semantic colors, spacing, typography, radius, and shadows.
2
3use crate::FontId;
4
5use super::{BoxShadow, Color, Radius, TextStyle};
6
7/// Semantic color tokens for built-in widgets.
8///
9/// # Examples
10///
11/// ```
12/// use ailloli_ui_core::style::Theme;
13/// let palette = Theme::dark().palette();
14/// assert_ne!(palette.text, palette.background);
15/// ```
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct ThemePalette {
18 /// Base canvas behind ordinary content.
19 pub background: Color,
20 /// Translucent or low-elevation control/panel surface.
21 pub surface: Color,
22 /// Opaque elevated overlay or panel surface.
23 pub surface_elevated: Color,
24 /// Standard separator and outline color.
25 pub border: Color,
26 /// Primary readable text color.
27 pub text: Color,
28 /// Secondary or de-emphasized text color.
29 pub text_muted: Color,
30 /// Primary brand/action color.
31 pub accent: Color,
32 /// Destructive action and error color.
33 pub danger: Color,
34 /// Successful state color.
35 pub success: Color,
36 /// Warning state color.
37 pub warning: Color,
38 /// Informational state color.
39 pub info: Color,
40 /// Keyboard-focus indicator color.
41 pub focus: Color,
42}
43
44/// Standard radii used by native widgets.
45///
46/// # Examples
47///
48/// ```
49/// use ailloli_ui_core::style::Theme;
50/// assert_eq!(Theme::dark().radius().md, 8.0);
51/// ```
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct ThemeRadius {
54 /// Small radius in logical pixels.
55 pub sm: f32,
56 /// Medium radius in logical pixels.
57 pub md: f32,
58 /// Large radius in logical pixels.
59 pub lg: f32,
60 /// Extra-large radius in logical pixels.
61 pub xl: f32,
62}
63
64impl ThemeRadius {
65 /// Returns the uniform medium radius used by buttons.
66 ///
67 /// # Examples
68 ///
69 /// ```
70 /// use ailloli_ui_core::style::{Radius, Theme};
71 /// assert_eq!(Theme::dark().radius().button(), Radius::uniform(8.0));
72 /// ```
73 pub const fn button(self) -> Radius {
74 Radius::uniform(self.md)
75 }
76
77 /// Returns the uniform medium radius used by input controls.
78 ///
79 /// # Examples
80 ///
81 /// ```
82 /// use ailloli_ui_core::style::{Radius, Theme};
83 /// assert_eq!(Theme::dark().radius().input(), Radius::uniform(8.0));
84 /// ```
85 pub const fn input(self) -> Radius {
86 Radius::uniform(self.md)
87 }
88
89 /// Returns the uniform large radius used by panels.
90 ///
91 /// # Examples
92 ///
93 /// ```
94 /// use ailloli_ui_core::style::{Radius, Theme};
95 /// assert_eq!(Theme::dark().radius().panel(), Radius::uniform(12.0));
96 /// ```
97 pub const fn panel(self) -> Radius {
98 Radius::uniform(self.lg)
99 }
100}
101
102/// Standard spacing scale in logical pixels.
103///
104/// # Examples
105///
106/// ```
107/// use ailloli_ui_core::style::Theme;
108/// assert_eq!(Theme::dark().spacing().xl, 24.0);
109/// ```
110#[derive(Debug, Clone, Copy, PartialEq)]
111pub struct ThemeSpacing {
112 /// Extra-small spacing in logical pixels.
113 pub xs: f32,
114 /// Small spacing in logical pixels.
115 pub sm: f32,
116 /// Medium spacing in logical pixels.
117 pub md: f32,
118 /// Large spacing in logical pixels.
119 pub lg: f32,
120 /// Extra-large spacing in logical pixels.
121 pub xl: f32,
122}
123
124/// Standard text styles used by native widgets and showcases.
125///
126/// # Examples
127///
128/// ```
129/// use ailloli_ui_core::style::Theme;
130/// assert_eq!(Theme::dark().typography().ui_md.px_size, 14);
131/// ```
132#[derive(Debug, Clone, Copy, PartialEq)]
133pub struct ThemeTypography {
134 /// Small UI sans-serif text.
135 pub ui_sm: TextStyle,
136 /// Default UI sans-serif text.
137 pub ui_md: TextStyle,
138 /// Large UI sans-serif text.
139 pub ui_lg: TextStyle,
140 /// Default monospace text for code and terminals.
141 pub mono_md: TextStyle,
142}
143
144/// Standard shadow presets for native boxes.
145///
146/// # Examples
147///
148/// ```
149/// use ailloli_ui_core::style::Theme;
150/// assert_eq!(Theme::dark().shadows().md.blur_radius, 12.0);
151/// ```
152#[derive(Debug, Clone, Copy, PartialEq)]
153pub struct ThemeShadows {
154 /// Small elevation shadow.
155 pub sm: BoxShadow,
156 /// Medium elevation shadow.
157 pub md: BoxShadow,
158 /// Large elevation shadow.
159 pub lg: BoxShadow,
160}
161
162/// Shared semantic widget states for theme-driven controls.
163///
164/// Values cover normal, hover, press, focus, disabled, selected, and invalid
165/// control states.
166///
167/// # Examples
168///
169/// ```
170/// use ailloli_ui_core::style::ThemeState;
171/// assert_ne!(ThemeState::Normal, ThemeState::Invalid);
172/// ```
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum ThemeState {
175 /// Idle enabled state.
176 Normal,
177 /// Pointer is over the control.
178 Hovered,
179 /// An activation gesture is held.
180 Pressed,
181 /// Control owns keyboard focus.
182 Focused,
183 /// Control does not accept interaction.
184 Disabled,
185 /// Control or option is selected.
186 Selected,
187 /// Current value failed validation.
188 Invalid,
189}
190
191/// Semantic color tokens for built-in chrome and controls.
192///
193/// # Examples
194///
195/// ```
196/// use ailloli_ui_core::style::Theme;
197/// let theme = Theme::dark();
198/// assert_ne!(theme.button_bg, theme.window_bg);
199/// ```
200#[derive(Debug, Clone, Copy)]
201pub struct Theme {
202 /// Client window background.
203 pub window_bg: Color,
204 /// Title bar background (client chrome).
205 pub titlebar_bg: Color,
206
207 /// Normal action-button fill.
208 pub button_bg: Color,
209 /// Action-button fill while hovered.
210 pub button_bg_hover: Color,
211 /// Action-button fill while pressed.
212 pub button_bg_pressed: Color,
213
214 /// Destructive close-control fill while hovered.
215 pub close_bg_hover: Color,
216 /// Destructive close-control fill while pressed.
217 pub close_bg_pressed: Color,
218
219 /// Hover fill for minimize/maximize title-bar controls.
220 pub titlebar_control_hover: Color,
221 /// Pressed fill for minimize/maximize title-bar controls.
222 pub titlebar_control_pressed: Color,
223
224 /// Default icon foreground on chrome.
225 pub icon_fg: Color,
226}
227
228impl Theme {
229 /// Returns the built-in dark theme with orange action accents.
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use ailloli_ui_core::style::Theme;
235 /// assert_eq!(Theme::dark().button_bg.as_rgba8(), (255, 90, 0, 255));
236 /// ```
237 pub fn dark() -> Self {
238 Self {
239 window_bg: Color::rgba(9, 11, 12, 1.0),
240 titlebar_bg: Color::rgba(17, 20, 22, 1.0),
241
242 button_bg: Color::rgba(255, 90, 0, 1.0),
243 button_bg_hover: Color::rgba(255, 111, 26, 1.0),
244 button_bg_pressed: Color::rgba(217, 72, 0, 1.0),
245
246 close_bg_hover: Color::rgba(239, 68, 68, 1.0),
247 close_bg_pressed: Color::rgba(185, 28, 28, 1.0),
248
249 titlebar_control_hover: Color::rgba(23, 26, 29, 1.0),
250 titlebar_control_pressed: Color::rgba(32, 37, 42, 1.0),
251
252 icon_fg: Color::rgba(244, 247, 248, 1.0),
253 }
254 }
255
256 /// Returns the built-in semantic color palette.
257 ///
258 /// The current palette is a fixed bundle and is not recomputed from
259 /// mutable public chrome fields on `Theme`.
260 ///
261 /// # Examples
262 ///
263 /// ```
264 /// use ailloli_ui_core::style::Theme;
265 /// assert_eq!(Theme::dark().palette().accent.as_rgba8(), (255, 90, 0, 255));
266 /// ```
267 pub fn palette(self) -> ThemePalette {
268 ThemePalette {
269 background: Color::rgba(22, 22, 22, 1.0),
270 surface: Color::rgba(17, 20, 22, 0.5),
271 surface_elevated: Color::rgba(23, 26, 29, 1.0),
272 border: Color::rgba(42, 47, 52, 1.0),
273 text: Color::rgba(244, 247, 248, 1.0),
274 text_muted: Color::rgba(139, 148, 158, 1.0),
275 accent: Color::rgba(255, 90, 0, 1.0),
276 danger: Color::rgba(239, 68, 68, 1.0),
277 success: Color::rgba(34, 197, 94, 1.0),
278 warning: Color::rgba(245, 158, 11, 1.0),
279 info: Color::rgba(56, 189, 248, 1.0),
280 focus: Color::rgba(255, 138, 61, 1.0),
281 }
282 }
283
284 /// Returns the fixed `4/8/12/16` logical-pixel radius scale.
285 ///
286 /// # Examples
287 ///
288 /// ```
289 /// use ailloli_ui_core::style::Theme;
290 /// assert_eq!((Theme::dark().radius().sm, Theme::dark().radius().xl), (4.0, 16.0));
291 /// ```
292 pub const fn radius(self) -> ThemeRadius {
293 ThemeRadius {
294 sm: 4.0,
295 md: 8.0,
296 lg: 12.0,
297 xl: 16.0,
298 }
299 }
300
301 /// Returns the fixed `4/8/12/16/24` logical-pixel spacing scale.
302 ///
303 /// # Examples
304 ///
305 /// ```
306 /// use ailloli_ui_core::style::Theme;
307 /// assert_eq!(Theme::dark().spacing().md, 12.0);
308 /// ```
309 pub const fn spacing(self) -> ThemeSpacing {
310 ThemeSpacing {
311 xs: 4.0,
312 sm: 8.0,
313 md: 12.0,
314 lg: 16.0,
315 xl: 24.0,
316 }
317 }
318
319 /// Builds standard text styles using the built-in palette text color.
320 ///
321 /// Sizes are 12, 14, and 18 logical pixels for UI text and 14 for
322 /// monospace text.
323 ///
324 /// # Examples
325 ///
326 /// ```
327 /// use ailloli_ui_core::FontId;
328 /// use ailloli_ui_core::style::Theme;
329 /// assert_eq!(Theme::dark().typography().mono_md.font, FontId::Mono);
330 /// ```
331 pub fn typography(self) -> ThemeTypography {
332 let palette = self.palette();
333 ThemeTypography {
334 ui_sm: TextStyle::new(FontId::Ui, 12, palette.text),
335 ui_md: TextStyle::new(FontId::Ui, 14, palette.text),
336 ui_lg: TextStyle::new(FontId::Ui, 18, palette.text),
337 mono_md: TextStyle::new(FontId::Mono, 14, palette.text),
338 }
339 }
340
341 /// Returns small, medium, and large [`BoxShadow`] presets.
342 ///
343 /// # Examples
344 ///
345 /// ```
346 /// use ailloli_ui_core::style::Theme;
347 /// assert_eq!(Theme::dark().shadows().lg.blur_radius, 24.0);
348 /// ```
349 pub fn shadows(self) -> ThemeShadows {
350 ThemeShadows {
351 sm: BoxShadow::sm(),
352 md: BoxShadow::md(),
353 lg: BoxShadow::lg(),
354 }
355 }
356}
357
358impl Default for Theme {
359 fn default() -> Self {
360 Self::dark()
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 //! Locks the built-in palette and native-widget scale values.
367
368 use super::*;
369
370 #[test]
371 fn default_theme_uses_dark_orange_bundle_tokens() {
372 let theme = Theme::default();
373 let palette = theme.palette();
374
375 assert_eq!(theme.window_bg, Color::rgba(9, 11, 12, 1.0));
376 assert_eq!(palette.background, Color::rgba(22, 22, 22, 1.0));
377 assert_eq!(palette.surface, Color::rgba(17, 20, 22, 0.5));
378 assert_eq!(palette.surface_elevated, Color::rgba(23, 26, 29, 1.0));
379 assert_eq!(palette.border, Color::rgba(42, 47, 52, 1.0));
380 assert_eq!(palette.text, Color::rgba(244, 247, 248, 1.0));
381 assert_eq!(palette.text_muted, Color::rgba(139, 148, 158, 1.0));
382 assert_eq!(palette.accent, Color::rgba(255, 90, 0, 1.0));
383 }
384
385 #[test]
386 fn theme_scales_are_stable_for_native_widgets() {
387 let theme = Theme::default();
388
389 assert_eq!(theme.radius().button(), Radius::uniform(8.0));
390 assert_eq!(theme.radius().panel(), Radius::uniform(12.0));
391 assert_eq!(theme.spacing().md, 12.0);
392 assert_eq!(theme.typography().ui_md.px_size, 14);
393 assert_eq!(theme.typography().mono_md.font, FontId::Mono);
394 assert_eq!(theme.shadows().md.blur_radius, BoxShadow::md().blur_radius);
395 }
396}