settings_ui: Add UI and buffer font family controls (#15124)

Marshall Bowers created

This PR adds settings controls for changing the UI and buffer font
families.

Release Notes:

- N/A

Change summary

crates/editor/src/editor_settings_controls.rs          | 71 +++++++++++
crates/settings_ui/src/appearance_settings_controls.rs | 71 +++++++++++
crates/theme/src/font_family_cache.rs                  | 52 ++++++++
crates/theme/src/theme.rs                              |  3 
4 files changed, 191 insertions(+), 6 deletions(-)

Detailed changes

crates/editor/src/editor_settings_controls.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::{AppContext, FontWeight};
 use project::project_settings::{InlineBlameSettings, ProjectSettings};
 use settings::{EditableSettingControl, Settings};
-use theme::ThemeSettings;
+use theme::{FontFamilyCache, ThemeSettings};
 use ui::{
     prelude::*, CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer,
     SettingsGroup,
@@ -21,13 +21,78 @@ impl RenderOnce for EditorSettingsControls {
         SettingsContainer::new()
             .child(
                 SettingsGroup::new("Font")
-                    .child(BufferFontSizeControl)
-                    .child(BufferFontWeightControl),
+                    .child(
+                        h_flex()
+                            .gap_2()
+                            .justify_between()
+                            .child(BufferFontFamilyControl)
+                            .child(BufferFontWeightControl),
+                    )
+                    .child(BufferFontSizeControl),
             )
             .child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
     }
 }
 
+#[derive(IntoElement)]
+struct BufferFontFamilyControl;
+
+impl EditableSettingControl for BufferFontFamilyControl {
+    type Value = SharedString;
+    type Settings = ThemeSettings;
+
+    fn name(&self) -> SharedString {
+        "Buffer Font Family".into()
+    }
+
+    fn read(cx: &AppContext) -> Self::Value {
+        let settings = ThemeSettings::get_global(cx);
+        settings.buffer_font.family.clone()
+    }
+
+    fn apply(
+        settings: &mut <Self::Settings as Settings>::FileContent,
+        value: Self::Value,
+        _cx: &AppContext,
+    ) {
+        settings.buffer_font_family = Some(value.to_string());
+    }
+}
+
+impl RenderOnce for BufferFontFamilyControl {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let value = Self::read(cx);
+
+        h_flex()
+            .gap_2()
+            .child(Icon::new(IconName::Font))
+            .child(DropdownMenu::new(
+                "buffer-font-family",
+                value.clone(),
+                ContextMenu::build(cx, |mut menu, cx| {
+                    let font_family_cache = FontFamilyCache::global(cx);
+
+                    for font_name in font_family_cache.list_font_families(cx) {
+                        menu = menu.custom_entry(
+                            {
+                                let font_name = font_name.clone();
+                                move |_cx| Label::new(font_name.clone()).into_any_element()
+                            },
+                            {
+                                let font_name = font_name.clone();
+                                move |cx| {
+                                    Self::write(font_name.clone(), cx);
+                                }
+                            },
+                        )
+                    }
+
+                    menu
+                }),
+            ))
+    }
+}
+
 #[derive(IntoElement)]
 struct BufferFontSizeControl;
 

crates/settings_ui/src/appearance_settings_controls.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{AppContext, FontWeight};
 use settings::{EditableSettingControl, Settings};
-use theme::{SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings};
+use theme::{FontFamilyCache, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings};
 use ui::{
     prelude::*, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
     ToggleButton,
@@ -29,8 +29,14 @@ impl RenderOnce for AppearanceSettingsControls {
             )
             .child(
                 SettingsGroup::new("Font")
-                    .child(UiFontSizeControl)
-                    .child(UiFontWeightControl),
+                    .child(
+                        h_flex()
+                            .gap_2()
+                            .justify_between()
+                            .child(UiFontFamilyControl)
+                            .child(UiFontWeightControl),
+                    )
+                    .child(UiFontSizeControl),
             )
     }
 }
@@ -159,6 +165,65 @@ impl RenderOnce for ThemeModeControl {
     }
 }
 
