settings ui: Add basic support for drop down menus (#38019)

Anthony Eid created

Enums with six or less fields can still use toggle groups by adding a
definition.

I also renamed the `OpenSettingsEditor` action to `OpenSettingsUi`

Release Notes:

- N/A

Change summary

crates/settings_ui/src/settings_ui.rs               | 52 ++++++++++++-
crates/settings_ui_macros/src/settings_ui_macros.rs | 55 +++++++++++++-
2 files changed, 94 insertions(+), 13 deletions(-)

Detailed changes

crates/settings_ui/src/settings_ui.rs 🔗

@@ -16,7 +16,10 @@ use settings::{
     SettingsValue,
 };
 use smallvec::SmallVec;
-use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*};
+use ui::{
+    ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple,
+    prelude::*,
+};
 use workspace::{
     Workspace,
     item::{Item, ItemEvent},
@@ -33,14 +36,14 @@ impl FeatureFlag for SettingsUiFeatureFlag {
 actions!(
     zed,
     [
-        /// Opens the settings editor.
-        OpenSettingsEditor
+        /// Opens settings UI.
+        OpenSettingsUi
     ]
 );
 
 pub fn open_settings_editor(
     workspace: &mut Workspace,
-    _: &OpenSettingsEditor,
+    _: &OpenSettingsUi,
     window: &mut Window,
     cx: &mut Context<Workspace>,
 ) {
@@ -62,7 +65,7 @@ pub fn open_settings_editor(
 pub fn init(cx: &mut App) {
     cx.observe_new(|workspace: &mut Workspace, _, _| {
         workspace.register_action_renderer(|div, _, _, cx| {
-            let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsEditor>()];
+            let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsUi>()];
             let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
             command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
                 if has_flag {
@@ -635,8 +638,8 @@ fn render_item_single(
             variants: values,
             labels: titles,
         } => render_toggle_button_group(settings_value, values, titles, window, cx),
-        SettingsUiItemSingle::DropDown { .. } => {
-            unimplemented!("This")
+        SettingsUiItemSingle::DropDown { variants, labels } => {
+            render_dropdown(settings_value, variants, labels, window, cx)
         }
         SettingsUiItemSingle::TextField => render_text_field(settings_value, window, cx),
     }
@@ -896,6 +899,41 @@ fn render_toggle_button_group(
     });
 }
 
+fn render_dropdown(
+    value: SettingsValue<serde_json::Value>,
+    variants: &'static [&'static str],
+    labels: &'static [&'static str],
+    window: &mut Window,
+    cx: &mut App,
+) -> AnyElement {
+    let value = downcast_any_item::<String>(value);
+    let id = element_id_from_path(&value.path);
+
+    let menu = window.use_state(cx, |window, cx| {
+        let path = value.path.clone();
+        let handler = Rc::new(move |variant: &'static str, cx: &mut App| {
+            SettingsValue::write_value(&path, serde_json::Value::String(variant.to_string()), cx);
+        });
+
+        ContextMenu::build(window, cx, |mut menu, _, _| {
+            for (label, variant) in labels.iter().zip(variants) {
+                menu = menu.entry(*label, None, {
+                    let handler = handler.clone();
+                    move |_, cx| {
+                        handler(variant, cx);
+                    }
+                });
+            }
+
+            menu
+        })
+    });
+
+    DropdownMenu::new(id, value.read(), menu.read(cx).clone())
+        .style(ui::DropdownStyle::Outlined)
+        .into_any_element()
+}
+
 fn render_toggle_button_group_inner(
     title: SharedString,
     labels: &'static [&'static str],

crates/settings_ui_macros/src/settings_ui_macros.rs 🔗

@@ -50,6 +50,10 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt
                     meta.input.parse::<Token![=]>()?;
                     let lit: LitStr = meta.input.parse()?;
                     path_name = Some(lit.value());
+                } else if meta.path.is_ident("render") {
+                    // Just consume the tokens even if we don't use them here
+                    meta.input.parse::<Token![=]>()?;
+                    let _lit: LitStr = meta.input.parse()?;
                 }
                 Ok(())
             })
@@ -170,6 +174,7 @@ fn generate_ui_item_body(group_name: Option<&String>, input: &syn::DeriveInput)
         }
         (None, Data::Enum(data_enum)) => {
             let serde_attrs = parse_serde_attributes(&input.attrs);
+            let render_as = parse_render_as(&input.attrs);
             let length = data_enum.variants.len();
 
             let mut variants = Vec::with_capacity(length);
@@ -187,13 +192,19 @@ fn generate_ui_item_body(group_name: Option<&String>, input: &syn::DeriveInput)
 
             let is_not_union = data_enum.variants.iter().all(|v| v.fields.is_empty());
             if is_not_union {
-                return if length > 6 {
-                    quote! {
-                        settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
+                return match render_as {
+                    RenderAs::ToggleGroup if length > 6 => {
+                        panic!("Can't set toggle group with more than six entries");
                     }
-                } else {
-                    quote! {
-                        settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
+                    RenderAs::ToggleGroup => {
+                        quote! {
+                            settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
+                        }
+                    }
+                    RenderAs::Default => {
+                        quote! {
+                            settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
+                        }
                     }
                 };
             }
@@ -369,6 +380,38 @@ impl SerdeOptions {
     }
 }
 
+enum RenderAs {
+    ToggleGroup,
+    Default,
+}
+
+fn parse_render_as(attrs: &[syn::Attribute]) -> RenderAs {
+    let mut render_as = RenderAs::Default;
+
+    for attr in attrs {
+        if !attr.path().is_ident("settings_ui") {
+            continue;
+        }
+
+        attr.parse_nested_meta(|meta| {
+            if meta.path.is_ident("render") {
+                meta.input.parse::<Token![=]>()?;
+                let lit = meta.input.parse::<LitStr>()?.value();
+
+                if lit == "toggle_group" {
+                    render_as = RenderAs::ToggleGroup;
+                } else {
+                    return Err(meta.error(format!("invalid `render` attribute: {}", lit)));
+                }
+            }
+            Ok(())
+        })
+        .unwrap();
+    }
+
+    render_as
+}
+
 fn parse_serde_attributes(attrs: &[syn::Attribute]) -> SerdeOptions {
     let mut options = SerdeOptions {
         rename_all: SerdeRenameAll::None,