From ff2eebf5222af47a9acf1abb7193731cd102824d Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:16:16 -0400 Subject: [PATCH] settings ui: Add basic support for drop down menus (#38019) 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 --- crates/settings_ui/src/settings_ui.rs | 52 +++++++++++++++--- .../src/settings_ui_macros.rs | 55 +++++++++++++++++-- 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 345a99752706272ce954b388c710a1d2a83ed02c..f448508971dbeb58126259be10111833fe4bb252 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/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, ) { @@ -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::()]; + let settings_ui_actions = [std::any::TypeId::of::()]; let has_flag = cx.has_flag::(); 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, + variants: &'static [&'static str], + labels: &'static [&'static str], + window: &mut Window, + cx: &mut App, +) -> AnyElement { + let value = downcast_any_item::(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], diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs index fcff230ef8e07b551ea769c5ab94643ef715954d..7dea9fc879e9f34761e3dbfcf9e703ebf3a72d12 100644 --- a/crates/settings_ui_macros/src/settings_ui_macros.rs +++ b/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::()?; 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::()?; + 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::()?; + let lit = meta.input.parse::()?.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,