diff --git a/README.md b/README.md index 6d603239..f78ae4cc 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,11 @@ This is an example of build app by using GPUI. - [ ] ContextMenu to let user copy, cut, paste - [ ] Selection by mouse, drag to select text - [x] Cursor blinking + - [ ] Textarea + - [ ] Input icon - [x] Button - [x] Button with Icon - - [ ] IconButton - - [ ] ToggleButton + - [x] IconButton - [x] Label - [x] Icon - [x] Checkbox diff --git a/crates/ui-story/src/button_story.rs b/crates/ui-story/src/button_story.rs index 52d4124e..db0b877c 100644 --- a/crates/ui-story/src/button_story.rs +++ b/crates/ui-story/src/button_story.rs @@ -8,6 +8,8 @@ use ui::{ h_flex, v_flex, Clickable, Disableable as _, Icon, IconName, Selectable, }; +use crate::section; + pub struct ButtonStory {} impl ButtonStory { @@ -22,23 +24,6 @@ impl ButtonStory { impl Render for ButtonStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - fn section(title: impl Into, cx: &ViewContext) -> Div { - use ui::theme::ActiveTheme; - let theme = cx.theme(); - - h_flex() - .items_center() - .gap_4() - .p_4() - .w_full() - .rounded_lg() - .border_1() - .border_color(theme.border) - .flex_wrap() - .justify_around() - .child(div().flex_none().w_full().child(title.into())) - } - v_flex() .w_full() .justify_start() diff --git a/crates/ui-story/src/input_story.rs b/crates/ui-story/src/input_story.rs index 9a71a7bb..070dab65 100644 --- a/crates/ui-story/src/input_story.rs +++ b/crates/ui-story/src/input_story.rs @@ -3,13 +3,18 @@ use gpui::{ VisualContext, WindowContext, }; -use ui::{input::TextInput, v_flex}; +use ui::{input::TextInput, v_flex, Icon, IconName}; + +use crate::section; pub struct InputStory { input1: View, input2: View, mash_input: View, disabled_input: View, + prefix_input1: View, + suffix_input1: View, + both_input1: View, } impl InputStory { @@ -31,6 +36,23 @@ impl InputStory { input }); + let prefix_input1 = cx.new_view(|cx| { + TextInput::new(cx) + .prefix(IconName::Search.view(cx)) + .placeholder("Search some thing...") + }); + let suffix_input1 = cx.new_view(|cx| { + TextInput::new(cx) + .suffix(IconName::Info.view(cx)) + .placeholder("Info here...") + }); + let both_input1 = cx.new_view(|cx| { + TextInput::new(cx) + .prefix(IconName::Search.view(cx)) + .suffix(IconName::Info.view(cx)) + .placeholder("This input have prefix and suffix.") + }); + Self { input1, input2: cx.new_view(|cx| { @@ -45,6 +67,9 @@ impl InputStory { input.set_disabled(true, cx); input }), + prefix_input1, + suffix_input1, + both_input1, } } @@ -55,14 +80,26 @@ impl InputStory { } impl Render for InputStory { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() .size_full() .justify_start() .gap_3() - .child(self.input1.clone()) - .child(self.input2.clone()) - .child(self.disabled_input.clone()) - .child(self.mash_input.clone()) + .child( + section("Normal Input", cx) + .child(self.input1.clone()) + .child(self.input2.clone()), + ) + .child( + section("Input State", cx) + .child(self.disabled_input.clone()) + .child(self.mash_input.clone()), + ) + .child( + section("Preifx and Suffix", cx) + .child(self.prefix_input1.clone()) + .child(self.both_input1.clone()) + .child(self.suffix_input1.clone()), + ) } } diff --git a/crates/ui-story/src/lib.rs b/crates/ui-story/src/lib.rs index c6bc6d7d..349ce5d8 100644 --- a/crates/ui-story/src/lib.rs +++ b/crates/ui-story/src/lib.rs @@ -19,7 +19,7 @@ pub use switch_story::SwitchStory; pub use tooltip_story::TooltipStory; use gpui::{ - div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, EventEmitter, + div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, EventEmitter, FocusableView, IntoElement, ParentElement, Render, SharedString, Styled as _, Task, View, ViewContext, VisualContext, WindowContext, }; @@ -30,7 +30,7 @@ use workspace::{ }; use anyhow::Result; -use ui::{divider::Divider, label::Label, v_flex}; +use ui::{divider::Divider, h_flex, label::Label, v_flex}; pub fn story_case( name: &'static str, @@ -40,6 +40,23 @@ pub fn story_case( StoryContainer::new(name, description, cx) } +pub fn section(title: impl Into, cx: &WindowContext) -> Div { + use ui::theme::ActiveTheme; + let theme = cx.theme(); + + h_flex() + .items_center() + .gap_4() + .p_4() + .w_full() + .rounded_lg() + .border_1() + .border_color(theme.border) + .flex_wrap() + .justify_around() + .child(div().flex_none().w_full().child(title.into())) +} + pub struct StoryContainer { focus_handle: gpui::FocusHandle, name: SharedString, diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 2e9f7ea0..84daf198 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -1,7 +1,9 @@ +use std::ops::Deref; + use crate::{button::ButtonSize, theme::ActiveTheme}; use gpui::{ - prelude::FluentBuilder as _, svg, AnyElement, Hsla, IntoElement, RenderOnce, SharedString, - StyleRefinement, Styled, Svg, WindowContext, + prelude::FluentBuilder as _, svg, AnyElement, AnyView, Hsla, IntoElement, Render, RenderOnce, + SharedString, StyleRefinement, Styled, Svg, View, VisualContext, WindowContext, }; #[derive(IntoElement)] @@ -44,6 +46,11 @@ impl IconName { } .into() } + + /// Return the icon as a View + pub fn view(self, cx: &mut WindowContext) -> View { + Icon::new(self).view(cx) + } } impl From for Icon { @@ -52,6 +59,12 @@ impl From for Icon { } } +impl From for AnyElement { + fn from(val: IconName) -> Self { + Icon::new(val).into_any_element() + } +} + impl RenderOnce for IconName { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { Icon::new(self) @@ -89,6 +102,11 @@ impl Icon { self } + + /// Create a new view for the icon + pub fn view(self, cx: &mut WindowContext) -> View { + cx.new_view(|_| self) + } } impl Styled for Icon { @@ -122,3 +140,20 @@ impl From for AnyElement { val.into_any_element() } } + +impl Render for Icon { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { + let text_color = self.text_color.unwrap_or_else(|| cx.theme().foreground); + + svg() + .flex_none() + .size_4() + .text_color(text_color) + .map(|this| match self.size { + ButtonSize::XSmall => this.size_3(), + ButtonSize::Small => this.size_3p5(), + ButtonSize::Medium => this.size_4(), + }) + .path(self.path.clone()) + } +} diff --git a/crates/ui/src/input.rs b/crates/ui/src/input.rs index cd268c9e..7680566b 100644 --- a/crates/ui/src/input.rs +++ b/crates/ui/src/input.rs @@ -73,6 +73,8 @@ pub fn init(cx: &mut AppContext) { pub struct TextInput { focus_handle: FocusHandle, text: SharedString, + prefix: Option, + suffix: Option, placeholder: SharedString, blink_cursor: Model, selected_range: Range, @@ -102,6 +104,8 @@ impl TextInput { disabled: false, masked: false, appearance: true, + prefix: None, + suffix: None, }; // Observe the blink cursor to repaint the view when it changes. @@ -158,6 +162,24 @@ impl TextInput { self } + /// Set the prefix element of the input field, for example a search Icon. + pub fn prefix(mut self, prefix: impl Into) -> Self { + self.prefix = Some(prefix.into()); + self + } + + /// Set the placeholder text of the input field. + pub fn placeholder(mut self, placeholder: impl Into) -> Self { + self.placeholder = placeholder.into(); + self + } + + /// Set the suffix element of the input field, for example a clear button. + pub fn suffix(mut self, suffix: impl Into) -> Self { + self.suffix = Some(suffix.into()); + self + } + /// Return the text of the input field. pub fn text(&self) -> SharedString { self.text.clone() @@ -609,6 +631,7 @@ impl Element for TextElement { impl Render for TextInput { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let focused = self.focus_handle.is_focused(cx); + div() .flex() .key_context("TextInput") @@ -654,8 +677,12 @@ impl Render for TextInput { cx.theme().background }) }) - .child(div().w_full().child(TextElement { + .when_some(self.prefix.clone(), |this, prefix| this.child(prefix)) + .gap_1() + .items_center() + .child(div().flex_grow().overflow_x_hidden().child(TextElement { input: cx.view().clone(), })) + .when_some(self.suffix.clone(), |this, suffix| this.child(suffix)) } }