1use std::sync::Arc;
2
3use gpui::{App, FontFeatures, FontWeight};
4use project::project_settings::ProjectSettings;
5use settings::{EditableSettingControl, Settings, SettingsContent};
6use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
7use ui::{
8 CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
9 prelude::*,
10};
11
12use crate::EditorSettings;
13
14#[derive(IntoElement)]
15pub struct EditorSettingsControls {}
16
17impl Default for EditorSettingsControls {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23impl EditorSettingsControls {
24 pub fn new() -> Self {
25 Self {}
26 }
27}
28
29impl RenderOnce for EditorSettingsControls {
30 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
31 SettingsContainer::new()
32 .child(
33 SettingsGroup::new("Font")
34 .child(
35 h_flex()
36 .gap_2()
37 .justify_between()
38 .child(BufferFontFamilyControl)
39 .child(BufferFontWeightControl),
40 )
41 .child(BufferFontSizeControl)
42 .child(BufferFontLigaturesControl),
43 )
44 .child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
45 .child(
46 SettingsGroup::new("Gutter").child(
47 h_flex()
48 .gap_2()
49 .justify_between()
50 .child(LineNumbersControl)
51 .child(RelativeLineNumbersControl),
52 ),
53 )
54 }
55}
56
57#[derive(IntoElement)]
58struct BufferFontFamilyControl;
59
60impl EditableSettingControl for BufferFontFamilyControl {
61 type Value = SharedString;
62
63 fn name(&self) -> SharedString {
64 "Buffer Font Family".into()
65 }
66
67 fn read(cx: &App) -> Self::Value {
68 let settings = ThemeSettings::get_global(cx);
69 settings.buffer_font.family.clone()
70 }
71
72 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
73 settings.theme.buffer_font_family = Some(FontFamilyName(value.into()));
74 }
75}
76
77impl RenderOnce for BufferFontFamilyControl {
78 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
79 let value = Self::read(cx);
80
81 h_flex()
82 .gap_2()
83 .child(Icon::new(IconName::Font))
84 .child(DropdownMenu::new(
85 "buffer-font-family",
86 value,
87 ContextMenu::build(window, cx, |mut menu, _, cx| {
88 let font_family_cache = FontFamilyCache::global(cx);
89
90 for font_name in font_family_cache.list_font_families(cx) {
91 menu = menu.custom_entry(
92 {
93 let font_name = font_name.clone();
94 move |_window, _cx| Label::new(font_name.clone()).into_any_element()
95 },
96 {
97 let font_name = font_name.clone();
98 move |_window, cx| {
99 Self::write(font_name.clone(), cx);
100 }
101 },
102 )
103 }
104
105 menu
106 }),
107 ))
108 }
109}
110
111#[derive(IntoElement)]
112struct BufferFontSizeControl;
113
114impl EditableSettingControl for BufferFontSizeControl {
115 type Value = Pixels;
116
117 fn name(&self) -> SharedString {
118 "Buffer Font Size".into()
119 }
120
121 fn read(cx: &App) -> Self::Value {
122 ThemeSettings::get_global(cx).buffer_font_size(cx)
123 }
124
125 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
126 settings.theme.buffer_font_size = Some(value.into());
127 }
128}
129
130impl RenderOnce for BufferFontSizeControl {
131 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
132 let value = Self::read(cx);
133
134 h_flex()
135 .gap_2()
136 .child(Icon::new(IconName::FontSize))
137 .child(NumericStepper::new(
138 "buffer-font-size",
139 value.to_string(),
140 move |_, _, cx| {
141 Self::write(value - px(1.), cx);
142 },
143 move |_, _, cx| {
144 Self::write(value + px(1.), cx);
145 },
146 ))
147 }
148}
149
150#[derive(IntoElement)]
151struct BufferFontWeightControl;
152
153impl EditableSettingControl for BufferFontWeightControl {
154 type Value = FontWeight;
155
156 fn name(&self) -> SharedString {
157 "Buffer Font Weight".into()
158 }
159
160 fn read(cx: &App) -> Self::Value {
161 let settings = ThemeSettings::get_global(cx);
162 settings.buffer_font.weight
163 }
164
165 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
166 settings.theme.buffer_font_weight = Some(value.0);
167 }
168}
169
170impl RenderOnce for BufferFontWeightControl {
171 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
172 let value = Self::read(cx);
173
174 h_flex()
175 .gap_2()
176 .child(Icon::new(IconName::FontWeight))
177 .child(DropdownMenu::new(
178 "buffer-font-weight",
179 value.0.to_string(),
180 ContextMenu::build(window, cx, |mut menu, _window, _cx| {
181 for weight in FontWeight::ALL {
182 menu = menu.custom_entry(
183 move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(),
184 {
185 move |_, cx| {
186 Self::write(weight, cx);
187 }
188 },
189 )
190 }
191
192 menu
193 }),
194 ))
195 }
196}
197
198#[derive(IntoElement)]
199struct BufferFontLigaturesControl;
200
201impl EditableSettingControl for BufferFontLigaturesControl {
202 type Value = bool;
203
204 fn name(&self) -> SharedString {
205 "Buffer Font Ligatures".into()
206 }
207
208 fn read(cx: &App) -> Self::Value {
209 let settings = ThemeSettings::get_global(cx);
210 settings
211 .buffer_font
212 .features
213 .is_calt_enabled()
214 .unwrap_or(true)
215 }
216
217 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
218 let value = if value { 1 } else { 0 };
219
220 let mut features = settings
221 .theme
222 .buffer_font_features
223 .as_ref()
224 .map(|features| features.tag_value_list().to_vec())
225 .unwrap_or_default();
226
227 if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
228 features[calt_index].1 = value;
229 } else {
230 features.push(("calt".into(), value));
231 }
232
233 settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
234 }
235}
236
237impl RenderOnce for BufferFontLigaturesControl {
238 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
239 let value = Self::read(cx);
240
241 CheckboxWithLabel::new(
242 "buffer-font-ligatures",
243 Label::new(self.name()),
244 value.into(),
245 |selection, _, cx| {
246 Self::write(
247 match selection {
248 ToggleState::Selected => true,
249 ToggleState::Unselected | ToggleState::Indeterminate => false,
250 },
251 cx,
252 );
253 },
254 )
255 }
256}
257
258#[derive(IntoElement)]
259struct InlineGitBlameControl;
260
261impl EditableSettingControl for InlineGitBlameControl {
262 type Value = bool;
263
264 fn name(&self) -> SharedString {
265 "Inline Git Blame".into()
266 }
267
268 fn read(cx: &App) -> Self::Value {
269 let settings = ProjectSettings::get_global(cx);
270 settings.git.inline_blame.enabled
271 }
272
273 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
274 settings
275 .git
276 .get_or_insert_default()
277 .inline_blame
278 .get_or_insert_default()
279 .enabled = Some(value)
280 }
281}
282
283impl RenderOnce for InlineGitBlameControl {
284 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
285 let value = Self::read(cx);
286
287 CheckboxWithLabel::new(
288 "inline-git-blame",
289 Label::new(self.name()),
290 value.into(),
291 |selection, _, cx| {
292 Self::write(
293 match selection {
294 ToggleState::Selected => true,
295 ToggleState::Unselected | ToggleState::Indeterminate => false,
296 },
297 cx,
298 );
299 },
300 )
301 }
302}
303
304#[derive(IntoElement)]
305struct LineNumbersControl;
306
307impl EditableSettingControl for LineNumbersControl {
308 type Value = bool;
309
310 fn name(&self) -> SharedString {
311 "Line Numbers".into()
312 }
313
314 fn read(cx: &App) -> Self::Value {
315 let settings = EditorSettings::get_global(cx);
316 settings.gutter.line_numbers
317 }
318
319 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
320 settings.editor.gutter.get_or_insert_default().line_numbers = Some(value);
321 }
322}
323
324impl RenderOnce for LineNumbersControl {
325 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
326 let value = Self::read(cx);
327
328 CheckboxWithLabel::new(
329 "line-numbers",
330 Label::new(self.name()),
331 value.into(),
332 |selection, _, cx| {
333 Self::write(
334 match selection {
335 ToggleState::Selected => true,
336 ToggleState::Unselected | ToggleState::Indeterminate => false,
337 },
338 cx,
339 );
340 },
341 )
342 }
343}
344
345#[derive(IntoElement)]
346struct RelativeLineNumbersControl;
347
348impl EditableSettingControl for RelativeLineNumbersControl {
349 type Value = bool;
350
351 fn name(&self) -> SharedString {
352 "Relative Line Numbers".into()
353 }
354
355 fn read(cx: &App) -> Self::Value {
356 let settings = EditorSettings::get_global(cx);
357 settings.relative_line_numbers
358 }
359
360 fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
361 settings.editor.relative_line_numbers = Some(value);
362 }
363}
364
365impl RenderOnce for RelativeLineNumbersControl {
366 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
367 let value = Self::read(cx);
368
369 DropdownMenu::new(
370 "relative-line-numbers",
371 if value { "Relative" } else { "Ascending" },
372 ContextMenu::build(window, cx, |menu, _window, _cx| {
373 menu.custom_entry(
374 |_window, _cx| Label::new("Ascending").into_any_element(),
375 move |_, cx| Self::write(false, cx),
376 )
377 .custom_entry(
378 |_window, _cx| Label::new("Relative").into_any_element(),
379 move |_, cx| Self::write(true, cx),
380 )
381 }),
382 )
383 }
384}