settings_ui: Add font ligature settings controls (#15301)

Marshall Bowers created

This PR adds settings controls for changing whether ligatures are
enabled for the UI and buffer fonts.

Release Notes:

- N/A

Change summary

crates/editor/src/editor_settings_controls.rs          | 77 +++++++++++
crates/gpui/src/text_system/font_features.rs           | 10 +
crates/settings_ui/src/appearance_settings_controls.rs | 77 +++++++++++
3 files changed, 158 insertions(+), 6 deletions(-)

Detailed changes

crates/editor/src/editor_settings_controls.rs 🔗

@@ -1,4 +1,6 @@
-use gpui::{AppContext, FontWeight};
+use std::sync::Arc;
+
+use gpui::{AppContext, FontFeatures, FontWeight};
 use project::project_settings::{InlineBlameSettings, ProjectSettings};
 use settings::{EditableSettingControl, Settings};
 use theme::{FontFamilyCache, ThemeSettings};
@@ -28,7 +30,8 @@ impl RenderOnce for EditorSettingsControls {
                             .child(BufferFontFamilyControl)
                             .child(BufferFontWeightControl),
                     )
-                    .child(BufferFontSizeControl),
+                    .child(BufferFontSizeControl)
+                    .child(BufferFontLigaturesControl),
             )
             .child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
     }
@@ -190,6 +193,76 @@ impl RenderOnce for BufferFontWeightControl {
     }
 }
 
+#[derive(IntoElement)]
+struct BufferFontLigaturesControl;
+
+impl EditableSettingControl for BufferFontLigaturesControl {
+    type Value = bool;
+    type Settings = ThemeSettings;
+
+    fn name(&self) -> SharedString {
+        "Buffer Font Ligatures".into()
+    }
+
+    fn read(cx: &AppContext) -> Self::Value {
+        let settings = ThemeSettings::get_global(cx);
+        settings
+            .buffer_font
+            .features
+            .is_calt_enabled()
+            .unwrap_or(true)
+    }
+
+    fn apply(
+        settings: &mut <Self::Settings as Settings>::FileContent,
+        value: Self::Value,
+        _cx: &AppContext,
+    ) {
+        let value = if value { 1 } else { 0 };
+
+        let mut features = settings
+            .buffer_font_features
+            .as_ref()
+            .map(|features| {
+                features
+                    .tag_value_list()
+                    .into_iter()
+                    .cloned()
+                    .collect::<Vec<_>>()
+            })
+            .unwrap_or_default();
+
+        if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
+            features[calt_index].1 = value;
+        } else {
+            features.push(("calt".into(), value));
+        }
+
+        settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
+    }
+}
+
+impl RenderOnce for BufferFontLigaturesControl {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let value = Self::read(cx);
+
+        CheckboxWithLabel::new(
+            "buffer-font-ligatures",
+            Label::new(self.name()),
+            value.into(),
+            |selection, cx| {
+                Self::write(
+                    match selection {
+                        Selection::Selected => true,
+                        Selection::Unselected | Selection::Indeterminate => false,
+                    },
+                    cx,
+                );
+            },
+        )
+    }
+}
+
 #[derive(IntoElement)]
 struct InlineGitBlameControl;
 

crates/gpui/src/text_system/font_features.rs 🔗

@@ -12,6 +12,16 @@ impl FontFeatures {
     pub fn tag_value_list(&self) -> &[(String, u32)] {
         &self.0.as_slice()
     }
+
+    /// Returns whether the `calt` feature is enabled.
+    ///
+    /// Returns `None` if the feature is not present.
+    pub fn is_calt_enabled(&self) -> Option<bool> {
+        self.0
+            .iter()
+            .find(|(feature, _)| feature == "calt")
+            .map(|(_, value)| *value == 1)
+    }
 }
 
 impl std::fmt::Debug for FontFeatures {

crates/settings_ui/src/appearance_settings_controls.rs 🔗

@@ -1,9 +1,11 @@
-use gpui::{AppContext, FontWeight};
+use std::sync::Arc;
+
+use gpui::{AppContext, FontFeatures, FontWeight};
 use settings::{EditableSettingControl, Settings};
 use theme::{FontFamilyCache, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings};
 use ui::{
-    prelude::*, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
-    ToggleButton,
+    prelude::*, CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer,
+    SettingsGroup, ToggleButton,
 };
 
 #[derive(IntoElement)]
@@ -36,7 +38,8 @@ impl RenderOnce for AppearanceSettingsControls {
                             .child(UiFontFamilyControl)
                             .child(UiFontWeightControl),
                     )
-                    .child(UiFontSizeControl),
+                    .child(UiFontSizeControl)
+                    .child(UiFontLigaturesControl),
             )
     }
 }
@@ -320,3 +323,69 @@ impl RenderOnce for UiFontWeightControl {
             ))
     }
 }
+
+#[derive(IntoElement)]
+struct UiFontLigaturesControl;
+
+impl EditableSettingControl for UiFontLigaturesControl {
+    type Value = bool;
+    type Settings = ThemeSettings;
+
+    fn name(&self) -> SharedString {
+        "UI Font Ligatures".into()
+    }
+
+    fn read(cx: &AppContext) -> Self::Value {
+        let settings = ThemeSettings::get_global(cx);
+        settings.ui_font.features.is_calt_enabled().unwrap_or(true)
+    }
+
+    fn apply(
+        settings: &mut <Self::Settings as Settings>::FileContent,
+        value: Self::Value,
+        _cx: &AppContext,
+    ) {
+        let value = if value { 1 } else { 0 };
+
+        let mut features = settings
+            .ui_font_features
+            .as_ref()
+            .map(|features| {
+                features
+                    .tag_value_list()
+                    .into_iter()
+                    .cloned()
+                    .collect::<Vec<_>>()
+            })
+            .unwrap_or_default();
+
+        if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
+            features[calt_index].1 = value;
+        } else {
+            features.push(("calt".into(), value));
+        }
+
+        settings.ui_font_features = Some(FontFeatures(Arc::new(features)));
+    }
+}
+
+impl RenderOnce for UiFontLigaturesControl {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let value = Self::read(cx);
+
+        CheckboxWithLabel::new(
+            "ui-font-ligatures",
+            Label::new(self.name()),
+            value.into(),
+            |selection, cx| {
+                Self::write(
+                    match selection {
+                        Selection::Selected => true,
+                        Selection::Unselected | Selection::Indeterminate => false,
+                    },
+                    cx,
+                );
+            },
+        )
+    }
+}