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