From 274e56b0860ec375dd16a1feaf3e29f2dcdd6ba6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 24 Jul 2024 14:09:13 -0400 Subject: [PATCH] settings_ui: Add UI and buffer font weight controls (#15104) This PR adds settings controls for the UI and buffer font weight settings. It also does some work around grouping the settings into related sections. Release Notes: - N/A --- Cargo.lock | 3 +- crates/editor/src/editor.rs | 2 + crates/editor/src/editor_settings_controls.rs | 167 +++++++++++++++ crates/gpui/src/text_system.rs | 13 ++ .../settings/src/editable_setting_control.rs | 33 +++ crates/settings/src/settings.rs | 2 + crates/settings_ui/Cargo.toml | 3 +- crates/settings_ui/src/settings_ui.rs | 28 ++- .../src/theme_settings_controls.rs | 112 +++++++++++ crates/settings_ui/src/theme_settings_ui.rs | 190 ------------------ crates/ui/src/components.rs | 6 +- crates/ui/src/components/dropdown_menu.rs | 138 +++++++++---- crates/ui/src/components/setting.rs | 41 ++-- .../ui/src/components/settings_container.rs | 33 +++ crates/ui/src/components/settings_group.rs | 36 ++++ crates/ui/src/components/stories/setting.rs | 22 +- 16 files changed, 557 insertions(+), 272 deletions(-) create mode 100644 crates/editor/src/editor_settings_controls.rs create mode 100644 crates/settings/src/editable_setting_control.rs create mode 100644 crates/settings_ui/src/theme_settings_controls.rs delete mode 100644 crates/settings_ui/src/theme_settings_ui.rs create mode 100644 crates/ui/src/components/settings_container.rs create mode 100644 crates/ui/src/components/settings_group.rs diff --git a/Cargo.lock b/Cargo.lock index 74195378b4..cf54851bc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9585,10 +9585,9 @@ name = "settings_ui" version = "0.1.0" dependencies = [ "command_palette_hooks", + "editor", "feature_flags", - "fs", "gpui", - "project", "settings", "theme", "ui", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c0aa4c332c..b4215bd224 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -18,6 +18,7 @@ mod blink_manager; mod debounced_delay; pub mod display_map; mod editor_settings; +mod editor_settings_controls; mod element; mod git; mod highlight_matching_bracket; @@ -57,6 +58,7 @@ use debounced_delay::DebouncedDelay; use display_map::*; pub use display_map::{DisplayPoint, FoldPlaceholder}; pub use editor_settings::{CurrentLineHighlight, EditorSettings}; +pub use editor_settings_controls::*; use element::LineWithInvisibles; pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs new file mode 100644 index 0000000000..815e7b4aee --- /dev/null +++ b/crates/editor/src/editor_settings_controls.rs @@ -0,0 +1,167 @@ +use gpui::{AppContext, FontWeight}; +use project::project_settings::{InlineBlameSettings, ProjectSettings}; +use settings::{EditableSettingControl, Settings}; +use theme::ThemeSettings; +use ui::{ + prelude::*, CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, + SettingsGroup, +}; + +#[derive(IntoElement)] +pub struct EditorSettingsControls {} + +impl EditorSettingsControls { + pub fn new() -> Self { + Self {} + } +} + +impl RenderOnce for EditorSettingsControls { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + SettingsContainer::new() + .child( + SettingsGroup::new("Font") + .child(BufferFontSizeControl) + .child(BufferFontWeightControl), + ) + .child(SettingsGroup::new("Editor").child(InlineGitBlameControl)) + } +} + +#[derive(IntoElement)] +struct BufferFontSizeControl; + +impl EditableSettingControl for BufferFontSizeControl { + type Value = Pixels; + type Settings = ThemeSettings; + + fn name(&self) -> SharedString { + "Buffer Font Size".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ThemeSettings::get_global(cx); + settings.buffer_font_size + } + + fn apply(settings: &mut ::FileContent, value: Self::Value) { + settings.buffer_font_size = Some(value.into()); + } +} + +impl RenderOnce for BufferFontSizeControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + h_flex() + .gap_2() + .child(Icon::new(IconName::FontSize)) + .child(NumericStepper::new( + value.to_string(), + move |_, cx| { + Self::write(value - px(1.), cx); + }, + move |_, cx| { + Self::write(value + px(1.), cx); + }, + )) + } +} + +#[derive(IntoElement)] +struct BufferFontWeightControl; + +impl EditableSettingControl for BufferFontWeightControl { + type Value = FontWeight; + type Settings = ThemeSettings; + + fn name(&self) -> SharedString { + "Buffer Font Weight".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ThemeSettings::get_global(cx); + settings.buffer_font.weight + } + + fn apply(settings: &mut ::FileContent, value: Self::Value) { + settings.buffer_font_weight = Some(value.0); + } +} + +impl RenderOnce for BufferFontWeightControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + h_flex() + .gap_2() + .child(Icon::new(IconName::FontWeight)) + .child(DropdownMenu::new( + "buffer-font-weight", + value.0.to_string(), + ContextMenu::build(cx, |mut menu, _cx| { + for weight in FontWeight::ALL { + menu = menu.custom_entry( + move |_cx| Label::new(weight.0.to_string()).into_any_element(), + { + move |cx| { + Self::write(weight, cx); + } + }, + ) + } + + menu + }), + )) + } +} + +#[derive(IntoElement)] +struct InlineGitBlameControl; + +impl EditableSettingControl for InlineGitBlameControl { + type Value = bool; + type Settings = ProjectSettings; + + fn name(&self) -> SharedString { + "Inline Git Blame".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ProjectSettings::get_global(cx); + settings.git.inline_blame_enabled() + } + + fn apply(settings: &mut ::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 InlineGitBlameControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + 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, + ); + }, + ) + } +} diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index b891192a70..a6ff8a8d16 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -595,6 +595,19 @@ impl FontWeight { pub const EXTRA_BOLD: FontWeight = FontWeight(800.0); /// Black weight (900), the thickest value. pub const BLACK: FontWeight = FontWeight(900.0); + + /// All of the font weights, in order from thinnest to thickest. + pub const ALL: [FontWeight; 9] = [ + Self::THIN, + Self::EXTRA_LIGHT, + Self::LIGHT, + Self::NORMAL, + Self::MEDIUM, + Self::SEMIBOLD, + Self::BOLD, + Self::EXTRA_BOLD, + Self::BLACK, + ]; } /// Allows italic or oblique faces to be selected. diff --git a/crates/settings/src/editable_setting_control.rs b/crates/settings/src/editable_setting_control.rs new file mode 100644 index 0000000000..a62097006f --- /dev/null +++ b/crates/settings/src/editable_setting_control.rs @@ -0,0 +1,33 @@ +use fs::Fs; +use gpui::{AppContext, RenderOnce, SharedString}; + +use crate::{update_settings_file, Settings}; + +/// A UI control that can be used to edit a setting. +pub trait EditableSettingControl: 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; + + /// Reads the setting value from the settings. + fn read(cx: &AppContext) -> Self::Value; + + /// 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 ::FileContent, value: Self::Value); + + /// Writes the given setting value to the settings files. + fn write(value: Self::Value, cx: &AppContext) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |settings, _cx| { + Self::apply(settings, value); + }); + } +} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 193f1a28a7..c47301c7ca 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,3 +1,4 @@ +mod editable_setting_control; mod keymap_file; mod settings_file; mod settings_store; @@ -7,6 +8,7 @@ use rust_embed::RustEmbed; use std::{borrow::Cow, str}; use util::asset_str; +pub use editable_setting_control::*; pub use keymap_file::KeymapFile; pub use settings_file::*; pub use settings_store::{ diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 8f6a33fe42..63ab61f4cb 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -13,10 +13,9 @@ path = "src/settings_ui.rs" [dependencies] command_palette_hooks.workspace = true +editor.workspace = true feature_flags.workspace = true -fs.workspace = true gpui.workspace = true -project.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index a166af53b0..ad401b197f 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,17 +1,16 @@ -mod theme_settings_ui; +mod theme_settings_controls; use std::any::TypeId; use command_palette_hooks::CommandPaletteFilter; +use editor::EditorSettingsControls; use feature_flags::{FeatureFlag, FeatureFlagViewExt}; use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, View}; use ui::prelude::*; use workspace::item::{Item, ItemEvent}; use workspace::Workspace; -use crate::theme_settings_ui::{ - BufferFontSizeSetting, EditableSetting, InlineGitBlameSetting, UiFontSizeSetting, -}; +use crate::theme_settings_controls::ThemeSettingsControls; pub struct SettingsUiFeatureFlag; @@ -101,16 +100,23 @@ impl Item for SettingsPage { } impl Render for SettingsPage { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { v_flex() .p_4() .size_full() + .gap_4() .child(Label::new("Settings").size(LabelSize::Large)) - .child(Label::new( - "Nothing to see here yet. Feature-flagged for staff.", - )) - .child(UiFontSizeSetting::new(cx)) - .child(BufferFontSizeSetting::new(cx)) - .child(InlineGitBlameSetting::new(cx)) + .child( + v_flex() + .gap_1() + .child(Label::new("Appearance")) + .child(ThemeSettingsControls::new()), + ) + .child( + v_flex() + .gap_1() + .child(Label::new("Editor")) + .child(EditorSettingsControls::new()), + ) } } diff --git a/crates/settings_ui/src/theme_settings_controls.rs b/crates/settings_ui/src/theme_settings_controls.rs new file mode 100644 index 0000000000..4b7cdb8cf8 --- /dev/null +++ b/crates/settings_ui/src/theme_settings_controls.rs @@ -0,0 +1,112 @@ +use gpui::{AppContext, FontWeight}; +use settings::{EditableSettingControl, Settings}; +use theme::ThemeSettings; +use ui::{prelude::*, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup}; + +#[derive(IntoElement)] +pub struct ThemeSettingsControls {} + +impl ThemeSettingsControls { + pub fn new() -> Self { + Self {} + } +} + +impl RenderOnce for ThemeSettingsControls { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + SettingsContainer::new().child( + SettingsGroup::new("Font") + .child(UiFontSizeControl) + .child(UiFontWeightControl), + ) + } +} + +#[derive(IntoElement)] +struct UiFontSizeControl; + +impl EditableSettingControl for UiFontSizeControl { + type Value = Pixels; + type Settings = ThemeSettings; + + fn name(&self) -> SharedString { + "UI Font Size".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ThemeSettings::get_global(cx); + settings.ui_font_size + } + + fn apply(settings: &mut ::FileContent, value: Self::Value) { + settings.ui_font_size = Some(value.into()); + } +} + +impl RenderOnce for UiFontSizeControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + h_flex() + .gap_2() + .child(Icon::new(IconName::FontSize)) + .child(NumericStepper::new( + value.to_string(), + move |_, cx| { + Self::write(value - px(1.), cx); + }, + move |_, cx| { + Self::write(value + px(1.), cx); + }, + )) + } +} + +#[derive(IntoElement)] +struct UiFontWeightControl; + +impl EditableSettingControl for UiFontWeightControl { + type Value = FontWeight; + type Settings = ThemeSettings; + + fn name(&self) -> SharedString { + "UI Font Weight".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ThemeSettings::get_global(cx); + settings.ui_font.weight + } + + fn apply(settings: &mut ::FileContent, value: Self::Value) { + settings.ui_font_weight = Some(value.0); + } +} + +impl RenderOnce for UiFontWeightControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + h_flex() + .gap_2() + .child(Icon::new(IconName::FontWeight)) + .child(DropdownMenu::new( + "ui-font-weight", + value.0.to_string(), + ContextMenu::build(cx, |mut menu, _cx| { + for weight in FontWeight::ALL { + menu = menu.custom_entry( + move |_cx| Label::new(weight.0.to_string()).into_any_element(), + { + move |cx| { + Self::write(weight, cx); + } + }, + ) + } + + menu + }), + )) + } +} diff --git a/crates/settings_ui/src/theme_settings_ui.rs b/crates/settings_ui/src/theme_settings_ui.rs deleted file mode 100644 index d8cc8cd311..0000000000 --- a/crates/settings_ui/src/theme_settings_ui.rs +++ /dev/null @@ -1,190 +0,0 @@ -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 { - 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 ::FileContent, value: Self::Value); - - /// Writes the given setting value to the settings files. - fn write(value: Self::Value, cx: &AppContext) { - let fs = ::global(cx); - - update_settings_file::(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 { - Some(IconName::FontSize) - } - - fn new(cx: &AppContext) -> Self { - let settings = ThemeSettings::get_global(cx); - - Self(settings.ui_font_size) - } - - fn apply(settings: &mut ::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 { - Some(IconName::FontSize) - } - - fn new(cx: &AppContext) -> Self { - let settings = ThemeSettings::get_global(cx); - - Self(settings.buffer_font_size) - } - - fn apply(settings: &mut ::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 ::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, - ); - }, - ) - } -} diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index d987a99d1c..72fe080388 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -18,6 +18,8 @@ mod popover_menu; mod radio; mod right_click_menu; mod setting; +mod settings_container; +mod settings_group; mod stack; mod tab; mod tab_bar; @@ -33,7 +35,7 @@ pub use checkbox::*; pub use context_menu::*; pub use disclosure::*; pub use divider::*; -use dropdown_menu::*; +pub use dropdown_menu::*; pub use facepile::*; pub use icon::*; pub use indicator::*; @@ -47,6 +49,8 @@ pub use popover_menu::*; pub use radio::*; pub use right_click_menu::*; pub use setting::*; +pub use settings_container::*; +pub use settings_group::*; pub use stack::*; pub use tab::*; pub use tab_bar::*; diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 7480bd139d..0fff168bbf 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -1,52 +1,107 @@ -use crate::prelude::*; +use gpui::{ClickEvent, CursorStyle, MouseButton, View}; -/// !!don't use this yet – it's not functional!! -/// -/// pub crate until this is functional -/// -/// just a placeholder for now for filling out the settings menu stories. -#[derive(Debug, Clone, IntoElement)] -pub(crate) struct DropdownMenu { - pub id: ElementId, - current_item: Option, - // items: Vec, +use crate::{prelude::*, ContextMenu, PopoverMenu}; + +#[derive(IntoElement)] +pub struct DropdownMenu { + id: ElementId, + label: SharedString, + menu: View, full_width: bool, disabled: bool, } impl DropdownMenu { - pub fn new(id: impl Into, _cx: &WindowContext) -> Self { + pub fn new( + id: impl Into, + label: impl Into, + menu: View, + ) -> Self { Self { id: id.into(), - current_item: None, - // items: Vec::new(), + label: label.into(), + menu, full_width: false, disabled: false, } } - pub fn current_item(mut self, current_item: Option) -> Self { - self.current_item = current_item; - self - } - pub fn full_width(mut self, full_width: bool) -> Self { self.full_width = full_width; self } +} - pub fn disabled(mut self, disabled: bool) -> Self { +impl Disableable for DropdownMenu { + fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } } impl RenderOnce for DropdownMenu { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + PopoverMenu::new(self.id) + .menu(move |_cx| Some(self.menu.clone())) + .trigger(DropdownMenuTrigger::new(self.label)) + } +} + +#[derive(IntoElement)] +struct DropdownMenuTrigger { + label: SharedString, + full_width: bool, + selected: bool, + disabled: bool, + cursor_style: CursorStyle, + on_click: Option>, +} + +impl DropdownMenuTrigger { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + full_width: false, + selected: false, + disabled: false, + cursor_style: CursorStyle::default(), + on_click: None, + } + } +} + +impl Disableable for DropdownMenuTrigger { + fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl Selectable for DropdownMenuTrigger { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + +impl Clickable for DropdownMenuTrigger { + fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { + self.on_click = Some(Box::new(handler)); + self + } + + fn cursor_style(mut self, cursor_style: CursorStyle) -> Self { + self.cursor_style = cursor_style; + self + } +} + +impl RenderOnce for DropdownMenuTrigger { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let disabled = self.disabled; h_flex() - .id(self.id) + .id("dropdown-menu-trigger") .justify_between() .rounded_md() .bg(cx.theme().colors().editor_background) @@ -55,23 +110,25 @@ impl RenderOnce for DropdownMenu { .py_0p5() .gap_2() .min_w_20() - .when_else( - self.full_width, - |full_width| full_width.w_full(), - |auto_width| auto_width.flex_none().w_auto(), - ) - .when_else( - disabled, - |disabled| disabled.cursor_not_allowed(), - |enabled| enabled.cursor_pointer(), - ) - .child( - Label::new(self.current_item.unwrap_or("".into())).color(if disabled { - Color::Disabled + .map(|el| { + if self.full_width { + el.w_full() } else { - Color::Default - }), - ) + el.flex_none().w_auto() + } + }) + .map(|el| { + if disabled { + el.cursor_not_allowed() + } else { + el.cursor_pointer() + } + }) + .child(Label::new(self.label).color(if disabled { + Color::Disabled + } else { + Color::Default + })) .child( Icon::new(IconName::ChevronUpDown) .size(IconSize::XSmall) @@ -81,5 +138,12 @@ impl RenderOnce for DropdownMenu { Color::Muted }), ) + .when_some(self.on_click.filter(|_| !disabled), |el, on_click| { + el.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default()) + .on_click(move |event, cx| { + cx.stop_propagation(); + (on_click)(event, cx) + }) + }) } } diff --git a/crates/ui/src/components/setting.rs b/crates/ui/src/components/setting.rs index df4b4bc1be..a394dac3de 100644 --- a/crates/ui/src/components/setting.rs +++ b/crates/ui/src/components/setting.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, Checkbox, ListHeader}; +use crate::{prelude::*, Checkbox, ContextMenu, ListHeader}; use super::DropdownMenu; @@ -42,12 +42,12 @@ pub enum SettingType { } #[derive(Debug, Clone, IntoElement)] -pub struct SettingsGroup { +pub struct LegacySettingsGroup { pub name: String, settings: Vec, } -impl SettingsGroup { +impl LegacySettingsGroup { pub fn new(name: impl Into) -> Self { Self { name: name.into(), @@ -61,7 +61,7 @@ impl SettingsGroup { } } -impl RenderOnce for SettingsGroup { +impl RenderOnce for LegacySettingsGroup { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { let empty_message = format!("No settings available for {}", self.name); @@ -207,7 +207,7 @@ impl RenderOnce for SettingsItem { let id: ElementId = self.id.clone().into(); // When the setting is disabled or toggled off, we don't want any secondary elements to be interactable - let secondary_element_disabled = self.disabled || self.toggled == Some(false); + let _secondary_element_disabled = self.disabled || self.toggled == Some(false); let full_width = match self.layout { SettingLayout::FullLine | SettingLayout::FullLineJustified => true, @@ -239,10 +239,12 @@ impl RenderOnce for SettingsItem { SettingType::Toggle(_) => None, SettingType::ToggleAnd(secondary_setting_type) => match secondary_setting_type { SecondarySettingType::Dropdown => Some( - DropdownMenu::new(id.clone(), &cx) - .current_item(current_string) - .disabled(secondary_element_disabled) - .into_any_element(), + DropdownMenu::new( + id.clone(), + current_string.unwrap_or_default(), + ContextMenu::build(cx, |menu, _cx| menu), + ) + .into_any_element(), ), }, SettingType::Input(input_type) => match input_type { @@ -250,10 +252,13 @@ impl RenderOnce for SettingsItem { InputType::Number => Some(div().child("number").into_any_element()), }, SettingType::Dropdown => Some( - DropdownMenu::new(id.clone(), &cx) - .current_item(current_string) - .full_width(true) - .into_any_element(), + DropdownMenu::new( + id.clone(), + current_string.unwrap_or_default(), + ContextMenu::build(cx, |menu, _cx| menu), + ) + .full_width(true) + .into_any_element(), ), SettingType::Range => Some(div().child("range").into_any_element()), SettingType::Unsupported => None, @@ -308,12 +313,12 @@ impl RenderOnce for SettingsItem { } } -pub struct SettingsMenu { +pub struct LegacySettingsMenu { name: SharedString, - groups: Vec, + groups: Vec, } -impl SettingsMenu { +impl LegacySettingsMenu { pub fn new(name: impl Into) -> Self { Self { name: name.into(), @@ -321,13 +326,13 @@ impl SettingsMenu { } } - pub fn add_group(mut self, group: SettingsGroup) -> Self { + pub fn add_group(mut self, group: LegacySettingsGroup) -> Self { self.groups.push(group); self } } -impl Render for SettingsMenu { +impl Render for LegacySettingsMenu { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let is_empty = self.groups.is_empty(); v_flex() diff --git a/crates/ui/src/components/settings_container.rs b/crates/ui/src/components/settings_container.rs new file mode 100644 index 0000000000..a398c9070b --- /dev/null +++ b/crates/ui/src/components/settings_container.rs @@ -0,0 +1,33 @@ +use gpui::AnyElement; +use smallvec::SmallVec; + +use crate::prelude::*; + +#[derive(IntoElement)] +pub struct SettingsContainer { + children: SmallVec<[AnyElement; 2]>, +} + +impl SettingsContainer { + pub fn new() -> Self { + Self { + children: SmallVec::new(), + } + } +} + +impl ParentElement for SettingsContainer { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl RenderOnce for SettingsContainer { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + v_flex() + .elevation_2(cx) + .px_2() + .gap_1() + .children(self.children) + } +} diff --git a/crates/ui/src/components/settings_group.rs b/crates/ui/src/components/settings_group.rs new file mode 100644 index 0000000000..6093964057 --- /dev/null +++ b/crates/ui/src/components/settings_group.rs @@ -0,0 +1,36 @@ +use gpui::AnyElement; +use smallvec::SmallVec; + +use crate::{prelude::*, ListHeader}; + +/// A group of settings. +#[derive(IntoElement)] +pub struct SettingsGroup { + header: SharedString, + children: SmallVec<[AnyElement; 2]>, +} + +impl SettingsGroup { + pub fn new(header: impl Into) -> Self { + Self { + header: header.into(), + children: SmallVec::new(), + } + } +} + +impl ParentElement for SettingsGroup { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl RenderOnce for SettingsGroup { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + v_flex() + .p_1() + .gap_2() + .child(ListHeader::new(self.header)) + .children(self.children) + } +} diff --git a/crates/ui/src/components/stories/setting.rs b/crates/ui/src/components/stories/setting.rs index 0a3f8a2d1d..bfdfb6a591 100644 --- a/crates/ui/src/components/stories/setting.rs +++ b/crates/ui/src/components/stories/setting.rs @@ -3,12 +3,12 @@ use gpui::View; use crate::prelude::*; use crate::{ - SecondarySettingType, SettingLayout, SettingType, SettingsGroup, SettingsItem, SettingsMenu, - ToggleType, + LegacySettingsGroup, LegacySettingsMenu, SecondarySettingType, SettingLayout, SettingType, + SettingsItem, ToggleType, }; pub struct SettingStory { - menus: Vec<(SharedString, View)>, + menus: Vec<(SharedString, View)>, } impl SettingStory { @@ -27,7 +27,7 @@ impl SettingStory { impl SettingStory { pub fn empty_menu(&mut self, cx: &mut ViewContext) { - let menu = cx.new_view(|_cx| SettingsMenu::new("Empty Menu")); + let menu = cx.new_view(|_cx| LegacySettingsMenu::new("Empty Menu")); self.menus.push(("Empty Menu".into(), menu)); } @@ -55,18 +55,18 @@ impl SettingStory { ) .layout(SettingLayout::FullLineJustified); - let group = SettingsGroup::new("Appearance") + let group = LegacySettingsGroup::new("Appearance") .add_setting(theme_setting) .add_setting(appearance_setting) .add_setting(high_contrast_setting); - let menu = cx.new_view(|_cx| SettingsMenu::new("Appearance").add_group(group)); + let menu = cx.new_view(|_cx| LegacySettingsMenu::new("Appearance").add_group(group)); self.menus.push(("Single Group".into(), menu)); } pub fn editor_example(&mut self, cx: &mut ViewContext) { - let font_group = SettingsGroup::new("Font") + let font_group = LegacySettingsGroup::new("Font") .add_setting( SettingsItem::new( "font-family", @@ -117,7 +117,7 @@ impl SettingStory { .toggled(true), ); - let editor_group = SettingsGroup::new("Editor") + let editor_group = LegacySettingsGroup::new("Editor") .add_setting( SettingsItem::new( "show-indent-guides", @@ -137,7 +137,7 @@ impl SettingStory { .toggled(false), ); - let gutter_group = SettingsGroup::new("Gutter") + let gutter_group = LegacySettingsGroup::new("Gutter") .add_setting( SettingsItem::new( "enable-git-hunks", @@ -158,7 +158,7 @@ impl SettingStory { .layout(SettingLayout::FullLineJustified), ); - let scrollbar_group = SettingsGroup::new("Scrollbar") + let scrollbar_group = LegacySettingsGroup::new("Scrollbar") .add_setting( SettingsItem::new( "scrollbar-visibility", @@ -198,7 +198,7 @@ impl SettingStory { ); let menu = cx.new_view(|_cx| { - SettingsMenu::new("Editor") + LegacySettingsMenu::new("Editor") .add_group(font_group) .add_group(editor_group) .add_group(gutter_group)