1use crate::one_themes::one_dark;
2use crate::{SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent};
3use anyhow::Result;
4use gpui::{
5 px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels, Subscription, ViewContext,
6};
7use refineable::Refineable;
8use schemars::{
9 gen::SchemaGenerator,
10 schema::{InstanceType, Schema, SchemaObject},
11 JsonSchema,
12};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use settings::{Settings, SettingsJsonSchemaParams};
16use std::sync::Arc;
17use util::ResultExt as _;
18
19const MIN_FONT_SIZE: Pixels = px(6.0);
20const MIN_LINE_HEIGHT: f32 = 1.0;
21
22#[derive(Clone)]
23pub struct ThemeSettings {
24 pub ui_font_size: Pixels,
25 pub ui_font: Font,
26 pub buffer_font: Font,
27 pub buffer_font_size: Pixels,
28 pub buffer_line_height: BufferLineHeight,
29 pub active_theme: Arc<Theme>,
30 pub theme_overrides: Option<ThemeStyleContent>,
31}
32
33#[derive(Default)]
34pub(crate) struct AdjustedBufferFontSize(Pixels);
35
36#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
37pub struct ThemeSettingsContent {
38 #[serde(default)]
39 pub ui_font_size: Option<f32>,
40 #[serde(default)]
41 pub ui_font_family: Option<String>,
42 #[serde(default)]
43 pub ui_font_features: Option<FontFeatures>,
44 #[serde(default)]
45 pub buffer_font_family: Option<String>,
46 #[serde(default)]
47 pub buffer_font_size: Option<f32>,
48 #[serde(default)]
49 pub buffer_line_height: Option<BufferLineHeight>,
50 #[serde(default)]
51 pub buffer_font_features: Option<FontFeatures>,
52 #[serde(default)]
53 pub theme: Option<String>,
54
55 /// EXPERIMENTAL: Overrides for the current theme.
56 ///
57 /// These values will override the ones on the current theme specified in `theme`.
58 #[serde(rename = "experimental.theme_overrides", default)]
59 pub theme_overrides: Option<ThemeStyleContent>,
60}
61
62#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
63#[serde(rename_all = "snake_case")]
64pub enum BufferLineHeight {
65 #[default]
66 Comfortable,
67 Standard,
68 Custom(f32),
69}
70
71impl BufferLineHeight {
72 pub fn value(&self) -> f32 {
73 match self {
74 BufferLineHeight::Comfortable => 1.618,
75 BufferLineHeight::Standard => 1.3,
76 BufferLineHeight::Custom(line_height) => *line_height,
77 }
78 }
79}
80
81impl ThemeSettings {
82 pub fn buffer_font_size(&self, cx: &AppContext) -> Pixels {
83 cx.try_global::<AdjustedBufferFontSize>()
84 .map_or(self.buffer_font_size, |size| size.0)
85 .max(MIN_FONT_SIZE)
86 }
87
88 pub fn line_height(&self) -> f32 {
89 f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
90 }
91
92 /// Applies the theme overrides, if there are any, to the current theme.
93 pub fn apply_theme_overrides(&mut self) {
94 if let Some(theme_overrides) = &self.theme_overrides {
95 let mut base_theme = (*self.active_theme).clone();
96
97 base_theme
98 .styles
99 .colors
100 .refine(&theme_overrides.theme_colors_refinement());
101 base_theme
102 .styles
103 .status
104 .refine(&theme_overrides.status_colors_refinement());
105 base_theme.styles.syntax = Arc::new(SyntaxTheme {
106 highlights: {
107 let mut highlights = base_theme.styles.syntax.highlights.clone();
108 // Overrides come second in the highlight list so that they take precedence
109 // over the ones in the base theme.
110 highlights.extend(theme_overrides.syntax_overrides());
111 highlights
112 },
113 });
114
115 self.active_theme = Arc::new(base_theme);
116 }
117 }
118}
119
120pub fn observe_buffer_font_size_adjustment<V: 'static>(
121 cx: &mut ViewContext<V>,
122 f: impl 'static + Fn(&mut V, &mut ViewContext<V>),
123) -> Subscription {
124 cx.observe_global::<AdjustedBufferFontSize>(f)
125}
126
127pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
128 if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::<AdjustedBufferFontSize>() {
129 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
130 let delta = *adjusted_size - buffer_font_size;
131 size + delta
132 } else {
133 size
134 }
135 .max(MIN_FONT_SIZE)
136}
137
138pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
139 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
140 let mut adjusted_size = cx
141 .try_global::<AdjustedBufferFontSize>()
142 .map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
143
144 f(&mut adjusted_size);
145 adjusted_size = adjusted_size.max(MIN_FONT_SIZE);
146 cx.set_global(AdjustedBufferFontSize(adjusted_size));
147 cx.refresh();
148}
149
150pub fn reset_font_size(cx: &mut AppContext) {
151 if cx.has_global::<AdjustedBufferFontSize>() {
152 cx.remove_global::<AdjustedBufferFontSize>();
153 cx.refresh();
154 }
155}
156
157impl settings::Settings for ThemeSettings {
158 const KEY: Option<&'static str> = None;
159
160 type FileContent = ThemeSettingsContent;
161
162 fn load(
163 defaults: &Self::FileContent,
164 user_values: &[&Self::FileContent],
165 cx: &mut AppContext,
166 ) -> Result<Self> {
167 let themes = cx.default_global::<ThemeRegistry>();
168
169 let mut this = Self {
170 ui_font_size: defaults.ui_font_size.unwrap().into(),
171 ui_font: Font {
172 family: defaults.ui_font_family.clone().unwrap().into(),
173 features: defaults.ui_font_features.clone().unwrap(),
174 weight: Default::default(),
175 style: Default::default(),
176 },
177 buffer_font: Font {
178 family: defaults.buffer_font_family.clone().unwrap().into(),
179 features: defaults.buffer_font_features.clone().unwrap(),
180 weight: FontWeight::default(),
181 style: FontStyle::default(),
182 },
183 buffer_font_size: defaults.buffer_font_size.unwrap().into(),
184 buffer_line_height: defaults.buffer_line_height.unwrap(),
185 active_theme: themes
186 .get(defaults.theme.as_ref().unwrap())
187 .or(themes.get(&one_dark().name))
188 .unwrap(),
189 theme_overrides: None,
190 };
191
192 for value in user_values.into_iter().copied().cloned() {
193 if let Some(value) = value.buffer_font_family {
194 this.buffer_font.family = value.into();
195 }
196 if let Some(value) = value.buffer_font_features {
197 this.buffer_font.features = value;
198 }
199
200 if let Some(value) = value.ui_font_family {
201 this.ui_font.family = value.into();
202 }
203 if let Some(value) = value.ui_font_features {
204 this.ui_font.features = value;
205 }
206
207 if let Some(value) = &value.theme {
208 if let Some(theme) = themes.get(value).log_err() {
209 this.active_theme = theme;
210 }
211 }
212
213 this.theme_overrides = value.theme_overrides;
214 this.apply_theme_overrides();
215
216 merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
217 merge(
218 &mut this.buffer_font_size,
219 value.buffer_font_size.map(Into::into),
220 );
221 merge(&mut this.buffer_line_height, value.buffer_line_height);
222 }
223
224 Ok(this)
225 }
226
227 fn json_schema(
228 generator: &mut SchemaGenerator,
229 params: &SettingsJsonSchemaParams,
230 cx: &AppContext,
231 ) -> schemars::schema::RootSchema {
232 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
233 let theme_names = cx
234 .global::<ThemeRegistry>()
235 .list_names(params.staff_mode)
236 .map(|theme_name| Value::String(theme_name.to_string()))
237 .collect();
238
239 let theme_name_schema = SchemaObject {
240 instance_type: Some(InstanceType::String.into()),
241 enum_values: Some(theme_names),
242 ..Default::default()
243 };
244
245 let available_fonts = params
246 .font_names
247 .iter()
248 .cloned()
249 .map(Value::String)
250 .collect();
251 let fonts_schema = SchemaObject {
252 instance_type: Some(InstanceType::String.into()),
253 enum_values: Some(available_fonts),
254 ..Default::default()
255 };
256 root_schema.definitions.extend([
257 ("ThemeName".into(), theme_name_schema.into()),
258 ("FontFamilies".into(), fonts_schema.into()),
259 ]);
260
261 root_schema
262 .schema
263 .object
264 .as_mut()
265 .unwrap()
266 .properties
267 .extend([
268 (
269 "theme".to_owned(),
270 Schema::new_ref("#/definitions/ThemeName".into()),
271 ),
272 (
273 "buffer_font_family".to_owned(),
274 Schema::new_ref("#/definitions/FontFamilies".into()),
275 ),
276 (
277 "ui_font_family".to_owned(),
278 Schema::new_ref("#/definitions/FontFamilies".into()),
279 ),
280 ]);
281
282 root_schema
283 }
284}
285
286fn merge<T: Copy>(target: &mut T, value: Option<T>) {
287 if let Some(value) = value {
288 *target = value;
289 }
290}