settings_ui: Add pickers for theme and icon themes (#40829)

Danilo Leal created

In the process of adding pickers for the theme and icon themes fields in
the settings UI, I felt like there was an improvement opportunity in
regards to where some of these components are stored. The `ui_input`
crate originally was meant only for the text field-like component, which
couldn't be in the regular `ui` crate due to the dependency with
`editor`. Given we had also added the number field thereβ€”which is
similar in also having the same dependencyβ€”it made sense to think of
this crate more like a home for form-like components rather than for
only one component.

However, we were also storing some settings UI-specific stuff in that
crate, which didn't feel right. So I ended up creating a new directory
within the `settings_ui` for components and moved all the pickers and
the custom input field there. I think this makes it for a cleaner
structure.

Release Notes:

- settings_ui: Added the ability to search for theme and icon themes in
their respective fields.

Change summary

Cargo.lock                                                        |   4 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs |  27 
crates/keymap_editor/src/keymap_editor.rs                         |   8 
crates/language_models/src/provider/anthropic.rs                  |   6 
crates/language_models/src/provider/bedrock.rs                    |  21 
crates/language_models/src/provider/deepseek.rs                   |   6 
crates/language_models/src/provider/google.rs                     |   6 
crates/language_models/src/provider/mistral.rs                    |  10 
crates/language_models/src/provider/ollama.rs                     |  11 
crates/language_models/src/provider/open_ai.rs                    |   6 
crates/language_models/src/provider/open_ai_compatible.rs         |   6 
crates/language_models/src/provider/open_router.rs                |   6 
crates/language_models/src/provider/vercel.rs                     |   6 
crates/language_models/src/provider/x_ai.rs                       |   6 
crates/onboarding/Cargo.toml                                      |   1 
crates/onboarding/src/onboarding.rs                               |   1 
crates/settings_ui/Cargo.toml                                     |   1 
crates/settings_ui/src/components.rs                              | 105 
crates/settings_ui/src/components/font_picker.rs                  |   0 
crates/settings_ui/src/components/icon_theme_picker.rs            | 189 
crates/settings_ui/src/components/input_field.rs                  |  96 
crates/settings_ui/src/components/theme_picker.rs                 | 179 
crates/settings_ui/src/settings_ui.rs                             | 241 
crates/ui_input/Cargo.toml                                        |   2 
crates/ui_input/src/input_field.rs                                | 222 
crates/ui_input/src/ui_input.rs                                   | 230 
crates/zed/src/zed/component_preview.rs                           |   7 
crates/zeta2_tools/src/zeta2_tools.rs                             |  18 
28 files changed, 882 insertions(+), 539 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -10788,7 +10788,6 @@ dependencies = [
  "telemetry",
  "theme",
  "ui",
- "ui_input",
  "util",
  "vim_mode_setting",
  "workspace",
@@ -15271,6 +15270,7 @@ dependencies = [
  "menu",
  "node_runtime",
  "paths",
+ "picker",
  "pretty_assertions",
  "project",
  "schemars 1.0.4",
@@ -18191,10 +18191,8 @@ version = "0.1.0"
 dependencies = [
  "component",
  "editor",
- "fuzzy",
  "gpui",
  "menu",
- "picker",
  "settings",
  "theme",
  "ui",

crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs πŸ”—

@@ -10,7 +10,7 @@ use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
 use ui::{
     Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
 };
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use workspace::{ModalView, Workspace};
 
 #[derive(Clone, Copy)]
@@ -33,9 +33,9 @@ impl LlmCompatibleProvider {
 }
 
 struct AddLlmProviderInput {
-    provider_name: Entity<SingleLineInput>,
-    api_url: Entity<SingleLineInput>,
-    api_key: Entity<SingleLineInput>,
+    provider_name: Entity<InputField>,
+    api_url: Entity<InputField>,
+    api_key: Entity<InputField>,
     models: Vec<ModelInput>,
 }
 
@@ -76,10 +76,10 @@ struct ModelCapabilityToggles {
 }
 
 struct ModelInput {
-    name: Entity<SingleLineInput>,
-    max_completion_tokens: Entity<SingleLineInput>,
-    max_output_tokens: Entity<SingleLineInput>,
-    max_tokens: Entity<SingleLineInput>,
+    name: Entity<InputField>,
+    max_completion_tokens: Entity<InputField>,
+    max_output_tokens: Entity<InputField>,
+    max_tokens: Entity<InputField>,
     capabilities: ModelCapabilityToggles,
 }
 
@@ -171,9 +171,9 @@ fn single_line_input(
     text: Option<&str>,
     window: &mut Window,
     cx: &mut App,
-) -> Entity<SingleLineInput> {
+) -> Entity<InputField> {
     cx.new(|cx| {
-        let input = SingleLineInput::new(window, cx, placeholder).label(label);
+        let input = InputField::new(window, cx, placeholder).label(label);
         if let Some(text) = text {
             input
                 .editor()
@@ -757,12 +757,7 @@ mod tests {
         models: Vec<(&str, &str, &str, &str)>,
         cx: &mut VisualTestContext,
     ) -> Option<SharedString> {
-        fn set_text(
-            input: &Entity<SingleLineInput>,
-            text: &str,
-            window: &mut Window,
-            cx: &mut App,
-        ) {
+        fn set_text(input: &Entity<InputField>, text: &str, window: &mut Window, cx: &mut App) {
             input.update(cx, |input, cx| {
                 input.editor().update(cx, |editor, cx| {
                     editor.set_text(text, window, cx);

crates/keymap_editor/src/keymap_editor.rs πŸ”—

@@ -32,7 +32,7 @@ use ui::{
     SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState,
     TableResizeBehavior, Tooltip, Window, prelude::*,
 };
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::ResultExt;
 use workspace::{
     Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
@@ -2114,7 +2114,7 @@ struct KeybindingEditorModal {
     editing_keybind: ProcessedBinding,
     editing_keybind_idx: usize,
     keybind_editor: Entity<KeystrokeInput>,
-    context_editor: Entity<SingleLineInput>,
+    context_editor: Entity<InputField>,
     action_arguments_editor: Option<Entity<ActionArgumentsEditor>>,
     fs: Arc<dyn Fs>,
     error: Option<InputError>,
@@ -2148,8 +2148,8 @@ impl KeybindingEditorModal {
         let keybind_editor = cx
             .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
 
-        let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
-            let input = SingleLineInput::new(window, cx, "Keybinding Context")
+        let context_editor: Entity<InputField> = cx.new(|cx| {
+            let input = InputField::new(window, cx, "Keybinding Context")
                 .label("Edit Context")
                 .label_size(LabelSize::Default);
 

crates/language_models/src/provider/anthropic.rs πŸ”—

@@ -21,7 +21,7 @@ use std::str::FromStr;
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -823,7 +823,7 @@ fn convert_usage(usage: &Usage) -> language_model::TokenUsage {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
     target_agent: ConfigurationViewTargetAgent,
@@ -862,7 +862,7 @@ impl ConfigurationView {
         }));
 
         Self {
-            api_key_editor: cx.new(|cx| SingleLineInput::new(window, cx, Self::PLACEHOLDER_TEXT)),
+            api_key_editor: cx.new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_TEXT)),
             state,
             load_credentials_task,
             target_agent,

crates/language_models/src/provider/bedrock.rs πŸ”—

@@ -42,7 +42,7 @@ use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore}
 use smol::lock::OnceCell;
 use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::ResultExt;
 
 use crate::AllLanguageModelSettings;
@@ -1006,10 +1006,10 @@ pub fn map_to_language_model_completion_events(
 }
 
 struct ConfigurationView {
-    access_key_id_editor: Entity<SingleLineInput>,
-    secret_access_key_editor: Entity<SingleLineInput>,
-    session_token_editor: Entity<SingleLineInput>,
-    region_editor: Entity<SingleLineInput>,
+    access_key_id_editor: Entity<InputField>,
+    secret_access_key_editor: Entity<InputField>,
+    session_token_editor: Entity<InputField>,
+    region_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -1047,20 +1047,19 @@ impl ConfigurationView {
 
         Self {
             access_key_id_editor: cx.new(|cx| {
-                SingleLineInput::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT)
+                InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT)
                     .label("Access Key ID")
             }),
             secret_access_key_editor: cx.new(|cx| {
-                SingleLineInput::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT)
+                InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT)
                     .label("Secret Access Key")
             }),
             session_token_editor: cx.new(|cx| {
-                SingleLineInput::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT)
+                InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT)
                     .label("Session Token (Optional)")
             }),
-            region_editor: cx.new(|cx| {
-                SingleLineInput::new(window, cx, Self::PLACEHOLDER_REGION).label("Region")
-            }),
+            region_editor: cx
+                .new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_REGION).label("Region")),
             state,
             load_credentials_task,
         }

crates/language_models/src/provider/deepseek.rs πŸ”—

@@ -20,7 +20,7 @@ use std::str::FromStr;
 use std::sync::{Arc, LazyLock};
 
 use ui::{Icon, IconName, List, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -525,7 +525,7 @@ impl DeepSeekEventMapper {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -533,7 +533,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "sk-00000000000000000000000000000000"));
+            cx.new(|cx| InputField::new(window, cx, "sk-00000000000000000000000000000000"));
 
         cx.observe(&state, |_, _, cx| {
             cx.notify();

crates/language_models/src/provider/google.rs πŸ”—

@@ -29,7 +29,7 @@ use std::sync::{
 };
 use strum::IntoEnumIterator;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::EnvVar;
 
@@ -751,7 +751,7 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     target_agent: language_model::ConfigurationViewTargetAgent,
     load_credentials_task: Option<Task<()>>,
@@ -788,7 +788,7 @@ impl ConfigurationView {
         }));
 
         Self {
-            api_key_editor: cx.new(|cx| SingleLineInput::new(window, cx, "AIzaSy...")),
+            api_key_editor: cx.new(|cx| InputField::new(window, cx, "AIzaSy...")),
             target_agent,
             state,
             load_credentials_task,

crates/language_models/src/provider/mistral.rs πŸ”—

@@ -20,7 +20,7 @@ use std::str::FromStr;
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -744,8 +744,8 @@ struct RawToolCall {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
-    codestral_api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
+    codestral_api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -753,9 +753,9 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
+            cx.new(|cx| InputField::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
         let codestral_api_key_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
+            cx.new(|cx| InputField::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
 
         cx.observe(&state, |_, _, cx| {
             cx.notify();

crates/language_models/src/provider/ollama.rs πŸ”—

@@ -23,7 +23,7 @@ use std::sync::LazyLock;
 use std::sync::atomic::{AtomicU64, Ordering};
 use std::{collections::HashMap, sync::Arc};
 use ui::{ButtonLike, ElevationIndex, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use zed_env_vars::{EnvVar, env_var};
 
 use crate::AllLanguageModelSettings;
@@ -623,18 +623,17 @@ fn map_to_language_model_completion_events(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
-    api_url_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
+    api_url_editor: Entity<InputField>,
     state: Entity<State>,
 }
 
 impl ConfigurationView {
     pub fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
-        let api_key_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "63e02e...").label("API key"));
+        let api_key_editor = cx.new(|cx| InputField::new(window, cx, "63e02e...").label("API key"));
 
         let api_url_editor = cx.new(|cx| {
-            let input = SingleLineInput::new(window, cx, OLLAMA_API_URL).label("API URL");
+            let input = InputField::new(window, cx, OLLAMA_API_URL).label("API URL");
             input.set_text(OllamaLanguageModelProvider::api_url(cx), window, cx);
             input
         });

crates/language_models/src/provider/open_ai.rs πŸ”—

@@ -21,7 +21,7 @@ use std::str::FromStr as _;
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -675,7 +675,7 @@ pub fn count_open_ai_tokens(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -683,7 +683,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "sk-000000000000000000000000000000000000000000000000",

crates/language_models/src/provider/open_ai_compatible.rs πŸ”—

@@ -14,7 +14,7 @@ use open_ai::{ResponseStreamEvent, stream_completion};
 use settings::{Settings, SettingsStore};
 use std::sync::Arc;
 use ui::{ElevationIndex, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::EnvVar;
 
@@ -340,7 +340,7 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -348,7 +348,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "000000000000000000000000000000000000000000000000000",

crates/language_models/src/provider/open_router.rs πŸ”—

@@ -18,7 +18,7 @@ use std::pin::Pin;
 use std::str::FromStr as _;
 use std::sync::{Arc, LazyLock};
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -692,7 +692,7 @@ pub fn count_open_router_tokens(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -700,7 +700,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "sk_or_000000000000000000000000000000000000000000000000",

crates/language_models/src/provider/vercel.rs πŸ”—

@@ -15,7 +15,7 @@ use settings::{Settings, SettingsStore};
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use vercel::{Model, VERCEL_API_URL};
 use zed_env_vars::{EnvVar, env_var};
@@ -362,7 +362,7 @@ pub fn count_vercel_tokens(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -370,7 +370,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "v1:0000000000000000000000000000000000000000000000000",

crates/language_models/src/provider/x_ai.rs πŸ”—

@@ -15,7 +15,7 @@ use settings::{Settings, SettingsStore};
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use x_ai::{Model, XAI_API_URL};
 use zed_env_vars::{EnvVar, env_var};
@@ -359,7 +359,7 @@ pub fn count_xai_tokens(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -367,7 +367,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "xai-0000000000000000000000000000000000000000000000000",

crates/onboarding/Cargo.toml πŸ”—

@@ -34,7 +34,6 @@ settings.workspace = true
 telemetry.workspace = true
 theme.workspace = true
 ui.workspace = true
-ui_input.workspace = true
 util.workspace = true
 vim_mode_setting.workspace = true
 workspace.workspace = true

crates/onboarding/src/onboarding.rs πŸ”—

@@ -17,7 +17,6 @@ use ui::{
     Divider, KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName,
     WithScrollbar as _, prelude::*, rems_from_px,
 };
-pub use ui_input::font_picker;
 use workspace::{
     AppState, Workspace, WorkspaceId,
     dock::DockPosition,

crates/settings_ui/Cargo.toml πŸ”—

@@ -26,6 +26,7 @@ fuzzy.workspace = true
 gpui.workspace = true
 menu.workspace = true
 paths.workspace = true
+picker.workspace = true
 project.workspace = true
 schemars.workspace = true
 search.workspace = true

crates/settings_ui/src/components.rs πŸ”—

@@ -1,96 +1,9 @@
-use editor::Editor;
-use gpui::{Focusable, div};
-use ui::{
-    ActiveTheme as _, App, FluentBuilder as _, InteractiveElement as _, IntoElement,
-    ParentElement as _, RenderOnce, Styled as _, Window,
-};
-
-#[derive(IntoElement)]
-pub struct SettingsEditor {
-    initial_text: Option<String>,
-    placeholder: Option<&'static str>,
-    confirm: Option<Box<dyn Fn(Option<String>, &mut App)>>,
-    tab_index: Option<isize>,
-}
-
-impl SettingsEditor {
-    pub fn new() -> Self {
-        Self {
-            initial_text: None,
-            placeholder: None,
-            confirm: None,
-            tab_index: None,
-        }
-    }
-
-    pub fn with_initial_text(mut self, initial_text: String) -> Self {
-        self.initial_text = Some(initial_text);
-        self
-    }
-
-    pub fn with_placeholder(mut self, placeholder: &'static str) -> Self {
-        self.placeholder = Some(placeholder);
-        self
-    }
-
-    pub fn on_confirm(mut self, confirm: impl Fn(Option<String>, &mut App) + 'static) -> Self {
-        self.confirm = Some(Box::new(confirm));
-        self
-    }
-
-    pub(crate) fn tab_index(mut self, arg: isize) -> Self {
-        self.tab_index = Some(arg);
-        self
-    }
-}
-
-impl RenderOnce for SettingsEditor {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
-        let editor = window.use_state(cx, {
-            move |window, cx| {
-                let mut editor = Editor::single_line(window, cx);
-                if let Some(text) = self.initial_text {
-                    editor.set_text(text, window, cx);
-                }
-
-                if let Some(placeholder) = self.placeholder {
-                    editor.set_placeholder_text(placeholder, window, cx);
-                }
-                // todo(settings_ui): We should have an observe global use for settings store
-                // so whenever a settings file is updated, the settings ui updates too
-                editor
-            }
-        });
-
-        let weak_editor = editor.downgrade();
-
-        let theme_colors = cx.theme().colors();
-
-        div()
-            .py_1()
-            .px_2()
-            .min_w_64()
-            .rounded_md()
-            .border_1()
-            .border_color(theme_colors.border)
-            .bg(theme_colors.editor_background)
-            .when_some(self.tab_index, |this, tab_index| {
-                let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true);
-                this.track_focus(&focus_handle)
-                    .focus(|s| s.border_color(theme_colors.border_focused))
-            })
-            .child(editor)
-            .when_some(self.confirm, |this, confirm| {
-                this.on_action::<menu::Confirm>({
-                    move |_, _, cx| {
-                        let Some(editor) = weak_editor.upgrade() else {
-                            return;
-                        };
-                        let new_value = editor.read_with(cx, |editor, cx| editor.text(cx));
-                        let new_value = (!new_value.is_empty()).then_some(new_value);
-                        confirm(new_value, cx);
-                    }
-                })
-            })
-    }
-}
+mod font_picker;
+mod icon_theme_picker;
+mod input_field;
+mod theme_picker;
+
+pub use font_picker::font_picker;
+pub use icon_theme_picker::icon_theme_picker;
+pub use input_field::*;
+pub use theme_picker::theme_picker;

crates/settings_ui/src/components/icon_theme_picker.rs πŸ”—

@@ -0,0 +1,189 @@
+use std::sync::Arc;
+
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window};
+use picker::{Picker, PickerDelegate};
+use theme::ThemeRegistry;
+use ui::{ListItem, ListItemSpacing, prelude::*};
+
+type IconThemePicker = Picker<IconThemePickerDelegate>;
+
+pub struct IconThemePickerDelegate {
+    icon_themes: Vec<SharedString>,
+    filtered_themes: Vec<StringMatch>,
+    selected_index: usize,
+    current_theme: SharedString,
+    on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
+}
+
+impl IconThemePickerDelegate {
+    fn new(
+        current_theme: SharedString,
+        on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
+        cx: &mut Context<IconThemePicker>,
+    ) -> Self {
+        let theme_registry = ThemeRegistry::global(cx);
+
+        let icon_themes: Vec<SharedString> = theme_registry
+            .list_icon_themes()
+            .into_iter()
+            .map(|theme_meta| theme_meta.name)
+            .collect();
+
+        let selected_index = icon_themes
+            .iter()
+            .position(|icon_themes| *icon_themes == current_theme)
+            .unwrap_or(0);
+
+        let filtered_themes = icon_themes
+            .iter()
+            .enumerate()
+            .map(|(index, icon_themes)| StringMatch {
+                candidate_id: index,
+                string: icon_themes.to_string(),
+                positions: Vec::new(),
+                score: 0.0,
+            })
+            .collect();
+
+        Self {
+            icon_themes,
+            filtered_themes,
+            selected_index,
+            current_theme,
+            on_theme_changed: Arc::new(on_theme_changed),
+        }
+    }
+}
+
+impl PickerDelegate for IconThemePickerDelegate {
+    type ListItem = AnyElement;
+
+    fn match_count(&self) -> usize {
+        self.filtered_themes.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<IconThemePicker>) {
+        self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1));
+        cx.notify();
+    }
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+        "Search icon theme…".into()
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        _window: &mut Window,
+        cx: &mut Context<IconThemePicker>,
+    ) -> Task<()> {
+        let icon_themes = self.icon_themes.clone();
+        let current_theme = self.current_theme.clone();
+
+        let matches: Vec<StringMatch> = if query.is_empty() {
+            icon_themes
+                .iter()
+                .enumerate()
+                .map(|(index, icon_theme)| StringMatch {
+                    candidate_id: index,
+                    string: icon_theme.to_string(),
+                    positions: Vec::new(),
+                    score: 0.0,
+                })
+                .collect()
+        } else {
+            let _candidates: Vec<StringMatchCandidate> = icon_themes
+                .iter()
+                .enumerate()
+                .map(|(id, icon_theme)| StringMatchCandidate::new(id, icon_theme.as_ref()))
+                .collect();
+
+            icon_themes
+                .iter()
+                .enumerate()
+                .filter(|(_, icon_theme)| icon_theme.to_lowercase().contains(&query.to_lowercase()))
+                .map(|(index, icon_theme)| StringMatch {
+                    candidate_id: index,
+                    string: icon_theme.to_string(),
+                    positions: Vec::new(),
+                    score: 0.0,
+                })
+                .collect()
+        };
+
+        let selected_index = if query.is_empty() {
+            icon_themes
+                .iter()
+                .position(|icon_theme| *icon_theme == current_theme)
+                .unwrap_or(0)
+        } else {
+            matches
+                .iter()
+                .position(|m| icon_themes[m.candidate_id] == current_theme)
+                .unwrap_or(0)
+        };
+
+        self.filtered_themes = matches;
+        self.selected_index = selected_index;
+        cx.notify();
+
+        Task::ready(())
+    }
+
+    fn confirm(
+        &mut self,
+        _secondary: bool,
+        _window: &mut Window,
+        cx: &mut Context<IconThemePicker>,
+    ) {
+        if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
+            let theme = theme_match.string.clone();
+            (self.on_theme_changed)(theme.into(), cx);
+        }
+    }
+
+    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<IconThemePicker>) {
+        cx.defer_in(window, |picker, window, cx| {
+            picker.set_query("", window, cx);
+        });
+        cx.emit(DismissEvent);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _window: &mut Window,
+        _cx: &mut Context<IconThemePicker>,
+    ) -> Option<Self::ListItem> {
+        let theme_match = self.filtered_themes.get(ix)?;
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .toggle_state(selected)
+                .child(Label::new(theme_match.string.clone()))
+                .into_any_element(),
+        )
+    }
+}
+
+pub fn icon_theme_picker(
+    current_theme: SharedString,
+    on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
+    window: &mut Window,
+    cx: &mut Context<IconThemePicker>,
+) -> IconThemePicker {
+    let delegate = IconThemePickerDelegate::new(current_theme, on_theme_changed, cx);
+
+    Picker::uniform_list(delegate, window, cx)
+        .show_scrollbar(true)
+        .width(rems_from_px(210.))
+        .max_height(Some(rems(18.).into()))
+}

crates/settings_ui/src/components/input_field.rs πŸ”—

@@ -0,0 +1,96 @@
+use editor::Editor;
+use gpui::{Focusable, div};
+use ui::{
+    ActiveTheme as _, App, FluentBuilder as _, InteractiveElement as _, IntoElement,
+    ParentElement as _, RenderOnce, Styled as _, Window,
+};
+
+#[derive(IntoElement)]
+pub struct SettingsInputField {
+    initial_text: Option<String>,
+    placeholder: Option<&'static str>,
+    confirm: Option<Box<dyn Fn(Option<String>, &mut App)>>,
+    tab_index: Option<isize>,
+}
+
+impl SettingsInputField {
+    pub fn new() -> Self {
+        Self {
+            initial_text: None,
+            placeholder: None,
+            confirm: None,
+            tab_index: None,
+        }
+    }
+
+    pub fn with_initial_text(mut self, initial_text: String) -> Self {
+        self.initial_text = Some(initial_text);
+        self
+    }
+
+    pub fn with_placeholder(mut self, placeholder: &'static str) -> Self {
+        self.placeholder = Some(placeholder);
+        self
+    }
+
+    pub fn on_confirm(mut self, confirm: impl Fn(Option<String>, &mut App) + 'static) -> Self {
+        self.confirm = Some(Box::new(confirm));
+        self
+    }
+
+    pub(crate) fn tab_index(mut self, arg: isize) -> Self {
+        self.tab_index = Some(arg);
+        self
+    }
+}
+
+impl RenderOnce for SettingsInputField {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
+        let editor = window.use_state(cx, {
+            move |window, cx| {
+                let mut editor = Editor::single_line(window, cx);
+                if let Some(text) = self.initial_text {
+                    editor.set_text(text, window, cx);
+                }
+
+                if let Some(placeholder) = self.placeholder {
+                    editor.set_placeholder_text(placeholder, window, cx);
+                }
+                // todo(settings_ui): We should have an observe global use for settings store
+                // so whenever a settings file is updated, the settings ui updates too
+                editor
+            }
+        });
+
+        let weak_editor = editor.downgrade();
+
+        let theme_colors = cx.theme().colors();
+
+        div()
+            .py_1()
+            .px_2()
+            .min_w_64()
+            .rounded_md()
+            .border_1()
+            .border_color(theme_colors.border)
+            .bg(theme_colors.editor_background)
+            .when_some(self.tab_index, |this, tab_index| {
+                let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true);
+                this.track_focus(&focus_handle)
+                    .focus(|s| s.border_color(theme_colors.border_focused))
+            })
+            .child(editor)
+            .when_some(self.confirm, |this, confirm| {
+                this.on_action::<menu::Confirm>({
+                    move |_, _, cx| {
+                        let Some(editor) = weak_editor.upgrade() else {
+                            return;
+                        };
+                        let new_value = editor.read_with(cx, |editor, cx| editor.text(cx));
+                        let new_value = (!new_value.is_empty()).then_some(new_value);
+                        confirm(new_value, cx);
+                    }
+                })
+            })
+    }
+}

crates/settings_ui/src/components/theme_picker.rs πŸ”—

@@ -0,0 +1,179 @@
+use std::sync::Arc;
+
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window};
+use picker::{Picker, PickerDelegate};
+use theme::ThemeRegistry;
+use ui::{ListItem, ListItemSpacing, prelude::*};
+
+type ThemePicker = Picker<ThemePickerDelegate>;
+
+pub struct ThemePickerDelegate {
+    themes: Vec<SharedString>,
+    filtered_themes: Vec<StringMatch>,
+    selected_index: usize,
+    current_theme: SharedString,
+    on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
+}
+
+impl ThemePickerDelegate {
+    fn new(
+        current_theme: SharedString,
+        on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
+        cx: &mut Context<ThemePicker>,
+    ) -> Self {
+        let theme_registry = ThemeRegistry::global(cx);
+
+        let themes = theme_registry.list_names();
+        let selected_index = themes
+            .iter()
+            .position(|theme| *theme == current_theme)
+            .unwrap_or(0);
+
+        let filtered_themes = themes
+            .iter()
+            .enumerate()
+            .map(|(index, theme)| StringMatch {
+                candidate_id: index,
+                string: theme.to_string(),
+                positions: Vec::new(),
+                score: 0.0,
+            })
+            .collect();
+
+        Self {
+            themes,
+            filtered_themes,
+            selected_index,
+            current_theme,
+            on_theme_changed: Arc::new(on_theme_changed),
+        }
+    }
+}
+
+impl PickerDelegate for ThemePickerDelegate {
+    type ListItem = AnyElement;
+
+    fn match_count(&self) -> usize {
+        self.filtered_themes.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<ThemePicker>) {
+        self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1));
+        cx.notify();
+    }
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+        "Search theme…".into()
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        _window: &mut Window,
+        cx: &mut Context<ThemePicker>,
+    ) -> Task<()> {
+        let themes = self.themes.clone();
+        let current_theme = self.current_theme.clone();
+
+        let matches: Vec<StringMatch> = if query.is_empty() {
+            themes
+                .iter()
+                .enumerate()
+                .map(|(index, theme)| StringMatch {
+                    candidate_id: index,
+                    string: theme.to_string(),
+                    positions: Vec::new(),
+                    score: 0.0,
+                })
+                .collect()
+        } else {
+            let _candidates: Vec<StringMatchCandidate> = themes
+                .iter()
+                .enumerate()
+                .map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref()))
+                .collect();
+
+            themes
+                .iter()
+                .enumerate()
+                .filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase()))
+                .map(|(index, theme)| StringMatch {
+                    candidate_id: index,
+                    string: theme.to_string(),
+                    positions: Vec::new(),
+                    score: 0.0,
+                })
+                .collect()
+        };
+
+        let selected_index = if query.is_empty() {
+            themes
+                .iter()
+                .position(|theme| *theme == current_theme)
+                .unwrap_or(0)
+        } else {
+            matches
+                .iter()
+                .position(|m| themes[m.candidate_id] == current_theme)
+                .unwrap_or(0)
+        };
+
+        self.filtered_themes = matches;
+        self.selected_index = selected_index;
+        cx.notify();
+
+        Task::ready(())
+    }
+
+    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<ThemePicker>) {
+        if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
+            let theme = theme_match.string.clone();
+            (self.on_theme_changed)(theme.into(), cx);
+        }
+    }
+
+    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<ThemePicker>) {
+        cx.defer_in(window, |picker, window, cx| {
+            picker.set_query("", window, cx);
+        });
+        cx.emit(DismissEvent);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _window: &mut Window,
+        _cx: &mut Context<ThemePicker>,
+    ) -> Option<Self::ListItem> {
+        let theme_match = self.filtered_themes.get(ix)?;
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .toggle_state(selected)
+                .child(Label::new(theme_match.string.clone()))
+                .into_any_element(),
+        )
+    }
+}
+
+pub fn theme_picker(
+    current_theme: SharedString,
+    on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
+    window: &mut Window,
+    cx: &mut Context<ThemePicker>,
+) -> ThemePicker {
+    let delegate = ThemePickerDelegate::new(current_theme, on_theme_changed, cx);
+
+    Picker::uniform_list(delegate, window, cx)
+        .show_scrollbar(true)
+        .width(rems_from_px(210.))
+        .max_height(Some(rems(18.).into()))
+}

