1use std::sync::Arc;
2
3use gpui::{AppContext, FontFeatures, FontWeight};
4use project::project_settings::{InlineBlameSettings, ProjectSettings};
5use settings::{EditableSettingControl, Settings};
6use theme::{FontFamilyCache, ThemeSettings};
7use ui::{
8 prelude::*, CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer,
9 SettingsGroup,
10};
11
12use crate::EditorSettings;
13
14#[derive(IntoElement)]
15pub struct EditorSettingsControls {}
16
17impl EditorSettingsControls {
18 pub fn new() -> Self {
19 Self {}
20 }
21}
22
23impl RenderOnce for EditorSettingsControls {
24 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
25 SettingsContainer::new()
26 .child(
27 SettingsGroup::new("Font")
28 .child(
29 h_flex()
30 .gap_2()
31 .justify_between()
32 .child(BufferFontFamilyControl)
33 .child(BufferFontWeightControl),
34 )
35 .child(BufferFontSizeControl)
36 .child(BufferFontLigaturesControl),
37 )
38 .child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
39 .child(
40 SettingsGroup::new("Gutter").child(
41 h_flex()
42 .gap_2()
43 .justify_between()
44 .child(LineNumbersControl)
45 .child(RelativeLineNumbersControl),
46 ),
47 )
48 }
49}
50
51#[derive(IntoElement)]
52struct BufferFontFamilyControl;
53
54impl EditableSettingControl for BufferFontFamilyControl {
55 type Value = SharedString;
56 type Settings = ThemeSettings;
57
58 fn name(&self) -> SharedString {
59 "Buffer Font Family".into()
60 }
61
62 fn read(cx: &AppContext) -> Self::Value {
63 let settings = ThemeSettings::get_global(cx);
64 settings.buffer_font.family.clone()
65 }
66
67 fn apply(
68 settings: &mut <Self::Settings as Settings>::FileContent,
69 value: Self::Value,
70 _cx: &AppContext,
71 ) {
72 settings.buffer_font_family = Some(value.to_string());
73 }
74}
75
76impl RenderOnce for BufferFontFamilyControl {
77 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
78 let value = Self::read(cx);
79
80 h_flex()
81 .gap_2()
82 .child(Icon::new(IconName::Font))
83 .child(DropdownMenu::new(
84 "buffer-font-family",
85 value.clone(),
86 ContextMenu::build(cx, |mut menu, cx| {
87 let font_family_cache = FontFamilyCache::global(cx);
88
89 for font_name in font_family_cache.list_font_families(cx) {
90 menu = menu.custom_entry(
91 {
92 let font_name = font_name.clone();
93 move |_cx| Label::new(font_name.clone()).into_any_element()
94 },
95 {
96 let font_name = font_name.clone();
97 move |cx| {
98 Self::write(font_name.clone(), cx);
99 }
100 },
101 )
102 }
103
104 menu
105 }),
106 ))
107 }
108}
109
110#[derive(IntoElement)]
111struct BufferFontSizeControl;
112
113impl EditableSettingControl for BufferFontSizeControl {
114 type Value = Pixels;
115 type Settings = ThemeSettings;
116
117 fn name(&self) -> SharedString {
118 "Buffer Font Size".into()
119 }
120
121 fn read(cx: &AppContext) -> Self::Value {
122 let settings = ThemeSettings::get_global(cx);
123 settings.buffer_font_size
124 }
125
126 fn apply(
127 settings: &mut <Self::Settings as Settings>::FileContent,
128 value: Self::Value,
129 _cx: &AppContext,
130 ) {
131 settings.buffer_font_size = Some(value.into());
132 }
133}
134
135impl RenderOnce for BufferFontSizeControl {
136 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
137 let value = Self::read(cx);
138
139 h_flex()
140 .gap_2()
141 .child(Icon::new(IconName::FontSize))
142 .child(NumericStepper::new(
143 value.to_string(),
144 move |_, cx| {
145 Self::write(value - px(1.), cx);
146 },
147 move |_, cx| {
148 Self::write(value + px(1.), cx);
149 },
150 ))
151 }
152}
153
154#[derive(IntoElement)]
155struct BufferFontWeightControl;
156
157impl EditableSettingControl for BufferFontWeightControl {
158 type Value = FontWeight;
159 type Settings = ThemeSettings;
160
161 fn name(&self) -> SharedString {
162 "Buffer Font Weight".into()
163 }
164
165 fn read(cx: &AppContext) -> Self::Value {
166 let settings = ThemeSettings::get_global(cx);
167 settings.buffer_font.weight
168 }
169
170 fn apply(
171 settings: &mut <Self::Settings as Settings>::FileContent,
172 value: Self::Value,
173 _cx: &AppContext,
174 ) {
175 settings.buffer_font_weight = Some(value.0);
176 }
177}
178
179impl RenderOnce for BufferFontWeightControl {
180 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
181 let value = Self::read(cx);
182
183 h_flex()
184 .gap_2()
185 .child(Icon::new(IconName::FontWeight))
186 .child(DropdownMenu::new(
187 "buffer-font-weight",
188 value.0.to_string(),
189 ContextMenu::build(cx, |mut menu, _cx| {
190 for weight in FontWeight::ALL {
191 menu = menu.custom_entry(
192 move |_cx| Label::new(weight.0.to_string()).into_any_element(),
193 {
194 move |cx| {
195 Self::write(weight, cx);
196 }
197 },
198 )
199 }
200
201 menu
202 }),
203 ))
204 }
205}
206
207#[derive(IntoElement)]
208struct BufferFontLigaturesControl;
209
210impl EditableSettingControl for BufferFontLigaturesControl {
211 type Value = bool;
212 type Settings = ThemeSettings;
213
214 fn name(&self) -> SharedString {
215 "Buffer Font Ligatures".into()
216 }
217
218 fn read(cx: &AppContext) -> Self::Value {
219 let settings = ThemeSettings::get_global(cx);
220 settings
221 .buffer_font
222 .features
223 .is_calt_enabled()
224 .unwrap_or(true)
225 }
226
227 fn apply(
228 settings: &mut <Self::Settings as Settings>::FileContent,
229 value: Self::Value,
230 _cx: &AppContext,
231 ) {
232 let value = if value { 1 } else { 0 };
233
234 let mut features = settings
235 .buffer_font_features
236 .as_ref()
237 .map(|features| {
238 features
239 .tag_value_list()
240 .into_iter()
241 .cloned()
242 .collect::<Vec<_>>()
243 })
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, cx: &mut WindowContext) -> 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 Selection::Selected => true,
268 Selection::Unselected | Selection::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: &AppContext) -> 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: &AppContext,
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, cx: &mut WindowContext) -> 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 Selection::Selected => true,
321 Selection::Unselected | Selection::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: &AppContext) -> 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: &AppContext,
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, cx: &mut WindowContext) -> 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 Selection::Selected => true,
374 Selection::Unselected | Selection::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: &AppContext) -> 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: &AppContext,
403 ) {
404 settings.relative_line_numbers = Some(value);
405 }
406}
407
408impl RenderOnce for RelativeLineNumbersControl {
409 fn render(self, cx: &mut WindowContext) -> 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(cx, |menu, _cx| {
416 menu.custom_entry(
417 |_cx| Label::new("Ascending").into_any_element(),
418 move |cx| Self::write(false, cx),
419 )
420 .custom_entry(
421 |_cx| Label::new("Relative").into_any_element(),
422 move |cx| Self::write(true, cx),
423 )
424 }),
425 )
426 }
427}