Skip to main content

drmkit_csd/
theme.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! What decorations look like.
5
6use crate::Color;
7
8/// The title bar's geometry and type.
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub struct TitleBar {
12    /// Height in pixels.
13    pub height: i32,
14    /// A CSS-style font stack.
15    pub font: String,
16    /// Point size.
17    pub font_size: i32,
18}
19
20/// Every color a decoration uses.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22#[allow(
23    clippy::exhaustive_structs,
24    reason = "built by callers with `..Default::default()`, which a new field does not break"
25)]
26pub struct Colors {
27    /// Top of the panel gradient.
28    pub panel_top: Color,
29    /// Bottom of it.
30    pub panel_bottom: Color,
31    /// The edge highlight while the window has focus.
32    pub rim_focused: Color,
33    /// And while it does not — the cue that tells a user which window their
34    /// keystrokes are going to.
35    pub rim_blurred: Color,
36    /// The drop shadow.
37    pub shadow: Color,
38    /// Title text.
39    pub title_text: Color,
40    /// The text's own shadow, for legibility over a translucent panel.
41    pub title_shadow: Color,
42}
43
44/// One button's two states.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46#[allow(
47    clippy::exhaustive_structs,
48    reason = "built by callers with `..Default::default()`, which a new field does not break"
49)]
50pub struct Button {
51    /// At rest.
52    pub fill: Color,
53    /// Under the pointer.
54    pub hover: Color,
55}
56
57/// The three window buttons.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59#[allow(
60    clippy::exhaustive_structs,
61    reason = "built by callers with `..Default::default()`, which a new field does not break"
62)]
63pub struct Buttons {
64    /// Close.
65    pub close: Button,
66    /// Minimize.
67    pub minimize: Button,
68    /// Maximize.
69    pub maximize: Button,
70}
71
72/// A complete decoration theme.
73#[derive(Debug, Clone, PartialEq)]
74#[allow(
75    clippy::exhaustive_structs,
76    reason = "built by callers with `..Default::default()`, which a new field does not break"
77)]
78pub struct Theme {
79    /// What to call it.
80    pub name: String,
81    /// Corner rounding, in pixels.
82    pub corner_radius: i32,
83    /// The title bar.
84    pub title_bar: TitleBar,
85    /// Colors.
86    pub colors: Colors,
87    /// Buttons.
88    pub buttons: Buttons,
89    /// How much dither to mix into the panel gradient, 0 to 1.
90    ///
91    /// A flat gradient across a wide title bar bands visibly at 8 bits per
92    /// channel; a little noise breaks the bands up. Costs a per-pixel
93    /// multiply, which is why a cheaper theme turns it down.
94    pub noise_amplitude: f64,
95    /// How far the shadow reaches, in pixels. Zero means none.
96    pub shadow_extent: i32,
97    /// How long a decoration animation runs. Zero means none.
98    pub animation_duration_ms: i32,
99}
100
101impl Default for Theme {
102    fn default() -> Self {
103        Self {
104            name: String::new(),
105            corner_radius: 0,
106            title_bar: TitleBar {
107                height: 0,
108                font: String::new(),
109                font_size: 0,
110            },
111            colors: Colors::default(),
112            buttons: Buttons::default(),
113            noise_amplitude: 0.0,
114            shadow_extent: 0,
115            animation_duration_ms: 0,
116        }
117    }
118}
119
120/// The reference theme.
121#[must_use]
122pub fn glass_default() -> Theme {
123    Theme {
124        name: "glass-default".to_owned(),
125        corner_radius: 8,
126        title_bar: TitleBar {
127            height: 28,
128            font: "Inter, sans-serif".to_owned(),
129            font_size: 13,
130        },
131        colors: Colors {
132            panel_top: Color::new(0xFF, 0xFF, 0xFF, 0x73),
133            panel_bottom: Color::new(0xFF, 0xFF, 0xFF, 0x26),
134            rim_focused: Color::new(0xFF, 0xFF, 0xFF, 0x99),
135            rim_blurred: Color::new(0xC8, 0xC8, 0xC8, 0x4C),
136            shadow: Color::new(0x00, 0x00, 0x00, 0x8C),
137            title_text: Color::new(0x1E, 0x1E, 0x1E, 0xFF),
138            title_shadow: Color::new(0xFF, 0xFF, 0xFF, 0x66),
139        },
140        buttons: Buttons {
141            close: Button {
142                fill: Color::new(0xFF, 0x5F, 0x56, 0xFF),
143                hover: Color::new(0xFF, 0x8A, 0x82, 0xFF),
144            },
145            minimize: Button {
146                fill: Color::new(0xFF, 0xBD, 0x2E, 0xFF),
147                hover: Color::new(0xFF, 0xD1, 0x66, 0xFF),
148            },
149            maximize: Button {
150                fill: Color::new(0x28, 0xC9, 0x40, 0xFF),
151                hover: Color::new(0x5E, 0xE0, 0x71, 0xFF),
152            },
153        },
154        noise_amplitude: 0.04,
155        shadow_extent: 24,
156        animation_duration_ms: 180,
157    }
158}
159
160/// The same look, cheaper to draw.
161///
162/// Less shadow, less noise, shorter animations — the three costs that scale
163/// with area and frame count. For a device where the decoration competes with
164/// the application for the same CPU.
165#[must_use]
166pub fn glass_lite() -> Theme {
167    Theme {
168        name: "glass-lite".to_owned(),
169        noise_amplitude: 0.02,
170        shadow_extent: 12,
171        animation_duration_ms: 120,
172        ..glass_default()
173    }
174}
175
176/// No shadow, no animation, no noise.
177///
178/// The shadow color is cleared as well as the extent. Either alone would
179/// stop it drawing, but leaving a visible color behind a zero extent invites
180/// a caller that raises the extent to inherit a shadow it never chose.
181#[must_use]
182pub fn glass_minimal() -> Theme {
183    let base = glass_default();
184    Theme {
185        name: "glass-minimal".to_owned(),
186        noise_amplitude: 0.0,
187        shadow_extent: 0,
188        animation_duration_ms: 0,
189        colors: Colors {
190            shadow: Color::default(),
191            ..base.colors
192        },
193        ..base
194    }
195}