crates/settings_ui/src/settings_ui.rs πŸ”—

@@ -36,7 +36,7 @@ use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
 use workspace::{OpenOptions, OpenVisible, Workspace, client_side_decorations};
 use zed_actions::OpenSettings;
 
-use crate::components::SettingsEditor;
+use crate::components::{SettingsInputField, font_picker, icon_theme_picker, theme_picker};
 
 const NAVBAR_CONTAINER_TAB_INDEX: isize = 0;
 const NAVBAR_GROUP_TAB_INDEX: isize = 1;
@@ -2869,7 +2869,7 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
         SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
     let initial_text = initial_text.filter(|s| !s.as_ref().is_empty());
 
-    SettingsEditor::new()
+    SettingsInputField::new()
         .tab_index(0)
         .when_some(initial_text, |editor, text| {
             editor.with_initial_text(text.as_ref().to_string())
@@ -2920,54 +2920,6 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
         .into_any_element()
 }
 
-fn render_font_picker(
-    field: SettingField<settings::FontFamilyName>,
-    file: SettingsUiFile,
-    _metadata: Option<&SettingsFieldMetadata>,
-    window: &mut Window,
-    cx: &mut App,
-) -> AnyElement {
-    let current_value = SettingsStore::global(cx)
-        .get_value_from_file(file.to_settings(), field.pick)
-        .1
-        .cloned()
-        .unwrap_or_else(|| SharedString::default().into());
-
-    let font_picker = cx.new(|cx| {
-        ui_input::font_picker(
-            current_value.clone().into(),
-            move |font_name, cx| {
-                update_settings_file(file.clone(), cx, move |settings, _cx| {
-                    (field.write)(settings, Some(font_name.into()));
-                })
-                .log_err(); // todo(settings_ui) don't log err
-            },
-            window,
-            cx,
-        )
-    });
-
-    PopoverMenu::new("font-picker")
-        .menu(move |_window, _cx| Some(font_picker.clone()))
-        .trigger(
-            Button::new("font-family-button", current_value)
-                .tab_index(0_isize)
-                .style(ButtonStyle::Outlined)
-                .size(ButtonSize::Medium)
-                .icon(IconName::ChevronUpDown)
-                .icon_color(Color::Muted)
-                .icon_size(IconSize::Small)
-                .icon_position(IconPosition::End),
-        )
-        .anchor(gpui::Corner::TopLeft)
-        .offset(gpui::Point {
-            x: px(0.0),
-            y: px(2.0),
-        })
-        .with_handle(ui::PopoverMenuHandle::default())
-        .into_any_element()
-}
-
 fn render_number_field<T: NumberFieldType + Send + Sync>(
     field: SettingField<T>,
     file: SettingsUiFile,
@@ -3056,6 +3008,59 @@ where
     .into_any_element()
 }
 
+fn render_picker_trigger_button(id: SharedString, label: SharedString) -> Button {
+    Button::new(id, label)
+        .tab_index(0_isize)
+        .style(ButtonStyle::Outlined)
+        .size(ButtonSize::Medium)
+        .icon(IconName::ChevronUpDown)
+        .icon_color(Color::Muted)
+        .icon_size(IconSize::Small)
+        .icon_position(IconPosition::End)
+}
+
+fn render_font_picker(
+    field: SettingField<settings::FontFamilyName>,
+    file: SettingsUiFile,
+    _metadata: Option<&SettingsFieldMetadata>,
+    window: &mut Window,
+    cx: &mut App,
+) -> AnyElement {
+    let current_value = SettingsStore::global(cx)
+        .get_value_from_file(file.to_settings(), field.pick)
+        .1
+        .cloned()
+        .unwrap_or_else(|| SharedString::default().into());
+
+    let font_picker = cx.new(|cx| {
+        font_picker(
+            current_value.clone().into(),
+            move |font_name, cx| {
+                update_settings_file(file.clone(), cx, move |settings, _cx| {
+                    (field.write)(settings, Some(font_name.into()));
+                })
+                .log_err(); // todo(settings_ui) don't log err
+            },
+            window,
+            cx,
+        )
+    });
+
+    PopoverMenu::new("font-picker")
+        .menu(move |_window, _cx| Some(font_picker.clone()))
+        .trigger(render_picker_trigger_button(
+            "font_family_picker_trigger".into(),
+            current_value.into(),
+        ))
+        .anchor(gpui::Corner::TopLeft)
+        .offset(gpui::Point {
+            x: px(0.0),
+            y: px(2.0),
+        })
+        .with_handle(ui::PopoverMenuHandle::default())
+        .into_any_element()
+}
+
 fn render_theme_picker(
     field: SettingField<settings::ThemeName>,
     file: SettingsUiFile,
@@ -3069,42 +3074,33 @@ fn render_theme_picker(
         .map(|theme_name| theme_name.0.into())
         .unwrap_or_else(|| cx.theme().name.clone());
 
-    DropdownMenu::new(
-        "font-picker",
-        current_value.clone(),
-        ContextMenu::build(window, cx, move |mut menu, _, cx| {
-            let all_theme_names = theme::ThemeRegistry::global(cx).list_names();
-            for theme_name in all_theme_names {
-                let file = file.clone();
-                let selected = theme_name.as_ref() == current_value.as_ref();
-                menu = menu.toggleable_entry(
-                    theme_name.clone(),
-                    selected,
-                    IconPosition::End,
-                    None,
-                    move |_, cx| {
-                        if selected {
-                            return;
-                        }
-                        let theme_name = theme_name.clone();
-                        update_settings_file(file.clone(), cx, move |settings, _cx| {
-                            (field.write)(settings, Some(settings::ThemeName(theme_name.into())));
-                        })
-                        .log_err(); // todo(settings_ui) don't log err
-                    },
-                );
-            }
-            menu
-        }),
-    )
-    .trigger_size(ButtonSize::Medium)
-    .style(DropdownStyle::Outlined)
-    .offset(gpui::Point {
-        x: px(0.0),
-        y: px(2.0),
-    })
-    .tab_index(0)
-    .into_any_element()
+    let theme_picker = cx.new(|cx| {
+        theme_picker(
+            current_value.clone(),
+            move |theme_name, cx| {
+                update_settings_file(file.clone(), cx, move |settings, _cx| {
+                    (field.write)(settings, Some(settings::ThemeName(theme_name.into())));
+                })
+                .log_err(); // todo(settings_ui) don't log err
+            },
+            window,
+            cx,
+        )
+    });
+
+    PopoverMenu::new("theme-picker")
+        .menu(move |_window, _cx| Some(theme_picker.clone()))
+        .trigger(render_picker_trigger_button(
+            "theme_picker_trigger".into(),
+            current_value,
+        ))
+        .anchor(gpui::Corner::TopLeft)
+        .offset(gpui::Point {
+            x: px(0.0),
+            y: px(2.0),
+        })
+        .with_handle(ui::PopoverMenuHandle::default())
+        .into_any_element()
 }
 
 fn render_icon_theme_picker(
@@ -3117,51 +3113,36 @@ fn render_icon_theme_picker(
     let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
     let current_value = value
         .cloned()
-        .map(|icon_theme_name| icon_theme_name.0.into())
-        .unwrap_or_else(|| theme::default_icon_theme().name.clone());
+        .map(|theme_name| theme_name.0.into())
+        .unwrap_or_else(|| cx.theme().name.clone());
 
-    DropdownMenu::new(
-        "font-picker",
-        current_value.clone(),
-        ContextMenu::build(window, cx, move |mut menu, _, cx| {
-            let all_theme_names = theme::ThemeRegistry::global(cx)
-                .list_icon_themes()
-                .into_iter()
-                .map(|theme| theme.name);
-            for theme_name in all_theme_names {
-                let file = file.clone();
-                let selected = theme_name.as_ref() == current_value.as_ref();
-                menu = menu.toggleable_entry(
-                    theme_name.clone(),
-                    selected,
-                    IconPosition::End,
-                    None,
-                    move |_, cx| {
-                        if selected {
-                            return;
-                        }
-                        let theme_name = theme_name.clone();
-                        update_settings_file(file.clone(), cx, move |settings, _cx| {
-                            (field.write)(
-                                settings,
-                                Some(settings::IconThemeName(theme_name.into())),
-                            );
-                        })
-                        .log_err(); // todo(settings_ui) don't log err
-                    },
-                );
-            }
-            menu
-        }),
-    )
-    .trigger_size(ButtonSize::Medium)
-    .style(DropdownStyle::Outlined)
-    .offset(gpui::Point {
-        x: px(0.0),
-        y: px(2.0),
-    })
-    .tab_index(0)
-    .into_any_element()
+    let icon_theme_picker = cx.new(|cx| {
+        icon_theme_picker(
+            current_value.clone(),
+            move |theme_name, cx| {
+                update_settings_file(file.clone(), cx, move |settings, _cx| {
+                    (field.write)(settings, Some(settings::IconThemeName(theme_name.into())));
+                })
+                .log_err(); // todo(settings_ui) don't log err
+            },
+            window,
+            cx,
+        )
+    });
+
+    PopoverMenu::new("icon-theme-picker")
+        .menu(move |_window, _cx| Some(icon_theme_picker.clone()))
+        .trigger(render_picker_trigger_button(
+            "icon_theme_picker_trigger".into(),
+            current_value,
+        ))
+        .anchor(gpui::Corner::TopLeft)
+        .offset(gpui::Point {
+            x: px(0.0),
+            y: px(2.0),
+        })
+        .with_handle(ui::PopoverMenuHandle::default())
+        .into_any_element()
 }
 
 #[cfg(test)]

crates/ui_input/Cargo.toml πŸ”—

@@ -14,10 +14,8 @@ path = "src/ui_input.rs"
 [dependencies]
 component.workspace = true
 editor.workspace = true
-fuzzy.workspace = true
 gpui.workspace = true
 menu.workspace = true
-picker.workspace = true
 settings.workspace = true
 theme.workspace = true
 ui.workspace = true

crates/ui_input/src/input_field.rs πŸ”—

@@ -0,0 +1,222 @@
+use component::{example_group, single_example};
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle};
+use settings::Settings;
+use std::sync::Arc;
+use theme::ThemeSettings;
+use ui::prelude::*;
+
+pub struct InputFieldStyle {
+    text_color: Hsla,
+    background_color: Hsla,
+    border_color: Hsla,
+}
+
+/// An Input Field component that can be used to create text fields like search inputs, form fields, etc.
+///
+/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc.
+#[derive(RegisterComponent)]
+pub struct InputField {
+    /// An optional label for the text field.
+    ///
+    /// Its position is determined by the [`FieldLabelLayout`].
+    label: Option<SharedString>,
+    /// The size of the label text.
+    label_size: LabelSize,
+    /// The placeholder text for the text field.
+    placeholder: SharedString,
+    /// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API.
+    ///
+    /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases.
+    pub editor: Entity<Editor>,
+    /// An optional icon that is displayed at the start of the text field.
+    ///
+    /// For example, a magnifying glass icon in a search field.
+    start_icon: Option<IconName>,
+    /// Whether the text field is disabled.
+    disabled: bool,
+    /// The minimum width of for the input
+    min_width: Length,
+}
+
+impl Focusable for InputField {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
+impl InputField {
+    pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into<SharedString>) -> Self {
+        let placeholder_text = placeholder.into();
+
+        let editor = cx.new(|cx| {
+            let mut input = Editor::single_line(window, cx);
+            input.set_placeholder_text(&placeholder_text, window, cx);
+            input
+        });
+
+        Self {
+            label: None,
+            label_size: LabelSize::Small,
+            placeholder: placeholder_text,
+            editor,
+            start_icon: None,
+            disabled: false,
+            min_width: px(192.).into(),
+        }
+    }
+
+    pub fn start_icon(mut self, icon: IconName) -> Self {
+        self.start_icon = Some(icon);
+        self
+    }
+
+    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
+        self.label = Some(label.into());
+        self
+    }
+
+    pub fn label_size(mut self, size: LabelSize) -> Self {
+        self.label_size = size;
+        self
+    }
+
+    pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
+        self.min_width = width.into();
+        self
+    }
+
+    pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
+        self.disabled = disabled;
+        self.editor
+            .update(cx, |editor, _| editor.set_read_only(disabled))
+    }
+
+    pub fn is_empty(&self, cx: &App) -> bool {
+        self.editor().read(cx).text(cx).trim().is_empty()
+    }
+
+    pub fn editor(&self) -> &Entity<Editor> {
+        &self.editor
+    }
+
+    pub fn text(&self, cx: &App) -> String {
+        self.editor().read(cx).text(cx)
+    }
+
+    pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
+        self.editor()
+            .update(cx, |editor, cx| editor.set_text(text, window, cx))
+    }
+}
+
+impl Render for InputField {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let settings = ThemeSettings::get_global(cx);
+        let theme_color = cx.theme().colors();
+
+        let mut style = InputFieldStyle {
+            text_color: theme_color.text,
+            background_color: theme_color.editor_background,
+            border_color: theme_color.border_variant,
+        };
+
+        if self.disabled {
+            style.text_color = theme_color.text_disabled;
+            style.background_color = theme_color.editor_background;
+            style.border_color = theme_color.border_disabled;
+        }
+
+        // if self.error_message.is_some() {
+        //     style.text_color = cx.theme().status().error;
+        //     style.border_color = cx.theme().status().error_border
+        // }
+
+        let text_style = TextStyle {
+            font_family: settings.ui_font.family.clone(),
+            font_features: settings.ui_font.features.clone(),
+            font_size: rems(0.875).into(),
+            font_weight: settings.buffer_font.weight,
+            font_style: FontStyle::Normal,
+            line_height: relative(1.2),
+            color: style.text_color,
+            ..Default::default()
+        };
+
+        let editor_style = EditorStyle {
+            background: theme_color.ghost_element_background,
+            local_player: cx.theme().players().local(),
+            syntax: cx.theme().syntax().clone(),
+            text: text_style,
+            ..Default::default()
+        };
+
+        v_flex()
+            .id(self.placeholder.clone())
+            .w_full()
+            .gap_1()
+            .when_some(self.label.clone(), |this, label| {
+                this.child(
+                    Label::new(label)
+                        .size(self.label_size)
+                        .color(if self.disabled {
+                            Color::Disabled
+                        } else {
+                            Color::Default
+                        }),
+                )
+            })
+            .child(
+                h_flex()
+                    .min_w(self.min_width)
+                    .min_h_8()
+                    .w_full()
+                    .px_2()
+                    .py_1p5()
+                    .flex_grow()
+                    .text_color(style.text_color)
+                    .rounded_md()
+                    .bg(style.background_color)
+                    .border_1()
+                    .border_color(style.border_color)
+                    .when_some(self.start_icon, |this, icon| {
+                        this.gap_1()
+                            .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
+                    })
+                    .child(EditorElement::new(&self.editor, editor_style)),
+            )
+    }
+}
+
+impl Component for InputField {
+    fn scope() -> ComponentScope {
+        ComponentScope::Input
+    }
+
+    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let input_small =
+            cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label"));
+
+        let input_regular = cx.new(|cx| {
+            InputField::new(window, cx, "placeholder")
+                .label("Regular Label")
+                .label_size(LabelSize::Default)
+        });
+
+        Some(
+            v_flex()
+                .gap_6()
+                .children(vec![example_group(vec![
+                    single_example(
+                        "Small Label (Default)",
+                        div().child(input_small).into_any_element(),
+                    ),
+                    single_example(
+                        "Regular Label",
+                        div().child(input_regular).into_any_element(),
+                    ),
+                ])])
+                .into_any_element(),
+        )
+    }
+}

