Skip to main content

gpui_base/
theme.rs

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