Skip to main content

rgpui_component/theme/
mod.rs

1use crate::{
2    list::ListSettings, notification::NotificationSettings, scroll::ScrollbarShow,
3    sheet::SheetSettings,
4};
5use rgpui::{App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance, px};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::{
9    ops::{Deref, DerefMut},
10    rc::Rc,
11    sync::Arc,
12};
13
14mod color;
15pub mod highlight;
16mod registry;
17mod schema;
18mod theme_color;
19
20pub use color::*;
21pub use highlight::*;
22pub use registry::*;
23pub use schema::*;
24pub use theme_color::*;
25
26pub fn init(cx: &mut App) {
27    registry::init(cx);
28
29    // Ensure theme is loaded directly on startup for WASM compatibility
30    Theme::change(ThemeMode::Light, None, cx);
31    Theme::sync_scrollbar_appearance(cx);
32}
33
34pub trait ActiveTheme {
35    fn theme(&self) -> &Theme;
36}
37
38impl ActiveTheme for App {
39    #[inline(always)]
40    fn theme(&self) -> &Theme {
41        Theme::global(self)
42    }
43}
44
45/// The global theme configuration.
46#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
47pub struct Theme {
48    pub colors: ThemeColor,
49    pub highlight_theme: Arc<HighlightTheme>,
50    pub light_theme: Rc<ThemeConfig>,
51    pub dark_theme: Rc<ThemeConfig>,
52
53    pub mode: ThemeMode,
54    /// The font family for the application, default is `.SystemUIFont`.
55    pub font_family: SharedString,
56    /// The base font size for the application, default is 16px.
57    pub font_size: Pixels,
58    /// The monospace font family for the application.
59    ///
60    /// Defaults to:
61    ///
62    /// - macOS: `Menlo`
63    /// - Windows: `Consolas`
64    /// - Linux: `DejaVu Sans Mono`
65    pub mono_font_family: SharedString,
66    /// The monospace font size for the application, default is 13px.
67    pub mono_font_size: Pixels,
68    /// Radius for the general elements.
69    pub radius: Pixels,
70    /// Radius for the large elements, e.g.: Dialog, Notification border radius.
71    pub radius_lg: Pixels,
72    pub shadow: bool,
73    pub transparent: Hsla,
74    /// Show the scrollbar mode, default: Scrolling
75    pub scrollbar_show: ScrollbarShow,
76    /// The notification setting.
77    #[serde(skip)]
78    pub notification: NotificationSettings,
79    /// Tile grid size, default is 4px.
80    pub tile_grid_size: Pixels,
81    /// The shadow of the tile panel.
82    pub tile_shadow: bool,
83    /// The border radius of the tile panel, default is 0px.
84    pub tile_radius: Pixels,
85    /// The list settings.
86    pub list: ListSettings,
87    /// The sheet settings.
88    pub sheet: SheetSettings,
89}
90
91impl Default for Theme {
92    fn default() -> Self {
93        Self::from(&ThemeColor::default())
94    }
95}
96
97impl Deref for Theme {
98    type Target = ThemeColor;
99
100    fn deref(&self) -> &Self::Target {
101        &self.colors
102    }
103}
104
105impl DerefMut for Theme {
106    fn deref_mut(&mut self) -> &mut Self::Target {
107        &mut self.colors
108    }
109}
110
111impl Global for Theme {}
112
113impl Theme {
114    /// Returns the global theme reference
115    #[inline(always)]
116    pub fn global(cx: &App) -> &Theme {
117        cx.global::<Theme>()
118    }
119
120    /// Returns the global theme mutable reference
121    #[inline(always)]
122    pub fn global_mut(cx: &mut App) -> &mut Theme {
123        cx.global_mut::<Theme>()
124    }
125
126    /// Returns true if the theme is dark.
127    #[inline(always)]
128    pub fn is_dark(&self) -> bool {
129        self.mode.is_dark()
130    }
131
132    /// Returns the current theme name.
133    pub fn theme_name(&self) -> &SharedString {
134        if self.is_dark() {
135            &self.dark_theme.name
136        } else {
137            &self.light_theme.name
138        }
139    }
140
141    /// Sync the theme with the system appearance
142    pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
143        // Better use window.appearance() for avoid error on Linux.
144        // https://github.com/longbridge/gpui-component/issues/104
145        let appearance = window
146            .as_ref()
147            .map(|window| window.appearance())
148            .unwrap_or_else(|| cx.window_appearance());
149
150        Self::change(appearance, window, cx);
151    }
152
153    /// Sync the Scrollbar showing behavior with the system
154    pub fn sync_scrollbar_appearance(cx: &mut App) {
155        Theme::global_mut(cx).scrollbar_show = if cx.should_auto_hide_scrollbars() {
156            ScrollbarShow::Scrolling
157        } else {
158            ScrollbarShow::Hover
159        };
160    }
161
162    /// Change the theme mode.
163    pub fn change(mode: impl Into<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
164        let mode = mode.into();
165        if !cx.has_global::<Theme>() {
166            let mut theme = Theme::default();
167            theme.light_theme = ThemeRegistry::global(cx).default_light_theme().clone();
168            theme.dark_theme = ThemeRegistry::global(cx).default_dark_theme().clone();
169            cx.set_global(theme);
170        }
171
172        let theme = cx.global_mut::<Theme>();
173        theme.mode = mode;
174        if mode.is_dark() {
175            theme.apply_config(&theme.dark_theme.clone());
176        } else {
177            theme.apply_config(&theme.light_theme.clone());
178        }
179
180        if let Some(window) = window {
181            window.refresh();
182        }
183    }
184
185    /// Get the input background color.
186    ///
187    /// For dark, use a transparent color mixed with the input border: `cx.theme().input`,
188    /// otherwise use the `cx.theme().background` color.
189    #[inline]
190    pub fn input_background(&self) -> Hsla {
191        if self.is_dark() {
192            self.input.mix_oklab(self.transparent, 0.3)
193        } else {
194            self.background
195        }
196    }
197
198    /// 获取编辑器背景色,未设置时使用输入框背景色。
199    #[inline]
200    pub fn editor_background(&self) -> Hsla {
201        self.highlight_theme
202            .style
203            .editor_background
204            .unwrap_or_else(|| self.input_background())
205    }
206}
207
208impl From<&ThemeColor> for Theme {
209    fn from(colors: &ThemeColor) -> Self {
210        Theme {
211            mode: ThemeMode::default(),
212            transparent: Hsla::transparent_black(),
213            font_family: ".SystemUIFont".into(),
214            font_size: px(16.),
215            mono_font_family: if cfg!(target_os = "macos") {
216                // https://en.wikipedia.org/wiki/Menlo_(typeface)
217                "Menlo".into()
218            } else if cfg!(target_os = "windows") {
219                "Consolas".into()
220            } else {
221                "DejaVu Sans Mono".into()
222            },
223            mono_font_size: px(13.),
224            radius: px(6.),
225            radius_lg: px(8.),
226            shadow: true,
227            scrollbar_show: ScrollbarShow::default(),
228            notification: NotificationSettings::default(),
229            tile_grid_size: px(8.),
230            tile_shadow: true,
231            tile_radius: px(0.),
232            list: ListSettings::default(),
233            colors: *colors,
234            light_theme: Rc::new(ThemeConfig::default()),
235            dark_theme: Rc::new(ThemeConfig::default()),
236            highlight_theme: HighlightTheme::default_light(),
237            sheet: SheetSettings::default(),
238        }
239    }
240}
241
242#[derive(
243    Debug,
244    Clone,
245    Copy,
246    Default,
247    PartialEq,
248    PartialOrd,
249    Eq,
250    Ord,
251    Hash,
252    Serialize,
253    Deserialize,
254    JsonSchema,
255)]
256#[serde(rename_all = "snake_case")]
257pub enum ThemeMode {
258    #[default]
259    Light,
260    Dark,
261}
262
263impl ThemeMode {
264    #[inline(always)]
265    pub fn is_dark(&self) -> bool {
266        matches!(self, Self::Dark)
267    }
268
269    /// Return lower_case theme name: `light`, `dark`.
270    pub fn name(&self) -> &'static str {
271        match self {
272            ThemeMode::Light => "light",
273            ThemeMode::Dark => "dark",
274        }
275    }
276}
277
278impl From<WindowAppearance> for ThemeMode {
279    fn from(appearance: WindowAppearance) -> Self {
280        match appearance {
281            WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
282            WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
283        }
284    }
285}