Introduce TextField by adding the ui_text_field crate (#10361)

There hasn't been a componentized way to create inputs or text fields
thus far due to the innate circular dependency between the `ui` and
`editor` crates. To bypass this issue we are introducing a new
`ui_text_field` crate to specifically handle this component.

`TextField` provides the ability to add stacked or inline labels, as
well as applies a standard visual style to inputs.

Example:

![CleanShot - 2024-04-10 at 11 22
13@2x](https://github.com/zed-industries/zed/assets/1714999/9bf5fc40-5024-4d01-9a8b-fb76f67d7e6e)

We'll continue to evolve this component in the near future and start
using it in the app once we've built out the needed functionality.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Nate Butler 2024-04-10 11:53:25 -04:00 committed by GitHub
parent d03f1c4cab
commit 03d853d344
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 220 additions and 0 deletions

11
Cargo.lock generated
View File

@ -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"

View File

@ -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" }

View File

@ -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 = []

View File

@ -0,0 +1 @@
../../LICENSE-GPL

View File

@ -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<SharedString>,
/// 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<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: View<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>,
/// 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<SharedString>, 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<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> 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<Self>) -> 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<Label> = if self.label_layout == FieldLabelLayout::Stacked {
self.label
.clone()
.map(|label| Label::new(label).size(LabelSize::Small))
} else {
None
};
let inline_label: Option<Label> = if self.label_layout == FieldLabelLayout::Inline {
self.label
.clone()
.map(|label| Label::new(label).size(LabelSize::Small))
} else {
None
};
div()
.when_some(stacked_label, |this, label| this.child(label))
.child(
v_flex()
.w_full()
.px_2()
.py_1()
.bg(style.background_color)
.text_color(style.text_color)
.rounded_lg()
.border()
.border_color(style.border_color)
.w_48()
.child(
h_flex()
.gap_2()
.when_some(inline_label, |this, label| this.child(label))
.child(
h_flex()
.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)),
),
),
)
}
}