diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index 88659ce0..e011db86 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -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(), - ), - ) } } diff --git a/crates/story/src/number_input_story.rs b/crates/story/src/number_input_story.rs index 7b88d887..31ae38a3 100644 --- a/crates/story/src/number_input_story.rs +++ b/crates/story/src/number_input_story.rs @@ -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( diff --git a/crates/ui/src/input/blink_cursor.rs b/crates/ui/src/input/blink_cursor.rs index eb565733..18bc8979 100644 --- a/crates/ui/src/input/blink_cursor.rs +++ b/crates/ui/src/input/blink_cursor.rs @@ -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. /// diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index f7fc2d71..e6bdc8f4 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -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() diff --git a/crates/ui/src/input/number_input.rs b/crates/ui/src/input/number_input.rs index 0cdd24d7..47169fb6 100644 --- a/crates/ui/src/input/number_input.rs +++ b/crates/ui/src/input/number_input.rs @@ -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| { diff --git a/crates/ui/src/input/text_input.rs b/crates/ui/src/input/text_input.rs index 26dd429b..3391b269 100644 --- a/crates/ui/src/input/text_input.rs +++ b/crates/ui/src/input/text_input.rs @@ -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, + style: StyleRefinement, size: Size, - no_gap: bool, prefix: Option, suffix: Option, height: Option, @@ -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) -> 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() { diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 8f5f6b8a..05be1c96 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -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 for Size { @@ -374,40 +394,22 @@ impl StyleSized 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]