diff --git a/Cargo.lock b/Cargo.lock index a2b4592d2b..7e0f2e6cb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10718,6 +10718,17 @@ dependencies = [ "windows 0.53.0", ] +[[package]] +name = "ui_text_field" +version = "0.1.0" +dependencies = [ + "editor", + "gpui", + "settings", + "theme", + "ui", +] + [[package]] name = "unicase" version = "2.7.0" diff --git a/Cargo.toml b/Cargo.toml index 045793059b..34f41d0425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ members = [ "crates/telemetry_events", "crates/time_format", "crates/ui", + "crates/ui_text_field", "crates/util", "crates/vcs_menu", "crates/vim", @@ -214,6 +215,7 @@ theme_selector = { path = "crates/theme_selector" } telemetry_events = { path = "crates/telemetry_events" } time_format = { path = "crates/time_format" } ui = { path = "crates/ui" } +ui_text_field = { path = "crates/ui_text_field" } util = { path = "crates/util" } vcs_menu = { path = "crates/vcs_menu" } vim = { path = "crates/vim" } diff --git a/crates/ui_text_field/Cargo.toml b/crates/ui_text_field/Cargo.toml new file mode 100644 index 0000000000..a9b6161a93 --- /dev/null +++ b/crates/ui_text_field/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ui_text_field" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/ui_text_field.rs" + +[dependencies] +editor.workspace = true +gpui.workspace = true +settings.workspace = true +theme.workspace = true +ui.workspace = true + +[features] +default = [] diff --git a/crates/ui_text_field/LICENSE-GPL b/crates/ui_text_field/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/ui_text_field/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui_text_field/src/ui_text_field.rs b/crates/ui_text_field/src/ui_text_field.rs new file mode 100644 index 0000000000..cdbc95f0c9 --- /dev/null +++ b/crates/ui_text_field/src/ui_text_field.rs @@ -0,0 +1,184 @@ +//! # UI – Text Field +//! +//! This crate provides a text field component that can be used to create text fields like search inputs, form fields, etc. +//! +//! It can't be located in the `ui` crate because it depends on `editor`. +//! + +use editor::*; +use gpui::*; +use settings::Settings; +use theme::ThemeSettings; +use ui::*; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum FieldLabelLayout { + Inline, + Stacked, +} + +pub struct TextFieldStyle { + text_color: Hsla, + background_color: Hsla, + border_color: Hsla, +} + +/// A Text Field view that can be used to create text fields like search inputs, form fields, etc. +/// +/// It wraps a single line [`Editor`] view and allows for common field properties like labels, placeholders, icons, etc. +pub struct TextField { + /// An optional label for the text field. + /// + /// Its position is determined by the [`FieldLabelLayout`]. + label: Option, + /// The placeholder text for the text field. + /// + /// All text fields must have placeholder text that is displayed when the field is empty. + placeholder: SharedString, + /// Exposes the underlying [`View`] 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: View, + /// 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, + /// The layout of the label relative to the text field. + label_layout: FieldLabelLayout, +} + +impl FocusableView for TextField { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl TextField { + pub fn new(placeholder: impl Into, cx: &mut WindowContext) -> Self { + let placeholder_text = placeholder.into(); + + let editor = cx.new_view(|cx| { + let mut input = Editor::single_line(cx); + input.set_placeholder_text(placeholder_text.clone(), cx); + input + }); + + Self { + label: None, + placeholder: placeholder_text, + editor, + start_icon: None, + label_layout: FieldLabelLayout::Stacked, + } + } + + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + pub fn placeholder(mut self, placeholder: impl Into) -> Self { + self.placeholder = placeholder.into(); + self + } + + pub fn start_icon(mut self, icon: IconName) -> Self { + self.start_icon = Some(icon); + self + } + + pub fn label_layout(mut self, layout: FieldLabelLayout) -> Self { + self.label_layout = layout; + self + } +} + +impl Render for TextField { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let theme_color = cx.theme().colors(); + + let style = TextFieldStyle { + text_color: theme_color.text, + background_color: theme_color.ghost_element_background, + border_color: theme_color.border, + }; + + // if self.disabled { + // style.text_color = theme_color.text_disabled; + // style.background_color = theme_color.ghost_element_disabled; + // 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.buffer_font.family.clone(), + font_features: settings.buffer_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + 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(), + text: text_style, + ..Default::default() + }; + + let stacked_label: Option