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