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