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

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

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

Release Notes:

- settings_ui: Added the ability to search for theme and icon themes in
their respective fields.
This commit is contained in:
Danilo Leal
2025-10-21 19:58:43 -03:00
committed by GitHub
parent b519f53b3e
commit 8f3da5c5cd
28 changed files with 881 additions and 538 deletions
Generated
+1 -3
View File
@@ -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);
+4 -4
View File
@@ -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,
+10 -11
View File
@@ -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",
+3 -3
View File
@@ -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",
-1
View File
@@ -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
-1
View File
@@ -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,
+1
View File
@@ -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
+8 -95
View File
@@ -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<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);
}
})
})
}
}
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()))
}
+111 -130
View File
@@ -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)]
-2
View File
@@ -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
+222
View File
@@ -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(),
)
}
}
+3 -227
View File
@@ -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(),
)
}
}
+3 -4
View File
@@ -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(),
+9 -9
View File
@@ -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