Skip to main content

pite_editor/
theme.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2use eframe::egui;
3
4const ACCENT: egui::Color32 = egui::Color32::from_rgb(11, 147, 135);
5pub(crate) const BG: egui::Color32 = egui::Color32::from_rgb(11, 14, 17);
6const PANEL: egui::Color32 = egui::Color32::from_rgb(20, 24, 29);
7const PANEL_STROKE: egui::Color32 = egui::Color32::from_rgb(42, 49, 56);
8pub(crate) const TEXT: egui::Color32 = egui::Color32::from_rgb(230, 233, 236);
9pub(crate) const FAINT: egui::Color32 = egui::Color32::from_rgb(154, 163, 173);
10const INACTIVE: egui::Color32 = egui::Color32::from_rgb(23, 28, 34);
11const HOVERED: egui::Color32 = egui::Color32::from_rgb(31, 38, 46);
12const ACTIVE: egui::Color32 = egui::Color32::from_rgb(30, 58, 53);
13
14pub fn apply(ctx: &egui::Context) {
15    ctx.set_fonts(fonts());
16    ctx.set_visuals(visuals());
17    ctx.global_style_mut(|style| {
18        style.spacing.item_spacing = egui::vec2(8.0, 6.0);
19        style.spacing.button_padding = egui::vec2(10.0, 5.0);
20        style.spacing.indent = 20.0;
21        style.spacing.scroll.bar_width = 10.0;
22        style.text_styles.insert(
23            egui::TextStyle::Body,
24            egui::FontId::new(15.0, egui::FontFamily::Proportional),
25        );
26        style.text_styles.insert(
27            egui::TextStyle::Button,
28            egui::FontId::new(15.0, egui::FontFamily::Proportional),
29        );
30        style.text_styles.insert(
31            egui::TextStyle::Heading,
32            egui::FontId::new(20.0, egui::FontFamily::Proportional),
33        );
34        style.text_styles.insert(
35            egui::TextStyle::Monospace,
36            egui::FontId::new(14.0, egui::FontFamily::Monospace),
37        );
38        style.text_styles.insert(
39            egui::TextStyle::Small,
40            egui::FontId::new(12.0, egui::FontFamily::Proportional),
41        );
42    });
43}
44
45fn fonts() -> egui::FontDefinitions {
46    let mut fonts = egui::FontDefinitions::default();
47    fonts.font_data.insert(
48        "Inter".to_string(),
49        egui::FontData::from_static(pite_render::text::FONT_BYTES).into(),
50    );
51    fonts.font_data.insert(
52        "JetBrainsMono".to_string(),
53        egui::FontData::from_static(include_bytes!("../fonts/JetBrainsMono-Regular.ttf")).into(),
54    );
55    fonts
56        .families
57        .insert(egui::FontFamily::Proportional, vec!["Inter".to_string()]);
58    fonts.families.insert(
59        egui::FontFamily::Monospace,
60        vec!["JetBrainsMono".to_string()],
61    );
62    fonts
63}
64
65fn widget(fill: egui::Color32) -> egui::style::WidgetVisuals {
66    egui::style::WidgetVisuals {
67        bg_fill: fill,
68        weak_bg_fill: fill,
69        bg_stroke: egui::Stroke::new(1.0_f32, PANEL_STROKE),
70        fg_stroke: egui::Stroke::new(1.0_f32, TEXT),
71        corner_radius: 6.0.into(),
72        expansion: 0.0,
73    }
74}
75
76fn visuals() -> egui::Visuals {
77    let mut visuals = egui::Visuals::dark();
78    visuals.dark_mode = true;
79    visuals.override_text_color = Some(TEXT);
80    visuals.weak_text_color = Some(FAINT);
81    visuals.faint_bg_color = PANEL;
82    visuals.extreme_bg_color = BG;
83    visuals.code_bg_color = BG;
84    visuals.window_fill = PANEL;
85    visuals.panel_fill = PANEL;
86    visuals.window_stroke = egui::Stroke::new(1.0_f32, PANEL_STROKE);
87    visuals.hyperlink_color = ACCENT;
88    // Selection = palette `dark` fill with near-white glyphs (simulated 7.9;
89    // the allow-listed `dark -> white` pair). Solid `dark` beats the old
90    // 20%-tint wash for region visibility while staying quieter than solid
91    // primary; egui paints selected glyphs with `selection.stroke.color`.
92    visuals.selection.bg_fill = egui::Color32::from_rgb(18, 77, 73);
93    visuals.selection.stroke = egui::Stroke::new(1.0_f32, TEXT);
94    visuals.widgets.noninteractive = widget(PANEL);
95    visuals.widgets.inactive = widget(INACTIVE);
96    visuals.widgets.hovered = widget(HOVERED);
97    visuals.widgets.active = widget(ACTIVE);
98    visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0_f32, ACCENT);
99    visuals.widgets.open = widget(INACTIVE);
100    visuals.window_corner_radius = 8.0.into();
101    visuals.menu_corner_radius = 6.0.into();
102    visuals
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn theme_applies_cleanly() {
111        let ctx = egui::Context::default();
112        apply(&ctx);
113        let style = ctx.global_style();
114        assert!(style.visuals.dark_mode);
115        assert_eq!(style.visuals.window_fill, PANEL);
116        assert_eq!(style.text_styles[&egui::TextStyle::Body].size, 15.0);
117        assert_eq!(style.visuals.selection.stroke.color, TEXT);
118    }
119}