- Makes the keymap editor search container more consistent with the project & file search corresponding elements - Changes the keymap editor menu item in the user menu be called "Keymap Editor", as opposed to "Key Binding", to match with the tab and action name Design note: Still a bit unsure about the extra space on the right for the keymap editor. This makes it way more consistent with the other search views, but it also just feels like space that could be used. On the other hand, though, it's very unlikely anyone will ever use more than 30% of the search bar width as search queries here are likely pretty short; definitely much shorter than project search queries. <img width="600" height="552" alt="Screenshot 2025-09-09 at 1 02@2x" src="https://github.com/user-attachments/assets/9825a129-2c5a-4852-9837-c586b88e9332" /> Release Notes: - N/A
216 lines
7.0 KiB
Rust
216 lines
7.0 KiB
Rust
//! # 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 component::{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::*;
|
||
|
||
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,
|
||
}
|
||
|
||
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.clone(), cx);
|
||
input
|
||
});
|
||
|
||
Self {
|
||
label: None,
|
||
label_size: LabelSize::Small,
|
||
placeholder: placeholder_text,
|
||
editor,
|
||
start_icon: None,
|
||
disabled: false,
|
||
}
|
||
}
|
||
|
||
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 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)
|
||
}
|
||
}
|
||
|
||
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_48()
|
||
.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(),
|
||
)
|
||
}
|
||
}
|