gpui_component/theme/
mod.rs

1use crate::{highlighter::HighlightTheme, scroll::ScrollbarShow};
2use gpui::{px, App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::{
6    ops::{Deref, DerefMut},
7    rc::Rc,
8    sync::Arc,
9};
10
11mod color;
12mod registry;
13mod schema;
14mod theme_color;
15
16pub use color::*;
17pub use registry::*;
18pub use schema::*;
19pub use theme_color::*;
20
21pub fn init(cx: &mut App) {
22    registry::init(cx);
23
24    Theme::sync_system_appearance(None, cx);
25    Theme::sync_scrollbar_appearance(cx);
26}
27
28pub trait ActiveTheme {
29    fn theme(&self) -> &Theme;
30}
31
32impl ActiveTheme for App {
33    #[inline(always)]
34    fn theme(&self) -> &Theme {
35        Theme::global(self)
36    }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
40pub struct Theme {
41    pub colors: ThemeColor,
42    pub highlight_theme: Arc<HighlightTheme>,
43    pub light_theme: Rc<ThemeConfig>,
44    pub dark_theme: Rc<ThemeConfig>,
45
46    pub mode: ThemeMode,
47    pub font_family: SharedString,
48    pub font_size: Pixels,
49    /// Radius for the general elements.
50    pub radius: Pixels,
51    /// Radius for the large elements, e.g.: Modal, Notification border radius.
52    pub radius_lg: Pixels,
53    pub shadow: bool,
54    pub transparent: Hsla,
55    /// Show the scrollbar mode, default: Scrolling
56    pub scrollbar_show: ScrollbarShow,
57    /// Tile grid size, default is 4px.
58    pub tile_grid_size: Pixels,
59    /// The shadow of the tile panel.
60    pub tile_shadow: bool,
61    /// The border radius of the tile panel, default is 0px.
62    pub tile_radius: Pixels,
63}
64
65impl Default for Theme {
66    fn default() -> Self {
67        Self::from(&ThemeColor::default())
68    }
69}
70
71impl Deref for Theme {
72    type Target = ThemeColor;
73
74    fn deref(&self) -> &Self::Target {
75        &self.colors
76    }
77}
78
79impl DerefMut for Theme {
80    fn deref_mut(&mut self) -> &mut Self::Target {
81        &mut self.colors
82    }
83}
84
85impl Global for Theme {}
86
87impl Theme {
88    /// Returns the global theme reference
89    #[inline(always)]
90    pub fn global(cx: &App) -> &Theme {
91        cx.global::<Theme>()
92    }
93
94    /// Returns the global theme mutable reference
95    #[inline(always)]
96    pub fn global_mut(cx: &mut App) -> &mut Theme {
97        cx.global_mut::<Theme>()
98    }
99
100    /// Returns true if the theme is dark.
101    #[inline(always)]
102    pub fn is_dark(&self) -> bool {
103        self.mode.is_dark()
104    }
105
106    /// Returns the current theme name.
107    pub fn theme_name(&self) -> &SharedString {
108        if self.is_dark() {
109            &self.dark_theme.name
110        } else {
111            &self.light_theme.name
112        }
113    }
114
115    // /// Sets the theme to default light.
116    // pub fn set_default_light(&mut self) {
117    //     self.light_theme = ThemeColor::light();
118    //     self.colors = ThemeColor::light();
119    //     self.light_highlight_theme = Arc::new(HighlightTheme::default_light());
120    //     self.highlight_theme = self.light_highlight_theme.clone();
121    // }
122
123    // /// Sets the theme to default dark.
124    // pub fn set_default_dark(&mut self) {
125    //     self.dark_theme = ThemeColor::dark();
126    //     self.colors = ThemeColor::dark();
127    //     self.dark_highlight_theme = Arc::new(HighlightTheme::default_dark());
128    //     self.highlight_theme = self.dark_highlight_theme.clone();
129    // }
130
131    /// Sync the theme with the system appearance
132    pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
133        // Better use window.appearance() for avoid error on Linux.
134        // https://github.com/longbridge/gpui-component/issues/104
135        let appearance = window
136            .as_ref()
137            .map(|window| window.appearance())
138            .unwrap_or_else(|| cx.window_appearance());
139
140        Self::change(appearance, window, cx);
141    }
142
143    /// Sync the Scrollbar showing behavior with the system
144    pub fn sync_scrollbar_appearance(cx: &mut App) {
145        Theme::global_mut(cx).scrollbar_show = if cx.should_auto_hide_scrollbars() {
146            ScrollbarShow::Scrolling
147        } else {
148            ScrollbarShow::Hover
149        };
150    }
151
152    pub fn change(mode: impl Into<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
153        let mode = mode.into();
154        if !cx.has_global::<Theme>() {
155            let mut theme = Theme::default();
156            theme.light_theme = ThemeRegistry::global(cx).default_light_theme().clone();
157            theme.dark_theme = ThemeRegistry::global(cx).default_dark_theme().clone();
158            cx.set_global(theme);
159        }
160
161        let theme = cx.global_mut::<Theme>();
162        theme.mode = mode;
163        if mode.is_dark() {
164            theme.apply_config(&theme.dark_theme.clone());
165        } else {
166            theme.apply_config(&theme.light_theme.clone());
167        }
168
169        if let Some(window) = window {
170            window.refresh();
171        }
172    }
173
174    /// Get the editor background color, if not set, use the theme background color.
175    #[inline]
176    pub(crate) fn editor_background(&self) -> Hsla {
177        self.highlight_theme
178            .style
179            .editor_background
180            .unwrap_or(self.background)
181    }
182}
183
184impl From<&ThemeColor> for Theme {
185    fn from(colors: &ThemeColor) -> Self {
186        Theme {
187            mode: ThemeMode::default(),
188            transparent: Hsla::transparent_black(),
189            font_size: px(16.),
190            font_family: if cfg!(target_os = "macos") {
191                ".SystemUIFont".into()
192            } else if cfg!(target_os = "windows") {
193                "Segoe UI".into()
194            } else {
195                "FreeMono".into()
196            },
197            radius: px(6.),
198            radius_lg: px(8.),
199            shadow: true,
200            scrollbar_show: ScrollbarShow::default(),
201            tile_grid_size: px(8.),
202            tile_shadow: true,
203            tile_radius: px(0.),
204            colors: *colors,
205            light_theme: Rc::new(ThemeConfig::default()),
206            dark_theme: Rc::new(ThemeConfig::default()),
207            highlight_theme: HighlightTheme::default_light(),
208        }
209    }
210}
211
212#[derive(
213    Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Hash, Serialize, Deserialize, JsonSchema,
214)]
215#[serde(rename_all = "snake_case")]
216pub enum ThemeMode {
217    #[default]
218    Light,
219    Dark,
220}
221
222impl ThemeMode {
223    #[inline(always)]
224    pub fn is_dark(&self) -> bool {
225        matches!(self, Self::Dark)
226    }
227
228    /// Return lower_case theme name: `light`, `dark`.
229    pub fn name(&self) -> &'static str {
230        match self {
231            ThemeMode::Light => "light",
232            ThemeMode::Dark => "dark",
233        }
234    }
235}
236
237impl From<WindowAppearance> for ThemeMode {
238    fn from(appearance: WindowAppearance) -> Self {
239        match appearance {
240            WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
241            WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
242        }
243    }
244}