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::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 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 = settings2::get::<ThemeSettings>(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 = settings2::get::<ThemeSettings>(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::Setting 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 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.theme = theme;
140 }
141 }
142
143 merge(&mut this.buffer_font_size, value.buffer_font_size.map(Into::into));
144 merge(&mut this.buffer_line_height, value.buffer_line_height);
145 }
146
147 Ok(this)
148 }
149
150 fn json_schema(
151 generator: &mut SchemaGenerator,
152 params: &SettingsJsonSchemaParams,
153 cx: &AppContext,
154 ) -> schemars::schema::RootSchema {
155 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
156 let theme_names = cx
157 .global::<Arc<ThemeRegistry>>()
158 .list_names(params.staff_mode)
159 .map(|theme_name| Value::String(theme_name.to_string()))
160 .collect();
161
162 let theme_name_schema = SchemaObject {
163 instance_type: Some(InstanceType::String.into()),
164 enum_values: Some(theme_names),
165 ..Default::default()
166 };
167
168 root_schema
169 .definitions
170 .extend([("ThemeName".into(), theme_name_schema.into())]);
171
172 root_schema
173 .schema
174 .object
175 .as_mut()
176 .unwrap()
177 .properties
178 .extend([(
179 "theme".to_owned(),
180 Schema::new_ref("#/definitions/ThemeName".into()),
181 )]);
182
183 root_schema
184 }
185}
186
187fn merge<T: Copy>(target: &mut T, value: Option<T>) {
188 if let Some(value) = value {
189 *target = value;
190 }
191}