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 "buffer-font-size",
144 value.to_string(),
145 move |_, cx| {
146 Self::write(value - px(1.), cx);
147 },
148 move |_, cx| {
149 Self::write(value + px(1.), cx);
150 },
151 ))
152 }
153}
154
155#[derive(IntoElement)]
156struct BufferFontWeightControl;
157
158impl EditableSettingControl for BufferFontWeightControl {
159 type Value = FontWeight;
160 type Settings = ThemeSettings;
161
162 fn name(&self) -> SharedString {
163 "Buffer Font Weight".into()
164 }
165
166 fn read(cx: &AppContext) -> Self::Value {
167 let settings = ThemeSettings::get_global(cx);
168 settings.buffer_font.weight
169 }
170
171 fn apply(
172 settings: &mut <Self::Settings as Settings>::FileContent,
173 value: Self::Value,
174 _cx: &AppContext,
175 ) {
176 settings.buffer_font_weight = Some(value.0);
177 }
178}
179
180impl RenderOnce for BufferFontWeightControl {
181 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
182 let value = Self::read(cx);
183
184 h_flex()
185 .gap_2()
186 .child(Icon::new(IconName::FontWeight))
187 .child(DropdownMenu::new(
188 "buffer-font-weight",
189 value.0.to_string(),
190 ContextMenu::build(cx, |mut menu, _cx| {
191 for weight in FontWeight::ALL {
192 menu = menu.custom_entry(
193 move |_cx| Label::new(weight.0.to_string()).into_any_element(),
194 {
195 move |cx| {
196 Self::write(weight, cx);
197 }
198 },
199 )
200 }
201
202 menu
203 }),
204 ))
205 }
206}
207
208#[derive(IntoElement)]
209struct BufferFontLigaturesControl;
210
211impl EditableSettingControl for BufferFontLigaturesControl {
212 type Value = bool;
213 type Settings = ThemeSettings;
214
215 fn name(&self) -> SharedString {
216 "Buffer Font Ligatures".into()
217 }
218
219 fn read(cx: &AppContext) -> Self::Value {
220 let settings = ThemeSettings::get_global(cx);
221 settings
222 .buffer_font
223 .features
224 .is_calt_enabled()
225 .unwrap_or(true)
226 }
227
228 fn apply(
229 settings: &mut <Self::Settings as Settings>::FileContent,
230 value: Self::Value,
231 _cx: &AppContext,
232 ) {
233 let value = if value { 1 } else { 0 };
234
235 let mut features = settings
236 .buffer_font_features
237 .as_ref()
238 .map(|features| {
239 features
240 .tag_value_list()
241 .into_iter()
242 .cloned()
243 .collect::<Vec<_>>()
244 })
245 .unwrap_or_default();
246
247 if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
248 features[calt_index].1 = value;
249 } else {
250 features.push(("calt".into(), value));
251 }
252
253 settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
254 }
255}
256
257impl RenderOnce for BufferFontLigaturesControl {
258 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
259 let value = Self::read(cx);
260
261 CheckboxWithLabel::new(
262 "buffer-font-ligatures",
263 Label::new(self.name()),
264 value.into(),
265 |selection, cx| {
266 Self::write(
267 match selection {
268 Selection::Selected => true,
269 Selection::Unselected | Selection::Indeterminate => false,
270 },
271 cx,
272 );
273 },
274 )
275 }
276}
277
278#[derive(IntoElement)]
279struct InlineGitBlameControl;
280
281impl EditableSettingControl for InlineGitBlameControl {
282 type Value = bool;
283 type Settings = ProjectSettings;
284
285 fn name(&self) -> SharedString {
286 "Inline Git Blame".into()
287 }
288
289 fn read(cx: &AppContext) -> Self::Value {
290 let settings = ProjectSettings::get_global(cx);
291 settings.git.inline_blame_enabled()
292 }
293
294 fn apply(
295 settings: &mut <Self::Settings as Settings>::FileContent,
296 value: Self::Value,
297 _cx: &AppContext,
298 ) {
299 if let Some(inline_blame) = settings.git.inline_blame.as_mut() {
300 inline_blame.enabled = value;
301 } else {
302 settings.git.inline_blame = Some(InlineBlameSettings {
303 enabled: false,
304 ..Default::default()
305 });
306 }
307 }
308}
309
310impl RenderOnce for InlineGitBlameControl {
311 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
312 let value = Self::read(cx);
313
314 CheckboxWithLabel::new(
315 "inline-git-blame",
316 Label::new(self.name()),
317 value.into(),
318 |selection, cx| {
319 Self::write(
320 match selection {
321 Selection::Selected => true,
322 Selection::Unselected | Selection::Indeterminate => false,
323 },
324 cx,
325 );
326 },
327 )
328 }
329}
330
331#[derive(IntoElement)]
332struct LineNumbersControl;
333
334impl EditableSettingControl for LineNumbersControl {
335 type Value = bool;
336 type Settings = EditorSettings;
337
338 fn name(&self) -> SharedString {
339 "Line Numbers".into()
340 }
341
342 fn read(cx: &AppContext) -> Self::Value {
343 let settings = EditorSettings::get_global(cx);
344 settings.gutter.line_numbers
345 }
346
347 fn apply(
348 settings: &mut <Self::Settings as Settings>::FileContent,
349 value: Self::Value,
350 _cx: &AppContext,
351 ) {
352 if let Some(gutter) = settings.gutter.as_mut() {
353 gutter.line_numbers = Some(value);
354 } else {
355 settings.gutter = Some(crate::editor_settings::GutterContent {
356 line_numbers: Some(value),
357 ..Default::default()
358 });
359 }
360 }
361}
362
363impl RenderOnce for LineNumbersControl {
364 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
365 let value = Self::read(cx);
366
367 CheckboxWithLabel::new(
368 "line-numbers",
369 Label::new(self.name()),
370 value.into(),
371 |selection, cx| {
372 Self::write(
373 match selection {
374 Selection::Selected => true,
375 Selection::Unselected | Selection::Indeterminate => false,
376 },
377 cx,
378 );
379 },
380 )
381 }
382}
383
384#[derive(IntoElement)]
385struct RelativeLineNumbersControl;
386
387impl EditableSettingControl for RelativeLineNumbersControl {
388 type Value = bool;
389 type Settings = EditorSettings;
390
391 fn name(&self) -> SharedString {
392 "Relative Line Numbers".into()
393 }
394
395 fn read(cx: &AppContext) -> Self::Value {
396 let settings = EditorSettings::get_global(cx);
397 settings.relative_line_numbers
398 }
399
400 fn apply(
401 settings: &mut <Self::Settings as Settings>::FileContent,
402 value: Self::Value,
403 _cx: &AppContext,
404 ) {
405 settings.relative_line_numbers = Some(value);
406 }
407}
408
409impl RenderOnce for RelativeLineNumbersControl {
410 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
411 let value = Self::read(cx);
412
413 DropdownMenu::new(
414 "relative-line-numbers",
415 if value { "Relative" } else { "Ascending" },
416 ContextMenu::build(cx, |menu, _cx| {
417 menu.custom_entry(
418 |_cx| Label::new("Ascending").into_any_element(),
419 move |cx| Self::write(false, cx),
420 )
421 .custom_entry(
422 |_cx| Label::new("Relative").into_any_element(),
423 move |cx| Self::write(true, cx),
424 )
425 }),
426 )
427 }
428}