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