editor_settings_controls.rs

  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}