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