diff --git a/Cargo.lock b/Cargo.lock index 08bffae4bc..f7d0fb48c8 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 5e1712e626..97e2cc3e8b 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/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, - api_url: Entity, - api_key: Entity, + provider_name: Entity, + api_url: Entity, + api_key: Entity, models: Vec, } @@ -76,10 +76,10 @@ struct ModelCapabilityToggles { } struct ModelInput { - name: Entity, - max_completion_tokens: Entity, - max_output_tokens: Entity, - max_tokens: Entity, + name: Entity, + max_completion_tokens: Entity, + max_output_tokens: Entity, + max_tokens: Entity, capabilities: ModelCapabilityToggles, } @@ -171,9 +171,9 @@ fn single_line_input( text: Option<&str>, window: &mut Window, cx: &mut App, -) -> Entity { +) -> Entity { 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 { - fn set_text( - input: &Entity, - text: &str, - window: &mut Window, - cx: &mut App, - ) { + fn set_text(input: &Entity, 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); diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index fce98ef596..2740ca14f6 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/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, - context_editor: Entity, + context_editor: Entity, action_arguments_editor: Option>, fs: Arc, error: Option, @@ -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 = cx.new(|cx| { - let input = SingleLineInput::new(window, cx, "Keybinding Context") + let context_editor: Entity = cx.new(|cx| { + let input = InputField::new(window, cx, "Keybinding Context") .label("Edit Context") .label_size(LabelSize::Default); diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 2897b836b1..9eb96cb798 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/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, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, 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, diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 47dd565f6a..f3e265e925 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/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, - secret_access_key_editor: Entity, - session_token_editor: Entity, - region_editor: Entity, + access_key_id_editor: Entity, + secret_access_key_editor: Entity, + session_token_editor: Entity, + region_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -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, } diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index ec420bfd83..8784d3805f 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/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, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -533,7 +533,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> 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(); diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index f6ac364611..a4d1202bee 100644 --- a/crates/language_models/src/provider/google.rs +++ b/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, + api_key_editor: Entity, state: Entity, target_agent: language_model::ConfigurationViewTargetAgent, load_credentials_task: Option>, @@ -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, diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index ad7bf600d5..66527792ff 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/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, - codestral_api_key_editor: Entity, + api_key_editor: Entity, + codestral_api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -753,9 +753,9 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> 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(); diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index a25ecbe01a..2150966c1a 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/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, - api_url_editor: Entity, + api_key_editor: Entity, + api_url_editor: Entity, state: Entity, } impl ConfigurationView { pub fn new(state: Entity, window: &mut Window, cx: &mut Context) -> 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 }); diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 3eaaa8b585..6c3f063c11 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/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, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -683,7 +683,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "sk-000000000000000000000000000000000000000000000000", diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index cca49c982c..c8a1da5f5a 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/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, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -348,7 +348,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "000000000000000000000000000000000000000000000000000", diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 5bfc97c41f..50131f0a8e 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/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, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -700,7 +700,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "sk_or_000000000000000000000000000000000000000000000000", diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index ad12e5a628..ff5d4567c6 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/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, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -370,7 +370,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "v1:0000000000000000000000000000000000000000000000000", diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 243a2e3e02..8b51ca1209 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/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, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -367,7 +367,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "xai-0000000000000000000000000000000000000000000000000", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 24278f3867..2ff3467c48 100644 --- a/crates/onboarding/Cargo.toml +++ b/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 diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index e252966814..a1139d7f25 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/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, diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index b8a75694ec..b2b40bf250 100644 --- a/crates/settings_ui/Cargo.toml +++ b/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 diff --git a/crates/settings_ui/src/components.rs b/crates/settings_ui/src/components.rs index d424d96f27..5026ca8061 100644 --- a/crates/settings_ui/src/components.rs +++ b/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, -}; +mod font_picker; +mod icon_theme_picker; +mod input_field; +mod theme_picker; -#[derive(IntoElement)] -pub struct SettingsEditor { - initial_text: Option, - placeholder: Option<&'static str>, - confirm: Option, &mut App)>>, - tab_index: Option, -} - -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, &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::({ - 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); - } - }) - }) - } -} +pub use font_picker::font_picker; +pub use icon_theme_picker::icon_theme_picker; +pub use input_field::*; +pub use theme_picker::theme_picker; diff --git a/crates/ui_input/src/font_picker.rs b/crates/settings_ui/src/components/font_picker.rs similarity index 100% rename from crates/ui_input/src/font_picker.rs rename to crates/settings_ui/src/components/font_picker.rs diff --git a/crates/settings_ui/src/components/icon_theme_picker.rs b/crates/settings_ui/src/components/icon_theme_picker.rs new file mode 100644 index 0000000000..33a648f81b --- /dev/null +++ b/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; + +pub struct IconThemePickerDelegate { + icon_themes: Vec, + filtered_themes: Vec, + selected_index: usize, + current_theme: SharedString, + on_theme_changed: Arc, +} + +impl IconThemePickerDelegate { + fn new( + current_theme: SharedString, + on_theme_changed: impl Fn(SharedString, &mut App) + 'static, + cx: &mut Context, + ) -> Self { + let theme_registry = ThemeRegistry::global(cx); + + let icon_themes: Vec = 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) { + 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 { + "Search icon theme…".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let icon_themes = self.icon_themes.clone(); + let current_theme = self.current_theme.clone(); + + let matches: Vec = 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 = 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, + ) { + 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) { + 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, + ) -> Option { + 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 { + 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())) +} diff --git a/crates/settings_ui/src/components/input_field.rs b/crates/settings_ui/src/components/input_field.rs new file mode 100644 index 0000000000..57917c3211 --- /dev/null +++ b/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, + placeholder: Option<&'static str>, + confirm: Option, &mut App)>>, + tab_index: Option, +} + +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, &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::({ + 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); + } + }) + }) + } +} diff --git a/crates/settings_ui/src/components/theme_picker.rs b/crates/settings_ui/src/components/theme_picker.rs new file mode 100644 index 0000000000..2146ab314f --- /dev/null +++ b/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; + +pub struct ThemePickerDelegate { + themes: Vec, + filtered_themes: Vec, + selected_index: usize, + current_theme: SharedString, + on_theme_changed: Arc, +} + +impl ThemePickerDelegate { + fn new( + current_theme: SharedString, + on_theme_changed: impl Fn(SharedString, &mut App) + 'static, + cx: &mut Context, + ) -> 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) { + 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 { + "Search theme…".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let themes = self.themes.clone(); + let current_theme = self.current_theme.clone(); + + let matches: Vec = 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 = 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) { + 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) { + 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, + ) -> Option { + 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 { + 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())) +} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index e4b9246476..5c76008e83 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/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 + Into + AsRef + 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 + From + Copy>( .into_any_element() } -fn render_font_picker( - field: SettingField, - 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( field: SettingField, 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, + 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, 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)] diff --git a/crates/ui_input/Cargo.toml b/crates/ui_input/Cargo.toml index 7aa74591b8..4e7b08241d 100644 --- a/crates/ui_input/Cargo.toml +++ b/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 diff --git a/crates/ui_input/src/input_field.rs b/crates/ui_input/src/input_field.rs new file mode 100644 index 0000000000..82f7f0261f --- /dev/null +++ b/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, + /// The size of the label text. + label_size: LabelSize, + /// The placeholder text for the text field. + placeholder: SharedString, + /// Exposes the underlying [`Entity`] 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, + /// 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, + /// 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) -> 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) -> 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) -> Self { + self.min_width = width.into(); + self + } + + pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context) { + 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 { + &self.editor + } + + pub fn text(&self, cx: &App) -> String { + self.editor().read(cx).text(cx) + } + + pub fn set_text(&self, text: impl Into>, 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) -> 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 { + 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(), + ) + } +} diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index 56f0626f0a..ddc0e659a2 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/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, - /// The size of the label text. - label_size: LabelSize, - /// The placeholder text for the text field. - placeholder: SharedString, - /// Exposes the underlying [`Entity`] 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, - /// 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, - /// 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) -> 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) -> 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) -> Self { - self.min_width = width.into(); - self - } - - pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context) { - 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 { - &self.editor - } - - pub fn text(&self, cx: &App) -> String { - self.editor().read(cx).text(cx) - } - - pub fn set_text(&self, text: impl Into>, 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) -> 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 { - 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(), - ) - } -} diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 75369bbe03..153d66f04e 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/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, components: Vec, cursor_index: usize, - filter_editor: Entity, + filter_editor: Entity, filter_text: String, focus_handle: FocusHandle, language_registry: Arc, @@ -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(), diff --git a/crates/zeta2_tools/src/zeta2_tools.rs b/crates/zeta2_tools/src/zeta2_tools.rs index 7b806e2b9a..dbb7a5af7d 100644 --- a/crates/zeta2_tools/src/zeta2_tools.rs +++ b/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, last_prediction: Option, - max_excerpt_bytes_input: Entity, - min_excerpt_bytes_input: Entity, - cursor_context_ratio_input: Entity, - max_prompt_bytes_input: Entity, - max_retrieved_declarations: Entity, + max_excerpt_bytes_input: Entity, + min_excerpt_bytes_input: Entity, + cursor_context_ratio_input: Entity, + max_prompt_bytes_input: Entity, + max_retrieved_declarations: Entity, active_view: ActiveView, zeta: Entity, _active_editor_subscription: Option, @@ -225,9 +225,9 @@ impl Zeta2Inspector { label: &'static str, window: &mut Window, cx: &mut Context, - ) -> Entity { + ) -> Entity { 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( - input: &Entity, + input: &Entity, cx: &App, ) -> T { input