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                "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}