Detailed changes
@@ -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",
@@ -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);
@@ -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);
@@ -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,
@@ -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,
}
@@ -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();
@@ -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,
@@ -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();
@@ -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
});
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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
@@ -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,
@@ -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
@@ -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;
@@ -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()))
+}
@@ -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);
+ }
+ })
+ })
+ }
+}
@@ -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()))
+}
@@ -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)]
@@ -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
@@ -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(),
+ )
+ }
+}
@@ -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(),
- )
- }
-}
@@ -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(),
@@ -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