1use crate::{Theme, ThemeRegistry};
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<Theme>,
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(defaults.theme.as_ref().unwrap()).unwrap(),
127 };
128
129 for value in user_values.into_iter().copied().cloned() {
130 if let Some(value) = value.buffer_font_family {
131 this.buffer_font.family = value.into();
132 }
133 if let Some(value) = value.buffer_font_features {
134 this.buffer_font.features = value;
135 }
136
137 if let Some(value) = &value.theme {
138 if let Some(theme) = themes.get(value).log_err() {
139 this.active_theme = theme;
140 }
141 }
142
143 merge(
144 &mut this.buffer_font_size,
145 value.buffer_font_size.map(Into::into),
146 );
147 merge(&mut this.buffer_line_height, value.buffer_line_height);
148 }
149
150 Ok(this)
151 }
152
153 fn json_schema(
154 generator: &mut SchemaGenerator,
155 params: &SettingsJsonSchemaParams,
156 cx: &AppContext,
157 ) -> schemars::schema::RootSchema {
158 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
159 let theme_names = cx
160 .global::<Arc<ThemeRegistry>>()
161 .list_names(params.staff_mode)
162 .map(|theme_name| Value::String(theme_name.to_string()))
163 .collect();
164
165 let theme_name_schema = SchemaObject {
166 instance_type: Some(InstanceType::String.into()),
167 enum_values: Some(theme_names),
168 ..Default::default()
169 };
170
171 root_schema
172 .definitions
173 .extend([("ThemeName".into(), theme_name_schema.into())]);
174
175 root_schema
176 .schema
177 .object
178 .as_mut()
179 .unwrap()
180 .properties
181 .extend([(
182 "theme".to_owned(),
183 Schema::new_ref("#/definitions/ThemeName".into()),
184 )]);
185
186 root_schema
187 }
188}
189
190fn merge<T: Copy>(target: &mut T, value: Option<T>) {
191 if let Some(value) = value {
192 *target = value;
193 }
194}