settings_ui: Add prototype for settings controls (#15090)

Marshall Bowers created

This PR adds a prototype for controlling settings via the settings UI.

Still staff-shipped.

Release Notes:

- N/A

Change summary

Cargo.lock                                  |   4 
crates/settings_ui/Cargo.toml               |   4 
crates/settings_ui/src/settings_ui.rs       |  11 +
crates/settings_ui/src/theme_settings_ui.rs | 190 +++++++++++++++++++++++
4 files changed, 208 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock 🔗

@@ -9586,7 +9586,11 @@ version = "0.1.0"
 dependencies = [
  "command_palette_hooks",
  "feature_flags",
+ "fs",
  "gpui",
+ "project",
+ "settings",
+ "theme",
  "ui",
  "workspace",
 ]

crates/settings_ui/Cargo.toml 🔗

@@ -14,6 +14,10 @@ path = "src/settings_ui.rs"
 [dependencies]
 command_palette_hooks.workspace = true
 feature_flags.workspace = true
+fs.workspace = true
 gpui.workspace = true
+project.workspace = true
+settings.workspace = true
+theme.workspace = true
 ui.workspace = true
 workspace.workspace = true

crates/settings_ui/src/settings_ui.rs 🔗

@@ -1,3 +1,5 @@
+mod theme_settings_ui;
+
 use std::any::TypeId;
 
 use command_palette_hooks::CommandPaletteFilter;
@@ -7,6 +9,10 @@ use ui::prelude::*;
 use workspace::item::{Item, ItemEvent};
 use workspace::Workspace;
 
+use crate::theme_settings_ui::{
+    BufferFontSizeSetting, EditableSetting, InlineGitBlameSetting, UiFontSizeSetting,
+};
+
 pub struct SettingsUiFeatureFlag;
 
 impl FeatureFlag for SettingsUiFeatureFlag {
@@ -95,7 +101,7 @@ impl Item for SettingsPage {
 }
 
 impl Render for SettingsPage {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         v_flex()
             .p_4()
             .size_full()
@@ -103,5 +109,8 @@ impl Render for SettingsPage {
             .child(Label::new(
                 "Nothing to see here yet. Feature-flagged for staff.",
             ))
+            .child(UiFontSizeSetting::new(cx))
+            .child(BufferFontSizeSetting::new(cx))
+            .child(InlineGitBlameSetting::new(cx))
     }
 }

crates/settings_ui/src/theme_settings_ui.rs 🔗

@@ -0,0 +1,190 @@
+use fs::Fs;
+use gpui::AppContext;
+use project::project_settings::{InlineBlameSettings, ProjectSettings};
+use settings::{update_settings_file, Settings};
+use theme::ThemeSettings;
+use ui::{prelude::*, CheckboxWithLabel, NumericStepper};
+
+pub trait EditableSetting: RenderOnce {
+    /// The type of the setting value.
+    type Value: Send;
+
+    /// The settings type to which this setting belongs.
+    type Settings: Settings;
+
+    /// Returns the name of this setting.
+    fn name(&self) -> SharedString;
+
+    /// Returns the icon to be displayed in place of the setting name.
+    fn icon(&self) -> Option<IconName> {
+        None
+    }
+
+    /// Returns a new instance of this setting.
+    fn new(cx: &AppContext) -> Self;
+
+    /// Applies the given setting file to the settings file contents.
+    ///
+    /// This will be called when writing the setting value back to the settings file.
+    fn apply(settings: &mut <Self::Settings as Settings>::FileContent, value: Self::Value);
+
+    /// Writes the given setting value to the settings files.
+    fn write(value: Self::Value, cx: &AppContext) {
+        let fs = <dyn Fs>::global(cx);
+
+        update_settings_file::<Self::Settings>(fs, cx, move |settings, _cx| {
+            Self::apply(settings, value);
+        });
+    }
+}
+
+#[derive(IntoElement)]
+pub struct UiFontSizeSetting(Pixels);
+
+impl EditableSetting for UiFontSizeSetting {
+    type Value = Pixels;
+    type Settings = ThemeSettings;
+
+    fn name(&self) -> SharedString {
+        "UI Font Size".into()
+    }
+
+    fn icon(&self) -> Option<IconName> {
+        Some(IconName::FontSize)
+    }
+
+    fn new(cx: &AppContext) -> Self {
+        let settings = ThemeSettings::get_global(cx);
+
+        Self(settings.ui_font_size)
+    }
+
+    fn apply(settings: &mut <Self::Settings as Settings>::FileContent, value: Self::Value) {
+        settings.ui_font_size = Some(value.into());
+    }
+}
+
+impl RenderOnce for UiFontSizeSetting {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        let value = self.0;
+
+        h_flex()
+            .gap_2()
+            .map(|el| {
+                if let Some(icon) = self.icon() {
+                    el.child(Icon::new(icon))
+                } else {
+                    el.child(Label::new(self.name()))
+                }
+            })
+            .child(NumericStepper::new(
+                self.0.to_string(),
+                move |_, cx| {
+                    Self::write(value - px(1.), cx);
+                },
+                move |_, cx| {
+                    Self::write(value + px(1.), cx);
+                },
+            ))
+    }
+}
+
+#[derive(IntoElement)]
+pub struct BufferFontSizeSetting(Pixels);
+
+impl EditableSetting for BufferFontSizeSetting {
+    type Value = Pixels;
+    type Settings = ThemeSettings;
+
+    fn name(&self) -> SharedString {
+        "Buffer Font Size".into()
+    }
+
+    fn icon(&self) -> Option<IconName> {
+        Some(IconName::FontSize)
+    }
+
+    fn new(cx: &AppContext) -> Self {
+        let settings = ThemeSettings::get_global(cx);
+
+        Self(settings.buffer_font_size)
+    }
+
+    fn apply(settings: &mut <Self::Settings as Settings>::FileContent, value: Self::Value) {
+        settings.buffer_font_size = Some(value.into());
+    }
+}
+
+impl RenderOnce for BufferFontSizeSetting {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        let value = self.0;
+
+        h_flex()
+            .gap_2()
+            .map(|el| {
+                if let Some(icon) = self.icon() {
+                    el.child(Icon::new(icon))
+                } else {
+                    el.child(Label::new(self.name()))
+                }
+            })
+            .child(NumericStepper::new(
+                self.0.to_string(),
+                move |_, cx| {
+                    Self::write(value - px(1.), cx);
+                },
+                move |_, cx| {
+                    Self::write(value + px(1.), cx);
+                },
+            ))
+    }
+}
+
+#[derive(IntoElement)]
+pub struct InlineGitBlameSetting(bool);
+
+impl EditableSetting for InlineGitBlameSetting {
+    type Value = bool;
+    type Settings = ProjectSettings;
+
+    fn name(&self) -> SharedString {
+        "Inline Git Blame".into()
+    }
+
+    fn new(cx: &AppContext) -> Self {
+        let settings = ProjectSettings::get_global(cx);
+        Self(settings.git.inline_blame_enabled())
+    }
+
+    fn apply(settings: &mut <Self::Settings as Settings>::FileContent, value: Self::Value) {
+        if let Some(inline_blame) = settings.git.inline_blame.as_mut() {
+            inline_blame.enabled = value;
+        } else {
+            settings.git.inline_blame = Some(InlineBlameSettings {
+                enabled: false,
+                ..Default::default()
+            });
+        }
+    }
+}
+
+impl RenderOnce for InlineGitBlameSetting {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        let value = self.0;
+
+        CheckboxWithLabel::new(
+            "inline-git-blame",
+            Label::new(self.name()),
+            value.into(),
+            |selection, cx| {
+                Self::write(
+                    match selection {
+                        Selection::Selected => true,
+                        Selection::Unselected | Selection::Indeterminate => false,
+                    },
+                    cx,
+                );
+            },
+        )
+    }
+}