+#[derive(IntoElement)]
+struct UiFontFamilyControl;
+
+impl EditableSettingControl for UiFontFamilyControl {
+    type Value = SharedString;
+    type Settings = ThemeSettings;
+
+    fn name(&self) -> SharedString {
+        "UI Font Family".into()
+    }
+
+    fn read(cx: &AppContext) -> Self::Value {
+        let settings = ThemeSettings::get_global(cx);
+        settings.ui_font.family.clone()
+    }
+
+    fn apply(
+        settings: &mut <Self::Settings as Settings>::FileContent,
+        value: Self::Value,
+        _cx: &AppContext,
+    ) {
+        settings.ui_font_family = Some(value.to_string());
+    }
+}
+
+impl RenderOnce for UiFontFamilyControl {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let value = Self::read(cx);
+
+        h_flex()
+            .gap_2()
+            .child(Icon::new(IconName::Font))
+            .child(DropdownMenu::new(
+                "ui-font-family",
+                value.clone(),
+                ContextMenu::build(cx, |mut menu, cx| {
+                    let font_family_cache = FontFamilyCache::global(cx);
+
+                    for font_name in font_family_cache.list_font_families(cx) {
+                        menu = menu.custom_entry(
+                            {
+                                let font_name = font_name.clone();
+                                move |_cx| Label::new(font_name.clone()).into_any_element()
+                            },
+                            {
+                                let font_name = font_name.clone();
+                                move |cx| {
+                                    Self::write(font_name.clone(), cx);
+                                }
+                            },
+                        )
+                    }
+
+                    menu
+                }),
+            ))
+    }
+}
+
 #[derive(IntoElement)]
 struct UiFontSizeControl;
 

crates/theme/src/font_family_cache.rs 🔗

@@ -0,0 +1,52 @@
+use std::sync::Arc;
+use std::time::Instant;
+
+use gpui::{AppContext, Global, ReadGlobal, SharedString};
+use parking_lot::RwLock;
+
+#[derive(Default)]
+struct FontFamilyCacheState {
+    loaded_at: Option<Instant>,
+    font_families: Vec<SharedString>,
+}
+
+/// A cache for the list of font families.
+///
+/// Listing the available font families from the text system is expensive,
+/// so we do it once and then use the cached values each render.
+#[derive(Default)]
+pub struct FontFamilyCache {
+    state: RwLock<FontFamilyCacheState>,
+}
+
+#[derive(Default)]
+struct GlobalFontFamilyCache(Arc<FontFamilyCache>);
+
+impl Global for GlobalFontFamilyCache {}
+
+impl FontFamilyCache {
+    pub fn init_global(cx: &mut AppContext) {
+        cx.default_global::<GlobalFontFamilyCache>();
+    }
+
+    pub fn global(cx: &AppContext) -> Arc<Self> {
+        GlobalFontFamilyCache::global(cx).0.clone()
+    }
+
+    pub fn list_font_families(&self, cx: &AppContext) -> Vec<SharedString> {
+        if self.state.read().loaded_at.is_some() {
+            return self.state.read().font_families.clone();
+        }
+
+        let mut lock = self.state.write();
+        lock.font_families = cx
+            .text_system()
+            .all_font_names()
+            .into_iter()
+            .map(SharedString::from)
+            .collect();
+        lock.loaded_at = Some(Instant::now());
+
+        lock.font_families.clone()
+    }
+}

crates/theme/src/theme.rs 🔗

@@ -8,6 +8,7 @@
 
 mod default_colors;
 mod default_theme;
+mod font_family_cache;
 mod one_themes;
 pub mod prelude;
 mod registry;
@@ -21,6 +22,7 @@ use std::sync::Arc;
 use ::settings::{Settings, SettingsStore};
 pub use default_colors::*;
 pub use default_theme::*;
+pub use font_family_cache::*;
 pub use registry::*;
 pub use scale::*;
 pub use schema::*;
@@ -82,6 +84,7 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
     }
 
     ThemeSettings::register(cx);
+    FontFamilyCache::init_global(cx);
 
     let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
     cx.observe_global::<SettingsStore>(move |cx| {