crates/ui_input/src/ui_input.rs πŸ”—

@@ -1,233 +1,9 @@
-//! # UI – Text Field
-//!
-//! This crate provides a text field component that can be used to create text fields like search inputs, form fields, etc.
+//! This crate provides UI components that can be used for form-like scenarios, such as a input and number field.
 //!
 //! It can't be located in the `ui` crate because it depends on `editor`.
 //!
-mod font_picker;
+mod input_field;
 mod number_field;
 
-use component::{example_group, single_example};
-use editor::{Editor, EditorElement, EditorStyle};
-pub use font_picker::*;
-use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle};
+pub use input_field::*;
 pub use number_field::*;
-use settings::Settings;
-use std::sync::Arc;
-use theme::ThemeSettings;
-use ui::prelude::*;
-
-pub struct SingleLineInputStyle {
-    text_color: Hsla,
-    background_color: Hsla,
-    border_color: Hsla,
-}
-
-/// A Text Field that can be used to create text fields like search inputs, form fields, etc.
-///
-/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc.
-#[derive(RegisterComponent)]
-pub struct SingleLineInput {
-    /// An optional label for the text field.
-    ///
-    /// Its position is determined by the [`FieldLabelLayout`].
-    label: Option<SharedString>,
-    /// The size of the label text.
-    label_size: LabelSize,
-    /// The placeholder text for the text field.
-    placeholder: SharedString,
-    /// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API.
-    ///
-    /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases.
-    pub editor: Entity<Editor>,
-    /// An optional icon that is displayed at the start of the text field.
-    ///
-    /// For example, a magnifying glass icon in a search field.
-    start_icon: Option<IconName>,
-    /// Whether the text field is disabled.
-    disabled: bool,
-    /// The minimum width of for the input
-    min_width: Length,
-}
-
-impl Focusable for SingleLineInput {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.editor.focus_handle(cx)
-    }
-}
-
-impl SingleLineInput {
-    pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into<SharedString>) -> Self {
-        let placeholder_text = placeholder.into();
-
-        let editor = cx.new(|cx| {
-            let mut input = Editor::single_line(window, cx);
-            input.set_placeholder_text(&placeholder_text, window, cx);
-            input
-        });
-
-        Self {
-            label: None,
-            label_size: LabelSize::Small,
-            placeholder: placeholder_text,
-            editor,
-            start_icon: None,
-            disabled: false,
-            min_width: px(192.).into(),
-        }
-    }
-
-    pub fn start_icon(mut self, icon: IconName) -> Self {
-        self.start_icon = Some(icon);
-        self
-    }
-
-    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
-        self.label = Some(label.into());
-        self
-    }
-
-    pub fn label_size(mut self, size: LabelSize) -> Self {
-        self.label_size = size;
-        self
-    }
-
-    pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
-        self.min_width = width.into();
-        self
-    }
-
-    pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
-        self.disabled = disabled;
-        self.editor
-            .update(cx, |editor, _| editor.set_read_only(disabled))
-    }
-
-    pub fn is_empty(&self, cx: &App) -> bool {
-        self.editor().read(cx).text(cx).trim().is_empty()
-    }
-
-    pub fn editor(&self) -> &Entity<Editor> {
-        &self.editor
-    }
-
-    pub fn text(&self, cx: &App) -> String {
-        self.editor().read(cx).text(cx)
-    }
-
-    pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
-        self.editor()
-            .update(cx, |editor, cx| editor.set_text(text, window, cx))
-    }
-}
-
-impl Render for SingleLineInput {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let settings = ThemeSettings::get_global(cx);
-        let theme_color = cx.theme().colors();
-
-        let mut style = SingleLineInputStyle {
-            text_color: theme_color.text,
-            background_color: theme_color.editor_background,
-            border_color: theme_color.border_variant,
-        };
-
-        if self.disabled {
-            style.text_color = theme_color.text_disabled;
-            style.background_color = theme_color.editor_background;
-            style.border_color = theme_color.border_disabled;
-        }
-
-        // if self.error_message.is_some() {
-        //     style.text_color = cx.theme().status().error;
-        //     style.border_color = cx.theme().status().error_border
-        // }
-
-        let text_style = TextStyle {
-            font_family: settings.ui_font.family.clone(),
-            font_features: settings.ui_font.features.clone(),
-            font_size: rems(0.875).into(),
-            font_weight: settings.buffer_font.weight,
-            font_style: FontStyle::Normal,
-            line_height: relative(1.2),
-            color: style.text_color,
-            ..Default::default()
-        };
-
-        let editor_style = EditorStyle {
-            background: theme_color.ghost_element_background,
-            local_player: cx.theme().players().local(),
-            syntax: cx.theme().syntax().clone(),
-            text: text_style,
-            ..Default::default()
-        };
-
-        v_flex()
-            .id(self.placeholder.clone())
-            .w_full()
-            .gap_1()
-            .when_some(self.label.clone(), |this, label| {
-                this.child(
-                    Label::new(label)
-                        .size(self.label_size)
-                        .color(if self.disabled {
-                            Color::Disabled
-                        } else {
-                            Color::Default
-                        }),
-                )
-            })
-            .child(
-                h_flex()
-                    .min_w(self.min_width)
-                    .min_h_8()
-                    .w_full()
-                    .px_2()
-                    .py_1p5()
-                    .flex_grow()
-                    .text_color(style.text_color)
-                    .rounded_md()
-                    .bg(style.background_color)
-                    .border_1()
-                    .border_color(style.border_color)
-                    .when_some(self.start_icon, |this, icon| {
-                        this.gap_1()
-                            .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
-                    })
-                    .child(EditorElement::new(&self.editor, editor_style)),
-            )
-    }
-}
-
-impl Component for SingleLineInput {
-    fn scope() -> ComponentScope {
-        ComponentScope::Input
-    }
-
-    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        let input_small =
-            cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Small Label"));
-
-        let input_regular = cx.new(|cx| {
-            SingleLineInput::new(window, cx, "placeholder")
-                .label("Regular Label")
-                .label_size(LabelSize::Default)
-        });
-
-        Some(
-            v_flex()
-                .gap_6()
-                .children(vec![example_group(vec![
-                    single_example(
-                        "Small Label (Default)",
-                        div().child(input_small).into_any_element(),
-                    ),
-                    single_example(
-                        "Regular Label",
-                        div().child(input_regular).into_any_element(),
-                    ),
-                ])])
-                .into_any_element(),
-        )
-    }
-}

