1use std::sync::Arc;
2
3use gpui::{App, FontFeatures, FontWeight};
4use project::project_settings::{InlineBlameSettings, ProjectSettings};
5use settings::{EditableSettingControl, Settings};
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 type Settings = ThemeSettings;
63
64 fn name(&self) -> SharedString {
65 "Buffer Font Family".into()
66 }
67
68 fn read(cx: &App) -> Self::Value {
69 let settings = ThemeSettings::get_global(cx);
70 settings.buffer_font.family.clone()
71 }
72
73 fn apply(
74 settings: &mut <Self::Settings as Settings>::FileContent,
75 value: Self::Value,
76 _cx: &App,
77 ) {
78 settings.buffer_font_family = Some(FontFamilyName(value.into()));
79 }
80}
81
82impl RenderOnce for BufferFontFamilyControl {
83 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
84 let value = Self::read(cx);
85
86 h_flex()
87 .gap_2()
88 .child(Icon::new(IconName::Font))
89 .child(DropdownMenu::new(
90 "buffer-font-family",
91 value,
92 ContextMenu::build(window, cx, |mut menu, _, cx| {
93 let font_family_cache = FontFamilyCache::global(cx);
94
95 for font_name in font_family_cache.list_font_families(cx) {
96 menu = menu.custom_entry(
97 {
98 let font_name = font_name.clone();
99 move |_window, _cx| Label::new(font_name.clone()).into_any_element()
100 },
101 {
102 let font_name = font_name.clone();
103 move |_window, cx| {
104 Self::write(font_name.clone(), cx);
105 }
106 },
107 )
108 }
109
110 menu
111 }),
112 ))
113 }
114}
115
116#[derive(IntoElement)]
117struct BufferFontSizeControl;
118
119impl EditableSettingControl for BufferFontSizeControl {
120 type Value = Pixels;
121 type Settings = ThemeSettings;
122
123 fn name(&self) -> SharedString {
124 "Buffer Font Size".into()
125 }
126
127 fn read(cx: &App) -> Self::Value {
128 ThemeSettings::get_global(cx).buffer_font_size(cx)
129 }
130
131 fn apply(
132 settings: &mut <Self::Settings as Settings>::FileContent,
133 value: Self::Value,
134 _cx: &App,
135 ) {
136 settings.buffer_font_size = Some(value.into());
137 }
138}
139
140impl RenderOnce for BufferFontSizeControl {
141 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
142 let value = Self::read(cx);
143
144 h_flex()
145 .gap_2()
146 .child(Icon::new(IconName::FontSize))
147 .child(NumericStepper::new(
148 "buffer-font-size",
149 value.to_string(),
150 move |_, _, cx| {
151 Self::write(value - px(1.), cx);
152 },
153 move |_, _, cx| {
154 Self::write(value + px(1.), cx);
155 },
156 ))
157 }
158}
159
160#[derive(IntoElement)]
161struct BufferFontWeightControl;
162
163impl EditableSettingControl for BufferFontWeightControl {
164 type Value = FontWeight;
165 type Settings = ThemeSettings;
166
167 fn name(&self) -> SharedString {
168 "Buffer Font Weight".into()
169 }
170
171 fn read(cx: &App) -> Self::Value {
172 let settings = ThemeSettings::get_global(cx);
173 settings.buffer_font.weight
174 }
175
176 fn apply(
177 settings: &mut <Self::Settings as Settings>::FileContent,
178 value: Self::Value,
179 _cx: &App,
180 ) {
181 settings.buffer_font_weight = Some(value.0);
182 }
183}
184
185impl RenderOnce for BufferFontWeightControl {
186 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
187 let value = Self::read(cx);
188
189 h_flex()
190 .gap_2()
191 .child(Icon::new(IconName::FontWeight))
192 .child(DropdownMenu::new(
193 "buffer-font-weight",
194 value.0.to_string(),
195 ContextMenu::build(window, cx, |mut menu, _window, _cx| {
196 for weight in FontWeight::ALL {
197 menu = menu.custom_entry(
198 move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(),
199 {
200 move |_, cx| {
201 Self::write(weight, cx);
202 }
203 },
204 )
205 }
206
207 menu
208 }),
209 ))
210 }
211}
212
213#[derive(IntoElement)]
214struct BufferFontLigaturesControl;
215
216impl EditableSettingControl for BufferFontLigaturesControl {
217 type Value = bool;
218 type Settings = ThemeSettings;
219
220 fn name(&self) -> SharedString {
221 "Buffer Font Ligatures".into()
222 }
223
224 fn read(cx: &App) -> Self::Value {
225 let settings = ThemeSettings::get_global(cx);
226 settings
227 .buffer_font
228 .features
229 .is_calt_enabled()
230 .unwrap_or(true)
231 }
232
233 fn apply(
234 settings: &mut <Self::Settings as Settings>::FileContent,
235 value: Self::Value,
236 _cx: &App,
237 ) {
238 let value = if value { 1 } else { 0 };
239
240 let mut features = settings
241 .buffer_font_features
242 .as_ref()
243 .map(|features| features.tag_value_list().to_vec())
244 .unwrap_or_default();
245
246 if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
247 features[calt_index].1 = value;
248 } else {
249 features.push(("calt".into(), value));
250 }
251
252 settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
253 }
254}
255
256impl RenderOnce for BufferFontLigaturesControl {
257 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
258 let value = Self::read(cx);
259
260 CheckboxWithLabel::new(
261 "buffer-font-ligatures",
262 Label::new(self.name()),
263 value.into(),
264 |selection, _, cx| {
265 Self::write(
266 match selection {
267 ToggleState::Selected => true,
268 ToggleState::Unselected | ToggleState::Indeterminate => false,
269 },
270 cx,
271 );
272 },
273 )
274 }
275}
276
277#[derive(IntoElement)]
278struct InlineGitBlameControl;
279
280impl EditableSettingControl for InlineGitBlameControl {
281 type Value = bool;
282 type Settings = ProjectSettings;
283
284 fn name(&self) -> SharedString {
285 "Inline Git Blame".into()
286 }
287
288 fn read(cx: &App) -> Self::Value {
289 let settings = ProjectSettings::get_global(cx);
290 settings.git.inline_blame_enabled()
291 }
292
293 fn apply(
294 settings: &mut <Self::Settings as Settings>::FileContent,
295 value: Self::Value,
296 _cx: &App,
297 ) {
298 if let Some(inline_blame) = settings.git.inline_blame.as_mut() {
299 inline_blame.enabled = value;
300 } else {
301 settings.git.inline_blame = Some(InlineBlameSettings {
302 enabled: false,
303 ..Default::default()
304 });
305 }
306 }
307}
308
309impl RenderOnce for InlineGitBlameControl {
310 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
311 let value = Self::read(cx);
312
313 CheckboxWithLabel::new(
314 "inline-git-blame",
315 Label::new(self.name()),
316 value.into(),
317 |selection, _, cx| {
318 Self::write(
319 match selection {
320 ToggleState::Selected => true,
321 ToggleState::Unselected | ToggleState::Indeterminate => false,
322 },
323 cx,
324 );
325 },
326 )
327 }
328}
329
330#[derive(IntoElement)]
331struct LineNumbersControl;
332
333impl EditableSettingControl for LineNumbersControl {
334 type Value = bool;
335 type Settings = EditorSettings;
336
337 fn name(&self) -> SharedString {
338 "Line Numbers".into()
339 }
340
341 fn read(cx: &App) -> Self::Value {
342 let settings = EditorSettings::get_global(cx);
343 settings.gutter.line_numbers
344 }
345
346 fn apply(
347 settings: &mut <Self::Settings as Settings>::FileContent,
348 value: Self::Value,
349 _cx: &App,
350 ) {
351 if let Some(gutter) = settings.gutter.as_mut() {
352 gutter.line_numbers = Some(value);
353 } else {
354 settings.gutter = Some(crate::editor_settings::GutterContent {
355 line_numbers: Some(value),
356 ..Default::default()
357 });
358 }
359 }
360}
361
362impl RenderOnce for LineNumbersControl {
363 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
364 let value = Self::read(cx);
365
366 CheckboxWithLabel::new(
367 "line-numbers",
368 Label::new(self.name()),
369 value.into(),
370 |selection, _, cx| {
371 Self::write(
372 match selection {
373 ToggleState::Selected => true,
374 ToggleState::Unselected | ToggleState::Indeterminate => false,
375 },
376 cx,
377 );
378 },
379 )
380 }
381}
382
383#[derive(IntoElement)]
384struct RelativeLineNumbersControl;
385
386impl EditableSettingControl for RelativeLineNumbersControl {
387 type Value = bool;
388 type Settings = EditorSettings;
389
390 fn name(&self) -> SharedString {
391 "Relative Line Numbers".into()
392 }
393
394 fn read(cx: &App) -> Self::Value {
395 let settings = EditorSettings::get_global(cx);
396 settings.relative_line_numbers
397 }
398
399 fn apply(
400 settings: &mut <Self::Settings as Settings>::FileContent,
401 value: Self::Value,
402 _cx: &App,
403 ) {
404 settings.relative_line_numbers = Some(value);
405 }
406}
407
408impl RenderOnce for RelativeLineNumbersControl {
409 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
410 let value = Self::read(cx);
411
412 DropdownMenu::new(
413 "relative-line-numbers",
414 if value { "Relative" } else { "Ascending" },
415 ContextMenu::build(window, cx, |menu, _window, _cx| {
416 menu.custom_entry(
417 |_window, _cx| Label::new("Ascending").into_any_element(),
418 move |_, cx| Self::write(false, cx),
419 )
420 .custom_entry(
421 |_window, _cx| Label::new("Relative").into_any_element(),
422 move |_, cx| Self::write(true, cx),
423 )
424 }),
425 )
426 }
427}