1use crate::{Theme, ThemeRegistry};
2use anyhow::Result;
3use gpui2::{FontFeatures, SharedString, Font, AppContext, Pixels, px, FontWeight, FontStyle};
4use schemars::{
5 gen::SchemaGenerator,
6 schema::{InstanceType, Schema, SchemaObject},
7 JsonSchema,
8};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use settings2::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, JsonSchema)]
19pub struct ThemeSettings {
20 pub buffer_font: Font,
21 pub buffer_font_size: Pixels,
22 pub buffer_line_height: BufferLineHeight,
23 #[serde(skip)]
24 pub theme: Arc<Theme>,
25}
26
27pub struct AdjustedBufferFontSize(pub f32);
28
29#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
30pub struct ThemeSettingsContent {
31 #[serde(default)]
32 pub buffer_font_family: Option<SharedString>,
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<SharedString>,
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: &AppContext) -> f32 {
64 if cx.has_global::<AdjustedBufferFontSize>() {
65 cx.global::<AdjustedBufferFontSize>().0
66 } else {
67 self.buffer_font_size
68 }
69 .max(MIN_FONT_SIZE)
70 }
71
72 pub fn line_height(&self) -> f32 {
73 f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
74 }
75}
76
77pub fn adjusted_font_size(size: f32, cx: &AppContext) -> f32 {
78 if let Some(adjusted_size) = cx.try_global::<AdjustedBufferFontSize>() {
79 let buffer_font_size = settings2::get::<ThemeSettings>(cx).buffer_font_size;
80 let delta = adjusted_size - buffer_font_size;
81 size + delta
82 } else {
83 size
84 }.max(MIN_FONT_SIZE)
85}
86
87pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
88 if !cx.has_global::<AdjustedBufferFontSize>() {
89 let buffer_font_size = settings2::get::<ThemeSettings>(cx).buffer_font_size;
90 cx.set_global(AdjustedBufferFontSize(buffer_font_size));
91 }
92 let mut delta = cx.global_mut::<AdjustedBufferFontSize>();
93 f(&mut delta.0);
94 delta.0 = delta
95 .0
96 .max(MIN_FONT_SIZE - settings2::get::<ThemeSettings>(cx).buffer_font_size);
97 cx.refresh_windows();
98}
99
100pub fn reset_font_size(cx: &mut AppContext) {
101 if cx.has_global::<AdjustedBufferFontSize>() {
102 cx.remove_global::<AdjustedBufferFontSize>();
103 cx.refresh_windows();
104 }
105}
106
107impl settings2::Setting for ThemeSettings {
108 const KEY: Option<&'static str> = None;
109
110 type FileContent = ThemeSettingsContent;
111
112 fn load(
113 defaults: &Self::FileContent,
114 user_values: &[&Self::FileContent],
115 cx: &AppContext,
116 ) -> Result<Self> {
117 let themes = cx.global::<Arc<ThemeRegistry>>();
118
119 let mut this = Self {
120 buffer_font: Font {
121 family: defaults.buffer_font_family.clone().unwrap(),
122 features: defaults.buffer_font_features.clone().unwrap(),
123 weight: FontWeight::default(),
124 style: FontStyle::default(),
125 },
126 buffer_font_size: defaults.buffer_font_size.unwrap(),
127 buffer_line_height: defaults.buffer_line_height.unwrap(),
128 theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
129 };
130
131 for value in user_values.into_iter().copied().cloned() {
132 let font_cache = cx.font_cache();
133 let mut family_changed = false;
134 if let Some(value) = value.buffer_font_family {
135 this.buffer_font_family_name = value;
136 family_changed = true;
137 }
138 if let Some(value) = value.buffer_font_features {
139 this.buffer_font_features = value;
140 family_changed = true;
141 }
142 if family_changed {
143 if let Some(id) = font_cache
144 .load_family(&[&this.buffer_font_family_name], &this.buffer_font_features)
145 .log_err()
146 {
147 this.buffer_font_family = id;
148 }
149 }
150
151 if let Some(value) = &value.theme {
152 if let Some(theme) = themes.get(value).log_err() {
153 this.theme = theme;
154 }
155 }
156
157 merge(&mut this.buffer_font_size, value.buffer_font_size);
158 merge(&mut this.buffer_line_height, value.buffer_line_height);
159 }
160
161 Ok(this)
162 }
163
164 fn json_schema(
165 generator: &mut SchemaGenerator,
166 params: &SettingsJsonSchemaParams,
167 cx: &AppContext,
168 ) -> schemars::schema::RootSchema {
169 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
170 let theme_names = cx
171 .global::<Arc<ThemeRegistry>>()
172 .list_names(params.staff_mode)
173 .map(|theme_name| Value::String(theme_name.to_string()))
174 .collect();
175
176 let theme_name_schema = SchemaObject {
177 instance_type: Some(InstanceType::String.into()),
178 enum_values: Some(theme_names),
179 ..Default::default()
180 };
181
182 root_schema
183 .definitions
184 .extend([("ThemeName".into(), theme_name_schema.into())]);
185
186 root_schema
187 .schema
188 .object
189 .as_mut()
190 .unwrap()
191 .properties
192 .extend([(
193 "theme".to_owned(),
194 Schema::new_ref("#/definitions/ThemeName".into()),
195 )]);
196
197 root_schema
198 }
199}
200
201fn merge<T: Copy>(target: &mut T, value: Option<T>) {
202 if let Some(value) = value {
203 *target = value;
204 }
205}