input: Fix Input prefix, suffix position, and mask * in vertical center. (#965)
- Fix Input, NumberInput padding x with prefix, suffix. - Fix Input mask char to render in vertical center. <img width="498" alt="image" src="https://github.com/user-attachments/assets/f7b34e17-2503-4a24-9e16-3a30b48bacb5" /> - Fix to only use `Tab`, `Shift-Tab` to indent in multi-line mode. - Improve blink cursor to render with 1.5px width and same as line-height.
This commit is contained in:
parent
56e282315d
commit
de491a790c
7 changed files with 109 additions and 145 deletions
|
|
@ -6,8 +6,7 @@ use gpui::{
|
|||
|
||||
use crate::section;
|
||||
use gpui_component::{
|
||||
button::{Button, ButtonVariant, ButtonVariants as _},
|
||||
h_flex,
|
||||
button::{Button, ButtonVariants as _},
|
||||
input::{InputEvent, InputState, MaskPattern, TextInput},
|
||||
v_flex, ContextModal, FocusableCycle, Icon, IconName, Sizable,
|
||||
};
|
||||
|
|
@ -157,14 +156,17 @@ impl FocusableCycle for InputStory {
|
|||
[
|
||||
self.input1.focus_handle(cx),
|
||||
self.input2.focus_handle(cx),
|
||||
self.input_esc.focus_handle(cx),
|
||||
self.disabled_input.focus_handle(cx),
|
||||
self.mask_input.focus_handle(cx),
|
||||
self.prefix_input1.focus_handle(cx),
|
||||
self.both_input1.focus_handle(cx),
|
||||
self.suffix_input1.focus_handle(cx),
|
||||
self.currency_input.focus_handle(cx),
|
||||
self.phone_input.focus_handle(cx),
|
||||
self.mask_input2.focus_handle(cx),
|
||||
self.large_input.focus_handle(cx),
|
||||
self.small_input.focus_handle(cx),
|
||||
self.input_esc.focus_handle(cx),
|
||||
]
|
||||
.to_vec()
|
||||
}
|
||||
|
|
@ -189,7 +191,7 @@ impl Render for InputStory {
|
|||
section("Normal Input")
|
||||
.max_w_md()
|
||||
.child(TextInput::new(&self.input1).cleanable())
|
||||
.child(self.input2.clone()),
|
||||
.child(TextInput::new(&self.input2)),
|
||||
)
|
||||
.child(
|
||||
section("Input State")
|
||||
|
|
@ -203,28 +205,18 @@ impl Render for InputStory {
|
|||
.child(
|
||||
TextInput::new(&self.prefix_input1)
|
||||
.cleanable()
|
||||
.prefix(Icon::new(IconName::Search).small().ml_3()),
|
||||
.prefix(Icon::new(IconName::Search).small()),
|
||||
)
|
||||
.child(
|
||||
TextInput::new(&self.both_input1)
|
||||
.cleanable()
|
||||
.prefix(div().child(Icon::new(IconName::Search).small()).ml_3())
|
||||
.suffix(
|
||||
Button::new("info")
|
||||
.ghost()
|
||||
.icon(IconName::Info)
|
||||
.xsmall()
|
||||
.mr_3(),
|
||||
),
|
||||
.prefix(div().child(Icon::new(IconName::Search).small()))
|
||||
.suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()),
|
||||
)
|
||||
.child(
|
||||
TextInput::new(&self.suffix_input1).cleanable().suffix(
|
||||
Button::new("info")
|
||||
.ghost()
|
||||
.icon(IconName::Info)
|
||||
.xsmall()
|
||||
.mr_3(),
|
||||
),
|
||||
TextInput::new(&self.suffix_input1)
|
||||
.cleanable()
|
||||
.suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
|
@ -264,8 +256,8 @@ impl Render for InputStory {
|
|||
.child(
|
||||
section("Input Size")
|
||||
.max_w_md()
|
||||
.child(TextInput::new(&self.large_input).large())
|
||||
.child(TextInput::new(&self.small_input).small()),
|
||||
.child(TextInput::new(&self.large_input).large().cleanable())
|
||||
.child(TextInput::new(&self.small_input).small().cleanable()),
|
||||
)
|
||||
.child(
|
||||
section("Cleanable and ESC to clean")
|
||||
|
|
@ -282,26 +274,5 @@ impl Render for InputStory {
|
|||
window.focused_input(cx).map(|input| input.read(cx).value())
|
||||
))),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("btn-submit")
|
||||
.flex_1()
|
||||
.with_variant(ButtonVariant::Primary)
|
||||
.label("Submit")
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(Tab), cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("btn-cancel")
|
||||
.flex_1()
|
||||
.label("Cancel")
|
||||
.into_element(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,13 +217,9 @@ impl Render for NumberInputStory {
|
|||
)
|
||||
.child(
|
||||
section("Small Size with suffix").max_w_md().child(
|
||||
NumberInput::new(&self.number_input2).small().suffix(
|
||||
Button::new("info")
|
||||
.ghost()
|
||||
.icon(IconName::Info)
|
||||
.xsmall()
|
||||
.mr_3(),
|
||||
),
|
||||
NumberInput::new(&self.number_input2)
|
||||
.small()
|
||||
.suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use gpui::{Context, Timer};
|
||||
use gpui::{px, Context, Pixels, Timer};
|
||||
|
||||
static INTERVAL: Duration = Duration::from_millis(500);
|
||||
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
||||
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
||||
|
||||
/// To manage the Input cursor blinking.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use smallvec::SmallVec;
|
|||
|
||||
use crate::{
|
||||
highlighter::{LanguageRegistry, SyntaxHighlighter},
|
||||
input::blink_cursor::CURSOR_WIDTH,
|
||||
ActiveTheme as _, Root,
|
||||
};
|
||||
|
||||
|
|
@ -174,14 +175,13 @@ impl TextElement {
|
|||
|
||||
if input.show_cursor(window, cx) {
|
||||
// cursor blink
|
||||
let cursor_height =
|
||||
window.text_style().font_size.to_pixels(window.rem_size()) + px(2.);
|
||||
let cursor_height = line_height;
|
||||
cursor_bounds = Some(Bounds::new(
|
||||
point(
|
||||
bounds.left() + cursor_pos.x + line_number_width,
|
||||
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
|
||||
),
|
||||
size(px(1.), cursor_height),
|
||||
size(CURSOR_WIDTH, cursor_height),
|
||||
));
|
||||
};
|
||||
}
|
||||
|
|
@ -864,13 +864,13 @@ impl Element for TextElement {
|
|||
invisible_top_padding += line.size(line_height).height;
|
||||
}
|
||||
|
||||
let mut offset_y = px(0.);
|
||||
let mut mask_offset_y = px(0.);
|
||||
if self.input.read(cx).masked {
|
||||
// Move down offset for vertical centering the *****
|
||||
if cfg!(target_os = "macos") {
|
||||
offset_y = px(3.);
|
||||
mask_offset_y = px(3.);
|
||||
} else {
|
||||
offset_y = px(2.5);
|
||||
mask_offset_y = px(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -879,6 +879,7 @@ impl Element for TextElement {
|
|||
.style
|
||||
.active_line;
|
||||
|
||||
let mut offset_y = px(0.);
|
||||
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
||||
offset_y += invisible_top_padding;
|
||||
|
||||
|
|
@ -909,7 +910,7 @@ impl Element for TextElement {
|
|||
}
|
||||
|
||||
// Paint text
|
||||
let mut offset_y = invisible_top_padding;
|
||||
let mut offset_y = mask_offset_y + invisible_top_padding;
|
||||
for line in prepaint
|
||||
.last_layout
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -122,11 +122,6 @@ impl RenderOnce for NumberInput {
|
|||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let focused = self.state.focus_handle(cx).is_focused(window);
|
||||
|
||||
let btn_size = match self.size {
|
||||
Size::XSmall | Size::Small => Size::Size(px(16.)),
|
||||
_ => Size::XSmall,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.key_context(KEY_CONTENT)
|
||||
|
|
@ -134,11 +129,7 @@ impl RenderOnce for NumberInput {
|
|||
.on_action(window.listener_for(&self.state, InputState::on_action_decrement))
|
||||
.flex_1()
|
||||
.input_size(self.size)
|
||||
.px(match self.size {
|
||||
Size::XSmall => px(1.),
|
||||
Size::Small => px(2.),
|
||||
_ => px(3.),
|
||||
})
|
||||
.px(self.size.input_px() / 2.)
|
||||
.bg(cx.theme().background)
|
||||
.border_color(cx.theme().input)
|
||||
.border_1()
|
||||
|
|
@ -147,8 +138,9 @@ impl RenderOnce for NumberInput {
|
|||
.child(
|
||||
Button::new("minus")
|
||||
.ghost()
|
||||
.with_size(btn_size)
|
||||
.with_size(self.size.smaller())
|
||||
.icon(IconName::Minus)
|
||||
.compact()
|
||||
.on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, window, cx| {
|
||||
|
|
@ -159,15 +151,17 @@ impl RenderOnce for NumberInput {
|
|||
.child(
|
||||
TextInput::new(&self.state)
|
||||
.appearance(false)
|
||||
.no_gap()
|
||||
.px(px(2.))
|
||||
.gap_0()
|
||||
.when_some(self.prefix, |this, prefix| this.prefix(prefix))
|
||||
.when_some(self.suffix, |this, suffix| this.suffix(suffix)),
|
||||
)
|
||||
.child(
|
||||
Button::new("plus")
|
||||
.ghost()
|
||||
.with_size(btn_size)
|
||||
.with_size(self.size.smaller())
|
||||
.icon(IconName::Plus)
|
||||
.compact()
|
||||
.on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, window, cx| {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
|
||||
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, Styled as _, Window,
|
||||
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
||||
Window,
|
||||
};
|
||||
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
|
|
@ -18,8 +19,8 @@ use super::InputState;
|
|||
#[derive(IntoElement)]
|
||||
pub struct TextInput {
|
||||
state: Entity<InputState>,
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
no_gap: bool,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
height: Option<DefiniteLength>,
|
||||
|
|
@ -44,7 +45,7 @@ impl TextInput {
|
|||
Self {
|
||||
state: state.clone(),
|
||||
size: Size::default(),
|
||||
no_gap: false,
|
||||
style: StyleRefinement::default(),
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
height: None,
|
||||
|
|
@ -115,14 +116,6 @@ impl TextInput {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set true to not use gap between input and prefix, suffix, and clear button.
|
||||
///
|
||||
/// Default: false
|
||||
pub(super) fn no_gap(mut self) -> Self {
|
||||
self.no_gap = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement {
|
||||
Button::new("toggle-mask")
|
||||
.icon(IconName::Eye)
|
||||
|
|
@ -147,6 +140,12 @@ impl TextInput {
|
|||
}
|
||||
}
|
||||
|
||||
impl Styled for TextInput {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TextInput {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
const LINE_HEIGHT: Rems = Rems(1.25);
|
||||
|
|
@ -161,25 +160,24 @@ impl RenderOnce for TextInput {
|
|||
|
||||
let state = self.state.read(cx);
|
||||
let focused = state.focus_handle.is_focused(window);
|
||||
let mut gap_x = match self.size {
|
||||
let gap_x = match self.size {
|
||||
Size::Small => px(4.),
|
||||
Size::Large => px(8.),
|
||||
_ => px(4.),
|
||||
};
|
||||
if self.no_gap {
|
||||
gap_x = px(0.);
|
||||
}
|
||||
|
||||
let prefix = self.prefix;
|
||||
let suffix = self.suffix;
|
||||
let show_clear_button =
|
||||
self.cleanable && !state.loading && !state.text.is_empty() && state.is_single_line();
|
||||
let bg = if state.disabled {
|
||||
cx.theme().muted
|
||||
} else {
|
||||
cx.theme().background
|
||||
};
|
||||
|
||||
let prefix = self.prefix;
|
||||
let suffix = self.suffix;
|
||||
let show_clear_button =
|
||||
self.cleanable && !state.loading && !state.text.is_empty() && state.is_single_line();
|
||||
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
||||
|
||||
div()
|
||||
.id(("input", self.state.entity_id()))
|
||||
.flex()
|
||||
|
|
@ -196,14 +194,16 @@ impl RenderOnce for TextInput {
|
|||
.on_action(window.listener_for(&self.state, InputState::delete_next_word))
|
||||
.on_action(window.listener_for(&self.state, InputState::enter))
|
||||
.on_action(window.listener_for(&self.state, InputState::escape))
|
||||
.on_action(window.listener_for(&self.state, InputState::indent_inline))
|
||||
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
||||
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
||||
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
||||
.on_action(window.listener_for(&self.state, InputState::paste))
|
||||
.on_action(window.listener_for(&self.state, InputState::cut))
|
||||
.on_action(window.listener_for(&self.state, InputState::undo))
|
||||
.on_action(window.listener_for(&self.state, InputState::redo))
|
||||
.when(state.is_multi_line(), |this| {
|
||||
this.on_action(window.listener_for(&self.state, InputState::indent_inline))
|
||||
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
||||
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
||||
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
||||
})
|
||||
})
|
||||
.on_action(window.listener_for(&self.state, InputState::left))
|
||||
.on_action(window.listener_for(&self.state, InputState::right))
|
||||
|
|
@ -263,39 +263,38 @@ impl RenderOnce for TextInput {
|
|||
})
|
||||
})
|
||||
})
|
||||
.when(prefix.is_none(), |this| this.input_pl(self.size))
|
||||
.input_pr(self.size)
|
||||
.input_px(self.size)
|
||||
.items_center()
|
||||
.gap(gap_x)
|
||||
.children(prefix)
|
||||
.child(self.state.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.id("suffix")
|
||||
.absolute()
|
||||
.gap(gap_x)
|
||||
.when(self.appearance, |this| this.bg(bg))
|
||||
.items_center()
|
||||
.when(suffix.is_none(), |this| this.pr_1())
|
||||
.right_0()
|
||||
.when(state.loading, |this| {
|
||||
this.child(Indicator::new().color(cx.theme().muted_foreground))
|
||||
})
|
||||
.when(self.mask_toggle, |this| {
|
||||
this.child(Self::render_toggle_mask_button(self.state.clone()))
|
||||
})
|
||||
.when(show_clear_button, |this| {
|
||||
this.child(clear_button(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.clean(window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
.children(suffix),
|
||||
)
|
||||
.when(has_suffix, |this| {
|
||||
this.pr(self.size.input_px() / 2.).child(
|
||||
h_flex()
|
||||
.id("suffix")
|
||||
.gap(gap_x)
|
||||
.when(self.appearance, |this| this.bg(bg))
|
||||
.items_center()
|
||||
.when(state.loading, |this| {
|
||||
this.child(Indicator::new().color(cx.theme().muted_foreground))
|
||||
})
|
||||
.when(self.mask_toggle, |this| {
|
||||
this.child(Self::render_toggle_mask_button(self.state.clone()))
|
||||
})
|
||||
.when(show_clear_button, |this| {
|
||||
this.child(clear_button(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.clean(window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
.children(suffix),
|
||||
)
|
||||
})
|
||||
.refine_style(&self.style)
|
||||
.when(state.is_multi_line(), |this| {
|
||||
let entity_id = self.state.entity_id();
|
||||
if state.last_layout.is_some() {
|
||||
|
|
|
|||
|
|
@ -285,6 +285,26 @@ impl Size {
|
|||
_ => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_px(&self) -> Pixels {
|
||||
match self {
|
||||
Self::Large => px(20.),
|
||||
Self::Medium => px(12.),
|
||||
Self::Small => px(8.),
|
||||
Self::XSmall => px(4.),
|
||||
_ => px(8.),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_py(&self) -> Pixels {
|
||||
match self {
|
||||
Size::Large => px(16.),
|
||||
Size::Medium => px(8.),
|
||||
Size::Small => px(4.),
|
||||
Size::XSmall => px(0.),
|
||||
_ => px(4.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pixels> for Size {
|
||||
|
|
@ -374,40 +394,22 @@ impl<T: Styled> StyleSized<T> for T {
|
|||
|
||||
#[inline]
|
||||
fn input_pl(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::Large => self.pl_5(),
|
||||
Size::Medium => self.pl_3(),
|
||||
_ => self.pl_2(),
|
||||
}
|
||||
self.pl(size.input_px())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn input_pr(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::Large => self.pr_5(),
|
||||
Size::Medium => self.pr_3(),
|
||||
_ => self.pr_2(),
|
||||
}
|
||||
self.pr(size.input_px())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn input_px(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::Large => self.px_5(),
|
||||
Size::Medium => self.px_3(),
|
||||
_ => self.px_2(),
|
||||
}
|
||||
self.px(size.input_px())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn input_py(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::Large => self.py_5(),
|
||||
Size::Medium => self.py_2(),
|
||||
Size::Small => self.py_1(),
|
||||
Size::XSmall => self.py_0(),
|
||||
_ => self.py_1(),
|
||||
}
|
||||
self.py(size.input_py())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
|||
Loading…
Reference in a new issue