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