Skip to main content

gpui_base/
theme.rs

1use gpui::{App, Global};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5use crate::{ScrollbarMode, ScrollbarMotion, ScrollbarStyles, SemanticThemeTokens};
6
7/// Application-wide defaults for Base behavior modules.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "lowercase")]
10pub enum ThemeAppearance {
11    #[default]
12    Light,
13    Dark,
14}
15
16#[derive(Clone, Default)]
17pub struct Theme {
18    pub appearance: ThemeAppearance,
19    pub tokens: SemanticThemeTokens,
20    pub scrollbar: ScrollbarTheme,
21    pub resizable: ResizableTheme,
22}
23
24impl Global for Theme {}
25
26impl Theme {
27    pub fn global(cx: &App) -> Self {
28        cx.try_global::<Self>().cloned().unwrap_or_default()
29    }
30
31    pub fn global_mut(cx: &mut App) -> &mut Self {
32        if !cx.has_global::<Self>() {
33            cx.set_global(Self::default());
34        }
35        cx.global_mut::<Self>()
36    }
37}
38
39/// Access to the active base theme through an application context.
40pub(crate) trait ActiveTheme {
41    fn theme(&self) -> Theme;
42}
43
44impl ActiveTheme for App {
45    #[inline(always)]
46    fn theme(&self) -> Theme {
47        Theme::global(self)
48    }
49}
50
51/// Global defaults used by [`crate::Scrollbar`].
52///
53/// `motion` defaults to motionless. Styled layers project their own timing;
54/// Base never installs a fade or slide of its own.
55#[derive(Clone, Default)]
56pub struct ScrollbarTheme {
57    mode: ScrollbarMode,
58    motion: ScrollbarMotion,
59    styles: ScrollbarStyles,
60}
61
62impl ScrollbarTheme {
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    pub fn with_mode(mut self, mode: ScrollbarMode) -> Self {
68        self.mode = mode;
69        self
70    }
71
72    pub fn with_motion(mut self, motion: ScrollbarMotion) -> Self {
73        self.motion = motion;
74        self
75    }
76
77    pub fn with_styles(mut self, styles: ScrollbarStyles) -> Self {
78        self.styles = styles;
79        self
80    }
81
82    pub fn mode(&self) -> ScrollbarMode {
83        self.mode
84    }
85
86    pub fn motion(&self) -> ScrollbarMotion {
87        self.motion
88    }
89
90    pub fn styles(&self) -> &ScrollbarStyles {
91        &self.styles
92    }
93}
94
95/// Global visual defaults used by resizable panel handles.
96///
97/// `None` means *unset*, not invisible: a handle with nothing projected onto it
98/// resolves from the active [`SemanticThemeTokens`] -- `border` at rest, `ring`
99/// while dragging -- which are the tokens those two states already mean
100/// everywhere else.
101///
102/// These were plain colors, so the Base default was `Hsla::default()`: fully
103/// transparent. That reads as a deliberate choice next to a styled façade,
104/// which projects its own values and never sees it, and as a missing divider
105/// to anything that does not -- and a consumer with no façade has no way to
106/// project anything. Making them optional keeps the projection exactly as it
107/// was while giving the unprojected case an answer.
108#[derive(Clone, Copy, Default)]
109pub struct ResizableTheme {
110    pub handle: Option<gpui::Hsla>,
111    pub active_handle: Option<gpui::Hsla>,
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn scrollbar_theme_supports_builder_construction() {
120        let mode = ScrollbarMode::Hover;
121        let motion = ScrollbarMotion::default().with_enter(std::time::Duration::from_millis(120));
122        let styles = ScrollbarStyles::default();
123        let theme = ScrollbarTheme::new()
124            .with_mode(mode)
125            .with_motion(motion)
126            .with_styles(styles);
127
128        assert_eq!(theme.mode(), mode);
129        assert_eq!(theme.motion(), motion);
130        let _ = theme.styles();
131    }
132}