Skip to main content

egui_components_theme/
theme.rs

1use crate::tokens::ThemeColor;
2use egui::{Color32, CornerRadius, Stroke};
3
4/// Sizing / spacing / radius / typography tokens.
5///
6/// These mirror gpui-component's per-component sizing constants but are
7/// centralized here so the look stays coherent across components.
8#[derive(Clone, Copy, Debug)]
9pub struct ThemeMetrics {
10    pub radius: f32,
11    pub radius_sm: f32,
12    pub radius_lg: f32,
13    pub border_width: f32,
14    pub focus_ring_width: f32,
15    pub button_height_sm: f32,
16    pub button_height_md: f32,
17    pub button_height_lg: f32,
18    pub button_padding_x_sm: f32,
19    pub button_padding_x_md: f32,
20    pub button_padding_x_lg: f32,
21    pub input_height: f32,
22    pub input_height_sm: f32,
23    pub input_height_lg: f32,
24    pub input_padding_x: f32,
25    pub switch_width: f32,
26    pub switch_height: f32,
27    pub switch_thumb_padding: f32,
28    pub checkbox_size: f32,
29    pub slider_thumb_radius: f32,
30    pub slider_track_height: f32,
31    pub font_size_xs: f32,
32    pub font_size_sm: f32,
33    pub font_size_md: f32,
34    pub font_size_lg: f32,
35}
36
37impl Default for ThemeMetrics {
38    fn default() -> Self {
39        Self {
40            radius: 6.0,
41            radius_sm: 4.0,
42            radius_lg: 8.0,
43            border_width: 1.0,
44            focus_ring_width: 2.0,
45            button_height_sm: 28.0,
46            button_height_md: 34.0,
47            button_height_lg: 40.0,
48            button_padding_x_sm: 10.0,
49            button_padding_x_md: 14.0,
50            button_padding_x_lg: 18.0,
51            input_height: 34.0,
52            input_height_sm: 28.0,
53            input_height_lg: 40.0,
54            input_padding_x: 10.0,
55            switch_width: 36.0,
56            switch_height: 20.0,
57            switch_thumb_padding: 2.0,
58            checkbox_size: 16.0,
59            slider_thumb_radius: 9.0,
60            slider_track_height: 6.0,
61            font_size_xs: 11.0,
62            font_size_sm: 13.0,
63            font_size_md: 14.0,
64            font_size_lg: 16.0,
65        }
66    }
67}
68
69/// The full theme: colors + metrics + mode flag.
70#[derive(Clone, Copy, Debug)]
71pub struct Theme {
72    pub mode: ThemeMode,
73    pub colors: ThemeColor,
74    pub metrics: ThemeMetrics,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum ThemeMode {
79    Light,
80    Dark,
81}
82
83impl Theme {
84    pub const fn light() -> Self {
85        Self {
86            mode: ThemeMode::Light,
87            colors: ThemeColor::light(),
88            metrics: ThemeMetrics {
89                radius: 6.0,
90                radius_sm: 4.0,
91                radius_lg: 8.0,
92                border_width: 1.0,
93                focus_ring_width: 2.0,
94                button_height_sm: 28.0,
95                button_height_md: 34.0,
96                button_height_lg: 40.0,
97                button_padding_x_sm: 10.0,
98                button_padding_x_md: 14.0,
99                button_padding_x_lg: 18.0,
100                input_height: 34.0,
101                input_height_sm: 28.0,
102                input_height_lg: 40.0,
103                input_padding_x: 10.0,
104                switch_width: 36.0,
105                switch_height: 20.0,
106                switch_thumb_padding: 2.0,
107                checkbox_size: 16.0,
108                slider_thumb_radius: 9.0,
109                slider_track_height: 6.0,
110                font_size_xs: 11.0,
111                font_size_sm: 13.0,
112                font_size_md: 14.0,
113                font_size_lg: 16.0,
114            },
115        }
116    }
117
118    pub const fn dark() -> Self {
119        Self {
120            mode: ThemeMode::Dark,
121            colors: ThemeColor::dark(),
122            metrics: Self::light().metrics,
123        }
124    }
125
126    pub fn corner(&self) -> CornerRadius {
127        CornerRadius::same(self.metrics.radius as u8)
128    }
129
130    pub fn corner_sm(&self) -> CornerRadius {
131        CornerRadius::same(self.metrics.radius_sm as u8)
132    }
133
134    pub fn corner_lg(&self) -> CornerRadius {
135        CornerRadius::same(self.metrics.radius_lg as u8)
136    }
137
138    pub fn border_stroke(&self) -> Stroke {
139        Stroke::new(self.metrics.border_width, self.colors.border)
140    }
141
142    pub fn focus_ring(&self) -> Stroke {
143        Stroke::new(self.metrics.focus_ring_width, self.colors.ring)
144    }
145
146    pub fn input_border_stroke(&self) -> Stroke {
147        Stroke::new(self.metrics.border_width, self.colors.input_border)
148    }
149
150    /// Push the theme's typography / colors into the supplied [`egui::Style`].
151    ///
152    /// Components do *not* require this — they read [`Theme`] directly via
153    /// [`Self::install`] so they look correct even when the host app uses its
154    /// own egui style. This is a convenience that makes plain `egui::Label`
155    /// and built-in widgets blend in with the theme palette.
156    pub fn apply_to_style(&self, style: &mut egui::Style) {
157        let c = &self.colors;
158        let visuals = &mut style.visuals;
159
160        visuals.dark_mode = matches!(self.mode, ThemeMode::Dark);
161        visuals.override_text_color = Some(c.foreground);
162        visuals.window_fill = c.popover_background;
163        visuals.panel_fill = c.background;
164        visuals.faint_bg_color = c.muted_background;
165        visuals.extreme_bg_color = c.background;
166        visuals.code_bg_color = c.muted_background;
167        visuals.window_stroke = self.border_stroke();
168        visuals.selection.bg_fill = c.selection_background;
169        visuals.selection.stroke = Stroke::new(1.0, c.primary_foreground);
170        visuals.hyperlink_color = c.link_foreground;
171
172        // Widget visuals — only used by built-in widgets; our own widgets paint manually.
173        let radius = self.corner();
174        visuals.widgets.noninteractive.bg_fill = c.background;
175        visuals.widgets.noninteractive.weak_bg_fill = c.muted_background;
176        visuals.widgets.noninteractive.bg_stroke = self.border_stroke();
177        visuals.widgets.noninteractive.fg_stroke = Stroke::new(1.0, c.foreground);
178        visuals.widgets.noninteractive.corner_radius = radius;
179
180        visuals.widgets.inactive.bg_fill = c.secondary_background;
181        visuals.widgets.inactive.weak_bg_fill = c.secondary_background;
182        visuals.widgets.inactive.bg_stroke = Stroke::NONE;
183        visuals.widgets.inactive.fg_stroke = Stroke::new(1.0, c.secondary_foreground);
184        visuals.widgets.inactive.corner_radius = radius;
185
186        visuals.widgets.hovered.bg_fill = c.secondary_hover_background;
187        visuals.widgets.hovered.weak_bg_fill = c.secondary_hover_background;
188        visuals.widgets.hovered.bg_stroke = Stroke::new(1.0, c.border);
189        visuals.widgets.hovered.fg_stroke = Stroke::new(1.0, c.secondary_foreground);
190        visuals.widgets.hovered.corner_radius = radius;
191
192        visuals.widgets.active.bg_fill = c.secondary_active_background;
193        visuals.widgets.active.weak_bg_fill = c.secondary_active_background;
194        visuals.widgets.active.bg_stroke = Stroke::new(1.0, c.border);
195        visuals.widgets.active.fg_stroke = Stroke::new(1.0, c.secondary_foreground);
196        visuals.widgets.active.corner_radius = radius;
197
198        visuals.widgets.open.bg_fill = c.secondary_background;
199        visuals.widgets.open.weak_bg_fill = c.secondary_background;
200        visuals.widgets.open.bg_stroke = self.border_stroke();
201        visuals.widgets.open.fg_stroke = Stroke::new(1.0, c.secondary_foreground);
202        visuals.widgets.open.corner_radius = radius;
203
204        // Typography
205        for (_text_style, font_id) in style.text_styles.iter_mut() {
206            // Keep relative ratios but bump base size to our md.
207            font_id.size = font_id.size.max(self.metrics.font_size_sm);
208        }
209        style.spacing.button_padding = egui::vec2(self.metrics.button_padding_x_md, 6.0);
210        style.spacing.item_spacing = egui::vec2(8.0, 6.0);
211    }
212
213    /// Convenience: install into the active [`egui::Context`] *and* stash the
214    /// theme in `ctx.data` so any component can fetch it via [`Theme::get`].
215    pub fn install(self, ctx: &egui::Context) {
216        ctx.all_styles_mut(|s| self.apply_to_style(s));
217        ctx.data_mut(|d| d.insert_temp(egui::Id::new(THEME_KEY), self));
218    }
219
220    /// Fetch the installed theme — or fall back to `Theme::light()` if the
221    /// host never called [`Self::install`].
222    pub fn get(ctx: &egui::Context) -> Self {
223        ctx.data(|d| d.get_temp::<Theme>(egui::Id::new(THEME_KEY)))
224            .unwrap_or_else(Self::light)
225    }
226}
227
228const THEME_KEY: &str = "egui-components-theme/theme";
229
230impl Default for Theme {
231    fn default() -> Self {
232        Self::light()
233    }
234}
235
236/// Mix two colors by `t` (0.0 = a, 1.0 = b).
237pub fn mix(a: Color32, b: Color32, t: f32) -> Color32 {
238    let t = t.clamp(0.0, 1.0);
239    let lerp = |x: u8, y: u8| (x as f32 * (1.0 - t) + y as f32 * t) as u8;
240    Color32::from_rgba_unmultiplied(
241        lerp(a.r(), b.r()),
242        lerp(a.g(), b.g()),
243        lerp(a.b(), b.b()),
244        lerp(a.a(), b.a()),
245    )
246}