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}