diff --git a/Cargo.lock b/Cargo.lock index d2f9af87d9..601810f1e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,7 @@ dependencies = [ "time", "time_format", "ui", + "ui_input", "util", "uuid", "vim_mode_setting", @@ -15310,8 +15311,10 @@ dependencies = [ name = "ui_input" version = "0.1.0" dependencies = [ + "component", "editor", "gpui", + "linkme", "settings", "theme", "ui", diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 2abbfb65f2..3da0f395ff 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -81,6 +81,7 @@ theme.workspace = true time.workspace = true time_format.workspace = true ui.workspace = true +ui_input.workspace = true util.workspace = true uuid.workspace = true vim_mode_setting.workspace = true diff --git a/crates/agent/src/assistant_configuration/add_context_server_modal.rs b/crates/agent/src/assistant_configuration/add_context_server_modal.rs index cc1f97a643..7ffdb053ac 100644 --- a/crates/agent/src/assistant_configuration/add_context_server_modal.rs +++ b/crates/agent/src/assistant_configuration/add_context_server_modal.rs @@ -1,17 +1,17 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig}; -use editor::Editor; use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*}; use serde_json::json; use settings::update_settings_file; use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*}; +use ui_input::SingleLineInput; use workspace::{ModalView, Workspace}; use crate::AddContextServer; pub struct AddContextServerModal { workspace: WeakEntity, - name_editor: Entity, - command_editor: Entity, + name_editor: Entity, + command_editor: Entity, } impl AddContextServerModal { @@ -33,15 +33,10 @@ impl AddContextServerModal { window: &mut Window, cx: &mut Context, ) -> Self { - let name_editor = cx.new(|cx| Editor::single_line(window, cx)); - let command_editor = cx.new(|cx| Editor::single_line(window, cx)); - - name_editor.update(cx, |editor, cx| { - editor.set_placeholder_text("Context server name", cx); - }); - - command_editor.update(cx, |editor, cx| { - editor.set_placeholder_text("Command to run the context server", cx); + let name_editor = + cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name")); + let command_editor = cx.new(|cx| { + SingleLineInput::new(window, cx, "Command").label("Command to run the context server") }); Self { @@ -52,8 +47,22 @@ impl AddContextServerModal { } fn confirm(&mut self, cx: &mut Context) { - let name = self.name_editor.read(cx).text(cx).trim().to_string(); - let command = self.command_editor.read(cx).text(cx).trim().to_string(); + let name = self + .name_editor + .read(cx) + .editor() + .read(cx) + .text(cx) + .trim() + .to_string(); + let command = self + .command_editor + .read(cx) + .editor() + .read(cx) + .text(cx) + .trim() + .to_string(); if name.is_empty() || command.is_empty() { return; @@ -104,8 +113,8 @@ impl EventEmitter for AddContextServerModal {} impl Render for AddContextServerModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let is_name_empty = self.name_editor.read(cx).text(cx).trim().is_empty(); - let is_command_empty = self.command_editor.read(cx).text(cx).trim().is_empty(); + let is_name_empty = self.name_editor.read(cx).is_empty(cx); + let is_command_empty = self.command_editor.read(cx).is_empty(cx); div() .elevation_3(cx) @@ -122,18 +131,8 @@ impl Render for AddContextServerModal { .header(ModalHeader::new().headline("Add Context Server")) .section( Section::new() - .child( - v_flex() - .gap_1() - .child(Label::new("Name")) - .child(self.name_editor.clone()), - ) - .child( - v_flex() - .gap_1() - .child(Label::new("Command")) - .child(self.command_editor.clone()), - ), + .child(self.name_editor.clone()) + .child(self.command_editor.clone()), ) .footer( ModalFooter::new() diff --git a/crates/ui_input/Cargo.toml b/crates/ui_input/Cargo.toml index f075b33f19..38c7a09393 100644 --- a/crates/ui_input/Cargo.toml +++ b/crates/ui_input/Cargo.toml @@ -12,8 +12,10 @@ workspace = true path = "src/ui_input.rs" [dependencies] +component.workspace = true editor.workspace = true gpui.workspace = true +linkme.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index 74f1807579..3bf2ad4a50 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -5,20 +5,14 @@ //! It can't be located in the `ui` crate because it depends on `editor`. //! +use component::{ComponentPreview, example_group, single_example}; use editor::{Editor, EditorElement, EditorStyle}; use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, TextStyle}; use settings::Settings; use theme::ThemeSettings; use ui::prelude::*; -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum FieldLabelLayout { - Hidden, - Inline, - Stacked, -} - -pub struct TextFieldStyle { +pub struct SingleLineInputStyle { text_color: Hsla, background_color: Hsla, border_color: Hsla, @@ -27,11 +21,13 @@ pub struct TextFieldStyle { /// 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. -pub struct TextField { +#[derive(IntoComponent)] +#[component(scope = "Input")] +pub struct SingleLineInput { /// An optional label for the text field. /// /// Its position is determined by the [`FieldLabelLayout`]. - label: SharedString, + label: Option, /// The placeholder text for the text field. placeholder: SharedString, /// Exposes the underlying [`Model`] to allow for customizing the editor beyond the provided API. @@ -42,25 +38,18 @@ pub struct TextField { /// /// For example, a magnifying glass icon in a search field. start_icon: Option, - /// The layout of the label relative to the text field. - with_label: FieldLabelLayout, /// Whether the text field is disabled. disabled: bool, } -impl Focusable for TextField { +impl Focusable for SingleLineInput { fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) } } -impl TextField { - pub fn new( - window: &mut Window, - cx: &mut App, - label: impl Into, - placeholder: impl Into, - ) -> Self { +impl SingleLineInput { + pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into) -> Self { let placeholder_text = placeholder.into(); let editor = cx.new(|cx| { @@ -70,11 +59,10 @@ impl TextField { }); Self { - label: label.into(), + label: None, placeholder: placeholder_text, editor, start_icon: None, - with_label: FieldLabelLayout::Hidden, disabled: false, } } @@ -84,8 +72,8 @@ impl TextField { self } - pub fn with_label(mut self, layout: FieldLabelLayout) -> Self { - self.with_label = layout; + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); self } @@ -95,25 +83,29 @@ impl TextField { .update(cx, |editor, _| editor.set_read_only(disabled)) } + pub fn is_empty(&self, cx: &App) -> bool { + self.editor().read(cx).text(cx).trim().is_empty() + } + pub fn editor(&self) -> &Entity { &self.editor } } -impl Render for TextField { +impl Render for SingleLineInput { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let theme_color = cx.theme().colors(); - let mut style = TextFieldStyle { + let mut style = SingleLineInputStyle { text_color: theme_color.text, - background_color: theme_color.ghost_element_background, - border_color: theme_color.border, + 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.ghost_element_disabled; + style.background_color = theme_color.editor_background; style.border_color = theme_color.border_disabled; } @@ -123,8 +115,8 @@ impl Render for TextField { // } let text_style = TextStyle { - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), + 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, @@ -140,13 +132,13 @@ impl Render for TextField { ..Default::default() }; - div() + v_flex() .id(self.placeholder.clone()) - .group("text-field") .w_full() - .when(self.with_label == FieldLabelLayout::Stacked, |this| { + .gap_1() + .when_some(self.label.clone(), |this, label| { this.child( - Label::new(self.label.clone()) + Label::new(label) .size(LabelSize::Default) .color(if self.disabled { Color::Disabled @@ -156,35 +148,37 @@ impl Render for TextField { ) }) .child( - v_flex().w_full().child( - h_flex() - .w_full() - .flex_grow() - .gap_2() - .when(self.with_label == FieldLabelLayout::Inline, |this| { - this.child(Label::new(self.label.clone()).size(LabelSize::Default)) - }) - .child( - h_flex() - .px_2() - .py_1() - .bg(style.background_color) - .text_color(style.text_color) - .rounded_lg() - .border_1() - .border_color(style.border_color) - .min_w_48() - .w_full() - .flex_grow() - .gap_1() - .when_some(self.start_icon, |this, icon| { - this.child( - Icon::new(icon).size(IconSize::Small).color(Color::Muted), - ) - }) - .child(EditorElement::new(&self.editor, editor_style)), - ), - ), + h_flex() + .px_2() + .py_1() + .bg(style.background_color) + .text_color(style.text_color) + .rounded_md() + .border_1() + .border_color(style.border_color) + .min_w_48() + .w_full() + .flex_grow() + .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 ComponentPreview for SingleLineInput { + fn preview(window: &mut Window, cx: &mut App) -> AnyElement { + let input_1 = + cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Some Label")); + + v_flex() + .gap_6() + .children(vec![example_group(vec![single_example( + "Default", + div().child(input_1.clone()).into_any_element(), + )])]) + .into_any_element() + } +}