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