Refine keymap UI design (#34437)

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
This commit is contained in:
Danilo Leal 2025-07-15 10:45:59 -03:00 committed by GitHub
parent a65c0b2bff
commit 858e176a1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 397 additions and 211 deletions

2
Cargo.lock generated
View file

@ -14684,6 +14684,7 @@ dependencies = [
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"schemars",
@ -14695,6 +14696,7 @@ dependencies = [
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"workspace",
"workspace-hack",

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 4L10 7L5 10V4Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View file

@ -191,6 +191,7 @@ pub enum IconName {
Play,
PlayAlt,
PlayBug,
PlayFilled,
Plus,
PocketKnife,
Power,

View file

@ -26,6 +26,7 @@ gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
notifications.workspace = true
paths.workspace = true
project.workspace = true
schemars.workspace = true
@ -37,6 +38,7 @@ theme.workspace = true
tree-sitter-json.workspace = true
tree-sitter-rust.workspace = true
ui.workspace = true
ui_input.workspace = true
util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true

View file

@ -10,20 +10,22 @@ use feature_flags::FeatureFlagViewExt;
use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Action, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity,
EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, KeyDownEvent, Keystroke,
ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, StyledText,
Subscription, WeakEntity, actions, anchored, deferred, div,
Action, Animation, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent,
Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext,
KeyDownEvent, Keystroke, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div,
};
use language::{Language, LanguageConfig, ToOffset as _};
use notifications::status_toast::{StatusToast, ToastIcon};
use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
use util::ResultExt;
use ui::{
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render,
SharedString, Styled as _, Tooltip, Window, prelude::*,
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, Modal, ModalFooter, ModalHeader,
ParentElement as _, Render, Section, SharedString, Styled as _, Tooltip, Window, prelude::*,
};
use ui_input::SingleLineInput;
use workspace::{
Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
register_serializable_item,
@ -637,7 +639,8 @@ impl KeymapEditor {
"Delete",
Box::new(DeleteBinding),
)
.action("Copy action", Box::new(CopyAction))
.separator()
.action("Copy Action", Box::new(CopyAction))
.action_disabled_when(
selected_binding_has_no_context,
"Copy Context",
@ -845,9 +848,15 @@ impl KeymapEditor {
self.search_mode = self.search_mode.invert();
self.update_matches(cx);
// Update the keystroke editor to turn the `search` bool on
self.keystroke_editor.update(cx, |keystroke_editor, cx| {
keystroke_editor.set_search_mode(self.search_mode == SearchMode::KeyStroke);
cx.notify();
});
match self.search_mode {
SearchMode::KeyStroke => {
window.focus(&self.keystroke_editor.focus_handle(cx));
window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
}
SearchMode::Normal => {}
}
@ -964,41 +973,60 @@ impl Render for KeymapEditor {
.gap_1()
.bg(theme.colors().editor_background)
.child(
h_flex()
v_flex()
.p_2()
.gap_1()
.key_context({
let mut context = KeyContext::new_with_defaults();
context.add("BufferSearchBar");
context
})
.gap_2()
.child(
div()
.size_full()
.h_8()
.pl_2()
.pr_1()
.py_1()
.border_1()
.border_color(theme.colors().border)
.rounded_lg()
.child(self.filter_editor.clone()),
)
.child(
// TODO: Ask Mikyala if there's a way to get have items be aligned by horizontally
// without embedding a h_flex in another h_flex
h_flex()
.gap_2()
.child(
div()
.key_context({
let mut context = KeyContext::new_with_defaults();
context.add("BufferSearchBar");
context
})
.size_full()
.h_8()
.pl_2()
.pr_1()
.py_1()
.border_1()
.border_color(theme.colors().border)
.rounded_lg()
.child(self.filter_editor.clone()),
)
.child(
IconButton::new(
"KeymapEditorToggleFiltersIcon",
IconName::Keyboard,
)
.shape(ui::IconButtonShape::Square)
.tooltip(|window, cx| {
Tooltip::for_action(
"Search by Keystroke",
&ToggleKeystrokeSearch,
window,
cx,
)
})
.toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
.on_click(|_, window, cx| {
window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
}),
)
.when(self.keybinding_conflict_state.any_conflicts(), |this| {
this.child(
IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
.shape(ui::IconButtonShape::Square)
.tooltip({
let filter_state = self.filter_state;
move |window, cx| {
Tooltip::for_action(
match filter_state {
FilterState::All => "Show conflicts",
FilterState::Conflicts => "Hide conflicts",
FilterState::All => "Show Conflicts",
FilterState::Conflicts => "Hide Conflicts",
},
&ToggleConflictFilter,
window,
@ -1006,7 +1034,7 @@ impl Render for KeymapEditor {
)
}
})
.selected_icon_color(Color::Error)
.selected_icon_color(Color::Warning)
.toggle_state(matches!(
self.filter_state,
FilterState::Conflicts
@ -1018,36 +1046,22 @@ impl Render for KeymapEditor {
);
}),
)
})
.child(
IconButton::new("KeymapEditorToggleFiltersIcon", IconName::Filter)
.tooltip(|window, cx| {
Tooltip::for_action(
"Toggle Keystroke Search",
&ToggleKeystrokeSearch,
window,
cx,
)
})
.toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
.on_click(|_, window, cx| {
window.dispatch_action(
ToggleKeystrokeSearch.boxed_clone(),
cx,
);
}),
),
),
}),
)
.when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
this.child(
div()
.map(|this| {
if self.keybinding_conflict_state.any_conflicts() {
this.pr(rems_from_px(54.))
} else {
this.pr_7()
}
})
.child(self.keystroke_editor.clone()),
)
}),
)
.when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
this.child(
div()
.child(self.keystroke_editor.clone())
.border_1()
.border_color(theme.colors().border)
.rounded_lg(),
)
})
.child(
Table::new()
.interactable(&self.table_interaction_state)
@ -1063,20 +1077,17 @@ impl Render for KeymapEditor {
.filter_map(|index| {
let candidate_id = this.matches.get(index)?.candidate_id;
let binding = &this.keybindings[candidate_id];
let action_name = binding.action_name.clone();
let action = div()
.child(binding.action_name.clone())
.id(("keymap action", index))
.child(command_palette::humanize_action_name(&action_name))
.when(!context_menu_deployed, |this| {
this.tooltip({
let action_name = binding.action_name.clone();
let action_docs = binding.action_docs;
move |_, cx| {
let action_tooltip = Tooltip::new(
command_palette::humanize_action_name(
&action_name,
),
);
let action_tooltip = Tooltip::new(&action_name);
let action_tooltip = match action_docs {
Some(docs) => action_tooltip.meta(docs),
None => action_tooltip,
@ -1285,11 +1296,12 @@ struct KeybindingEditorModal {
editing_keybind: ProcessedKeybinding,
editing_keybind_idx: usize,
keybind_editor: Entity<KeystrokeInput>,
context_editor: Entity<Editor>,
context_editor: Entity<SingleLineInput>,
input_editor: Option<Entity<Editor>>,
fs: Arc<dyn Fs>,
error: Option<InputError>,
keymap_editor: Entity<KeymapEditor>,
workspace: WeakEntity<Workspace>,
}
impl ModalView for KeybindingEditorModal {}
@ -1316,25 +1328,28 @@ impl KeybindingEditorModal {
let keybind_editor = cx
.new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
let context_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
let input = SingleLineInput::new(window, cx, "Keybinding Context")
.label("Edit Context")
.label_size(LabelSize::Default);
if let Some(context) = editing_keybind
.context
.as_ref()
.and_then(KeybindContextString::local)
{
editor.set_text(context.clone(), window, cx);
} else {
editor.set_placeholder_text("Keybinding context", cx);
input.editor().update(cx, |editor, cx| {
editor.set_text(context.clone(), window, cx);
});
}
cx.spawn(async |editor, cx| {
let editor_entity = input.editor().clone();
cx.spawn(async move |_input_handle, cx| {
let contexts = cx
.background_spawn(async { collect_contexts_from_assets() })
.await;
editor
editor_entity
.update(cx, |editor, _cx| {
editor.set_completion_provider(Some(std::rc::Rc::new(
KeyContextCompletionProvider { contexts },
@ -1344,17 +1359,19 @@ impl KeybindingEditorModal {
})
.detach_and_log_err(cx);
editor
input
});
let input_editor = editing_keybind.action_schema.clone().map(|_schema| {
cx.new(|cx| {
let mut editor = Editor::auto_height_unbounded(1, window, cx);
let workspace = workspace.clone();
if let Some(input) = editing_keybind.action_input.clone() {
editor.set_text(input.text, window, cx);
} else {
// TODO: default value from schema?
editor.set_placeholder_text("Action input", cx);
editor.set_placeholder_text("Action Input", cx);
}
cx.spawn(async |editor, cx| {
let json_language = load_json_language(workspace, cx).await;
@ -1383,6 +1400,7 @@ impl KeybindingEditorModal {
input_editor,
error: None,
keymap_editor,
workspace,
}
}
@ -1431,7 +1449,7 @@ impl KeybindingEditorModal {
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
let new_context = self
.context_editor
.read_with(cx, |editor, cx| editor.text(cx));
.read_with(cx, |input, cx| input.editor().read(cx).text(cx));
let new_context = new_context.is_empty().not().then_some(new_context);
let new_context_err = new_context.as_deref().and_then(|context| {
gpui::KeyBindingContextPredicate::parse(context)
@ -1503,6 +1521,25 @@ impl KeybindingEditorModal {
let create = self.creating;
let status_toast = StatusToast::new(
format!(
"Saved edits to the {} action.",
command_palette::humanize_action_name(&self.editing_keybind.action_name)
),
cx,
move |this, _cx| {
this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
.dismiss_button(true)
// .action("Undo", f) todo: wire the undo functionality
},
);
self.workspace
.update(cx, |workspace, cx| {
workspace.toggle_status_toast(status_toast, cx);
})
.log_err();
cx.spawn(async move |this, cx| {
if let Err(err) = save_keybinding_update(
create,
@ -1533,90 +1570,96 @@ impl KeybindingEditorModal {
impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors();
let input_base = || {
div()
.w_full()
.py_2()
.px_3()
.min_h_8()
.rounded_md()
.bg(theme.editor_background)
.border_1()
.border_color(theme.border_variant)
};
let action_name =
command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
v_flex()
.w(rems(34.))
.elevation_3(cx)
.child(
v_flex()
.p_3()
.child(Label::new("Edit Keystroke"))
.child(
Label::new("Input the desired keystroke for the selected action.")
.color(Color::Muted)
.mb_2(),
)
.child(self.keybind_editor.clone()),
)
.when_some(self.input_editor.clone(), |this, editor| {
this.child(
v_flex()
.p_3()
.pt_0()
.child(Label::new("Edit Input"))
.child(
Label::new("Input the desired input to the binding.")
.color(Color::Muted)
.mb_2(),
)
.child(input_base().child(editor)),
v_flex().w(rems(34.)).elevation_3(cx).child(
Modal::new("keybinding_editor_modal", None)
.header(
ModalHeader::new().child(
v_flex()
.pb_1p5()
.mb_1()
.gap_0p5()
.border_b_1()
.border_color(theme.border_variant)
.child(Label::new(action_name))
.when_some(self.editing_keybind.action_docs, |this, docs| {
this.child(
Label::new(docs).size(LabelSize::Small).color(Color::Muted),
)
}),
),
)
})
.child(
v_flex()
.p_3()
.pt_0()
.child(Label::new("Edit Context"))
.child(
Label::new("Input the desired context for the binding.")
.color(Color::Muted)
.mb_2(),
)
.child(input_base().child(self.context_editor.clone())),
)
.when_some(self.error.as_ref(), |this, error| {
this.child(
div().p_2().child(
Banner::new()
.map(|banner| match error {
InputError::Error(_) => banner.severity(ui::Severity::Error),
InputError::Warning(_) => banner.severity(ui::Severity::Warning),
.section(
Section::new().child(
v_flex()
.gap_2()
.child(
v_flex()
.child(Label::new("Edit Keystroke"))
.gap_1()
.child(self.keybind_editor.clone()),
)
.when_some(self.input_editor.clone(), |this, editor| {
this.child(
v_flex()
.mt_1p5()
.gap_1()
.child(Label::new("Edit Arguments"))
.child(
div()
.w_full()
.py_1()
.px_1p5()
.rounded_lg()
.bg(theme.editor_background)
.border_1()
.border_color(theme.border_variant)
.child(editor),
),
)
})
// For some reason, the div overflows its container to the
// right. The padding accounts for that.
.child(div().size_full().pr_2().child(Label::new(error.content()))),
.child(self.context_editor.clone())
.when_some(self.error.as_ref(), |this, error| {
this.child(
Banner::new()
.map(|banner| match error {
InputError::Error(_) => {
banner.severity(ui::Severity::Error)
}
InputError::Warning(_) => {
banner.severity(ui::Severity::Warning)
}
})
// For some reason, the div overflows its container to the
//right. The padding accounts for that.
.child(
div()
.size_full()
.pr_2()
.child(Label::new(error.content())),
),
)
}),
),
)
})
.child(
h_flex()
.p_2()
.w_full()
.gap_1()
.justify_end()
.border_t_1()
.border_color(theme.border_variant)
.child(
Button::new("cancel", "Cancel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(
Button::new("save-btn", "Save").on_click(
cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
),
.footer(
ModalFooter::new().end_slot(
h_flex()
.gap_1()
.child(
Button::new("cancel", "Cancel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(Button::new("save-btn", "Save").on_click(cx.listener(
|this, _event, _window, cx| {
this.save(cx);
},
))),
),
)
),
)
}
}
@ -1848,6 +1891,7 @@ struct KeystrokeInput {
inner_focus_handle: FocusHandle,
intercept_subscription: Option<Subscription>,
_focus_subscriptions: [Subscription; 2],
search: bool,
}
impl KeystrokeInput {
@ -1870,6 +1914,7 @@ impl KeystrokeInput {
outer_focus_handle,
intercept_subscription: None,
_focus_subscriptions,
search: false,
}
}
@ -1987,6 +2032,14 @@ impl KeystrokeInput {
))
})
}
fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
self.inner_focus_handle.clone()
}
fn set_search_mode(&mut self, search: bool) {
self.search = search;
}
}
impl EventEmitter<()> for KeystrokeInput {}
@ -2000,7 +2053,84 @@ impl Focusable for KeystrokeInput {
impl Render for KeystrokeInput {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors();
let is_inner_focused = self.inner_focus_handle.is_focused(window);
let is_focused = self.outer_focus_handle.contains_focused(window, cx);
let is_recording = self.inner_focus_handle.is_focused(window);
let horizontal_padding = rems_from_px(64.);
let recording_bg_color = colors
.editor_background
.blend(colors.text_accent.opacity(0.1));
let recording_indicator = h_flex()
.h_4()
.pr_1()
.gap_0p5()
.border_1()
.border_color(colors.border)
.bg(colors
.editor_background
.blend(colors.text_accent.opacity(0.1)))
.rounded_sm()
.child(
Icon::new(IconName::Circle)
.size(IconSize::Small)
.color(Color::Error)
.with_animation(
"recording-pulse",
Animation::new(std::time::Duration::from_secs(2))
.repeat()
.with_easing(gpui::pulsating_between(0.4, 0.8)),
{
let color = Color::Error.color(cx);
move |this, delta| this.color(Color::Custom(color.opacity(delta)))
},
),
)
.child(
Label::new("REC")
.size(LabelSize::XSmall)
.weight(FontWeight::SEMIBOLD)
.color(Color::Error),
);
let search_indicator = h_flex()
.h_4()
.pr_1()
.gap_0p5()
.border_1()
.border_color(colors.border)
.bg(colors
.editor_background
.blend(colors.text_accent.opacity(0.1)))
.rounded_sm()
.child(
Icon::new(IconName::Circle)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"recording-pulse",
Animation::new(std::time::Duration::from_secs(2))
.repeat()
.with_easing(gpui::pulsating_between(0.4, 0.8)),
{
let color = Color::Accent.color(cx);
move |this, delta| this.color(Color::Custom(color.opacity(delta)))
},
),
)
.child(
Label::new("SEARCH")
.size(LabelSize::XSmall)
.weight(FontWeight::SEMIBOLD)
.color(Color::Accent),
);
let record_icon = if self.search {
IconName::MagnifyingGlass
} else {
IconName::PlayFilled
};
return h_flex()
.id("keystroke-input")
@ -2008,18 +2138,23 @@ impl Render for KeystrokeInput {
.py_2()
.px_3()
.gap_2()
.min_h_8()
.min_h_10()
.w_full()
.flex_1()
.justify_between()
.rounded_md()
.rounded_lg()
.overflow_hidden()
.bg(colors.editor_background)
.border_2()
.map(|this| {
if is_recording {
this.bg(recording_bg_color)
} else {
this.bg(colors.editor_background)
}
})
.border_1()
.border_color(colors.border_variant)
.focus(|mut s| {
s.border_color = Some(colors.border_focused);
s
.when(is_focused, |parent| {
parent.border_color(colors.border_focused)
})
.on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
// TODO: replace with action
@ -2028,19 +2163,29 @@ impl Render for KeystrokeInput {
cx.notify();
}
}))
.child(
h_flex()
.w(horizontal_padding)
.gap_0p5()
.justify_start()
.flex_none()
.when(is_recording, |this| {
this.map(|this| {
if self.search {
this.child(search_indicator)
} else {
this.child(recording_indicator)
}
})
}),
)
.child(
h_flex()
.id("keystroke-input-inner")
.track_focus(&self.inner_focus_handle)
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
.on_key_up(cx.listener(Self::on_key_up))
.when(self.highlight_on_focus, |this| {
this.focus(|mut style| {
style.border_color = Some(colors.border_focused);
style
})
})
.w_full()
.size_full()
.min_w_0()
.justify_center()
.flex_wrap()
@ -2049,40 +2194,52 @@ impl Render for KeystrokeInput {
)
.child(
h_flex()
.w(horizontal_padding)
.gap_0p5()
.justify_end()
.flex_none()
.when(is_inner_focused, |this| {
this.child(
Icon::new(IconName::Circle)
.color(Color::Error)
.with_animation(
"recording-pulse",
gpui::Animation::new(std::time::Duration::from_secs(1))
.repeat()
.with_easing(gpui::pulsating_between(0.8, 1.0)),
{
let color = Color::Error.color(cx);
move |this, delta| {
this.color(Color::Custom(color.opacity(delta)))
.map(|this| {
if is_recording {
this.child(
IconButton::new("stop-record-btn", IconName::StopFilled)
.shape(ui::IconButtonShape::Square)
.map(|this| {
if self.search {
this.tooltip(Tooltip::text("Stop Searching"))
} else {
this.tooltip(Tooltip::text("Stop Recording"))
}
},
),
)
})
.icon_color(Color::Error)
.on_click(cx.listener(|this, _event, window, _cx| {
this.outer_focus_handle.focus(window);
})),
)
} else {
this.child(
IconButton::new("record-btn", record_icon)
.shape(ui::IconButtonShape::Square)
.map(|this| {
if self.search {
this.tooltip(Tooltip::text("Start Searching"))
} else {
this.tooltip(Tooltip::text("Start Recording"))
}
})
.when(!is_focused, |this| this.icon_color(Color::Muted))
.on_click(cx.listener(|this, _event, window, _cx| {
this.inner_focus_handle.focus(window);
})),
)
}
})
.child(
IconButton::new("backspace-btn", IconName::Delete)
.tooltip(Tooltip::text("Delete Keystroke"))
.when(!is_inner_focused, |this| this.icon_color(Color::Muted))
.on_click(cx.listener(|this, _event, _window, cx| {
this.keystrokes.pop();
cx.emit(());
cx.notify();
})),
)
.child(
IconButton::new("clear-btn", IconName::Eraser)
IconButton::new("clear-btn", IconName::Delete)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Clear Keystrokes"))
.when(!is_inner_focused, |this| this.icon_color(Color::Muted))
.when(!is_recording || !is_focused, |this| {
this.icon_color(Color::Muted)
})
.on_click(cx.listener(|this, _event, _window, cx| {
this.keystrokes.clear();
cx.emit(());

View file

@ -27,6 +27,8 @@ pub struct SingleLineInput {
///
/// 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.
@ -59,6 +61,7 @@ impl SingleLineInput {
Self {
label: None,
label_size: LabelSize::Small,
placeholder: placeholder_text,
editor,
start_icon: None,
@ -76,6 +79,11 @@ impl SingleLineInput {
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
@ -138,7 +146,7 @@ impl Render for SingleLineInput {
.when_some(self.label.clone(), |this, label| {
this.child(
Label::new(label)
.size(LabelSize::Small)
.size(self.label_size)
.color(if self.disabled {
Color::Disabled
} else {
@ -148,16 +156,17 @@ impl Render for SingleLineInput {
})
.child(
h_flex()
.min_w_48()
.min_h_8()
.w_full()
.px_2()
.py_1p5()
.bg(style.background_color)
.flex_grow()
.text_color(style.text_color)
.rounded_md()
.rounded_lg()
.bg(style.background_color)
.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))
@ -173,16 +182,28 @@ impl Component for SingleLineInput {
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let input_1 =
cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Some Label"));
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(
"Default",
div().child(input_1.clone()).into_any_element(),
)])])
.children(vec![example_group(vec![
single_example(
"Small Label (Default)",
div().child(input_small.clone()).into_any_element(),
),
single_example(
"Regular Label",
div().child(input_regular.clone()).into_any_element(),
),
])])
.into_any_element(),
)
}