1use std::sync::Arc;
2
3use gpui::{App, 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 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(value.to_string());
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.clone(),
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 let settings = ThemeSettings::get_global(cx);
129 settings.buffer_font_size
130 }
131
132 fn apply(
133 settings: &mut <Self::Settings as Settings>::FileContent,
134 value: Self::Value,
135 _cx: &App,
136 ) {
137 settings.buffer_font_size = Some(value.into());
138 }
139}
140
141impl RenderOnce for BufferFontSizeControl {
142 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
143 let value = Self::read(cx);
144
145 h_flex()
146 .gap_2()
147 .child(Icon::new(IconName::FontSize))
148 .child(NumericStepper::new(
149 "buffer-font-size",
150 value.to_string(),
151 move |_, _, cx| {
152 Self::write(value - px(1.), cx);
153 },
154 move |_, _, cx| {
155 Self::write(value + px(1.), cx);
156 },
157 ))
158 }
159}
160
161#[derive(IntoElement)]
162struct BufferFontWeightControl;
163
164impl EditableSettingControl for BufferFontWeightControl {
165 type Value = FontWeight;
166 type Settings = ThemeSettings;
167
168 fn name(&self) -> SharedString {
169 "Buffer Font Weight".into()
170 }
171
172 fn read(cx: &App) -> Self::Value {
173 let settings = ThemeSettings::get_global(cx);
174 settings.buffer_font.weight
175 }
176
177 fn apply(
178 settings: &mut <Self::Settings as Settings>::FileContent,
179 value: Self::Value,
180 _cx: &App,
181 ) {
182 settings.buffer_font_weight = Some(value.0);
183 }
184}
185
186impl RenderOnce for BufferFontWeightControl {
187 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
188 let value = Self::read(cx);
189
190 h_flex()
191 .gap_2()
192 .child(Icon::new(IconName::FontWeight))
193 .child(DropdownMenu::new(
194 "buffer-font-weight",
195 value.0.to_string(),
196 ContextMenu::build(window, cx, |mut menu, _window, _cx| {
197 for weight in FontWeight::ALL {
198 menu = menu.custom_entry(
199 move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(),
200 {
201 move |_, cx| {
202 Self::write(weight, cx);
203 }
204 },
205 )
206 }
207
208 menu
209 }),
210 ))
211 }
212}
213
214#[derive(IntoElement)]
215struct BufferFontLigaturesControl;
216
217impl EditableSettingControl for BufferFontLigaturesControl {
218 type Value = bool;
219 type Settings = ThemeSettings;
220
221 fn name(&self) -> SharedString {
222 "Buffer Font Ligatures".into()
223 }
224
225 fn read(cx: &App) -> Self::Value {
226 let settings = ThemeSettings::get_global(cx);
227 settings
228 .buffer_font
229 .features
230 .is_calt_enabled()
231 .unwrap_or(true)
232 }
233
234 fn apply(
235 settings: &mut <Self::Settings as Settings>::FileContent,
236 value: Self::Value,
237 _cx: &App,
238 ) {
239 let value = if value { 1 } else { 0 };
240
241 let mut features = settings
242 .buffer_font_features
243 .as_ref()
244 .map(|features| features.tag_value_list().to_vec())
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, _window: &mut Window, cx: &mut App) -> 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 ToggleState::Selected => true,
269 ToggleState::Unselected | ToggleState::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: &App) -> 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: &App,
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, _window: &mut Window, cx: &mut App) -> 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 ToggleState::Selected => true,
322 ToggleState::Unselected | ToggleState::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: &App) -> 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: &App,
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, _window: &mut Window, cx: &mut App) -> 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 ToggleState::Selected => true,
375 ToggleState::Unselected | ToggleState::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: &App) -> 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: &App,
404 ) {
405 settings.relative_line_numbers = Some(value);
406 }
407}
408
409impl RenderOnce for RelativeLineNumbersControl {
410 fn render(self, window: &mut Window, cx: &mut App) -> 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(window, cx, |menu, _window, _cx| {
417 menu.custom_entry(
418 |_window, _cx| Label::new("Ascending").into_any_element(),
419 move |_, cx| Self::write(false, cx),
420 )
421 .custom_entry(
422 |_window, _cx| Label::new("Relative").into_any_element(),
423 move |_, cx| Self::write(true, cx),
424 )
425 }),
426 )
427 }
428}