crates/zed/src/zed/component_preview.rs πŸ”—

@@ -17,7 +17,7 @@ use persistence::COMPONENT_PREVIEW_DB;
 use project::Project;
 use std::{iter::Iterator, ops::Range, sync::Arc};
 use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use workspace::{
     AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items,
     item::ItemEvent,
@@ -99,7 +99,7 @@ struct ComponentPreview {
     component_map: HashMap<ComponentId, ComponentMetadata>,
     components: Vec<ComponentMetadata>,
     cursor_index: usize,
-    filter_editor: Entity<SingleLineInput>,
+    filter_editor: Entity<InputField>,
     filter_text: String,
     focus_handle: FocusHandle,
     language_registry: Arc<LanguageRegistry>,
@@ -126,8 +126,7 @@ impl ComponentPreview {
         let sorted_components = component_registry.sorted_components();
         let selected_index = selected_index.into().unwrap_or(0);
         let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
-        let filter_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "Find components or usages…"));
+        let filter_editor = cx.new(|cx| InputField::new(window, cx, "Find components or usages…"));
 
         let component_list = ListState::new(
             sorted_components.len(),

crates/zeta2_tools/src/zeta2_tools.rs πŸ”—

@@ -17,7 +17,7 @@ use language::{Buffer, DiskState};
 use ordered_float::OrderedFloat;
 use project::{Project, WorktreeId, telemetry_snapshot::TelemetrySnapshot};
 use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
 use workspace::{Item, SplitDirection, Workspace};
 use zeta2::{PredictionDebugInfo, Zeta, Zeta2FeatureFlag, ZetaOptions};
@@ -65,11 +65,11 @@ pub struct Zeta2Inspector {
     focus_handle: FocusHandle,
     project: Entity<Project>,
     last_prediction: Option<LastPrediction>,
-    max_excerpt_bytes_input: Entity<SingleLineInput>,
-    min_excerpt_bytes_input: Entity<SingleLineInput>,
-    cursor_context_ratio_input: Entity<SingleLineInput>,
-    max_prompt_bytes_input: Entity<SingleLineInput>,
-    max_retrieved_declarations: Entity<SingleLineInput>,
+    max_excerpt_bytes_input: Entity<InputField>,
+    min_excerpt_bytes_input: Entity<InputField>,
+    cursor_context_ratio_input: Entity<InputField>,
+    max_prompt_bytes_input: Entity<InputField>,
+    max_retrieved_declarations: Entity<InputField>,
     active_view: ActiveView,
     zeta: Entity<Zeta>,
     _active_editor_subscription: Option<Subscription>,
@@ -225,9 +225,9 @@ impl Zeta2Inspector {
         label: &'static str,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Entity<SingleLineInput> {
+    ) -> Entity<InputField> {
         let input = cx.new(|cx| {
-            SingleLineInput::new(window, cx, "")
+            InputField::new(window, cx, "")
                 .label(label)
                 .label_min_width(px(64.))
         });
@@ -241,7 +241,7 @@ impl Zeta2Inspector {
                 };
 
                 fn number_input_value<T: FromStr + Default>(
-                    input: &Entity<SingleLineInput>,
+                    input: &Entity<InputField>,
                     cx: &App,
                 ) -> T {
                     input