diff --git a/assets/icons/eye-off.svg b/assets/icons/eye-off.svg new file mode 100644 index 00000000..abf12804 --- /dev/null +++ b/assets/icons/eye-off.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg new file mode 100644 index 00000000..4b3a970b --- /dev/null +++ b/assets/icons/eye.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/crates/story/src/button_story.rs b/crates/story/src/button_story.rs index 38e2baf0..3f03fd14 100644 --- a/crates/story/src/button_story.rs +++ b/crates/story/src/button_story.rs @@ -84,6 +84,19 @@ impl Render for ButtonStory { .icon(Icon::new(IconName::Maximize)) .style(ButtonStyle::Secondary) .on_click(Self::on_click), + ) + .child( + Button::new("button-icon-4", cx) + .style(ButtonStyle::Secondary) + .child( + h_flex() + .items_center() + .gap_2() + .child("Custom Child") + .child(IconName::ChevronDown) + .child(IconName::Eye), + ) + .on_click(Self::on_click), ), ), ) diff --git a/crates/story/src/checkbox_story.rs b/crates/story/src/checkbox_story.rs index cd688a94..1e1f0f17 100644 --- a/crates/story/src/checkbox_story.rs +++ b/crates/story/src/checkbox_story.rs @@ -3,7 +3,14 @@ use gpui::{ WindowContext, }; -use ui::{checkbox::Checkbox, h_flex, radio::Radio, v_flex, Disableable as _, Selection}; +use ui::{ + button::{Button, ButtonStyle}, + checkbox::Checkbox, + h_flex, + label::Label, + radio::Radio, + v_flex, Clickable, Disableable as _, IconName, Selection, +}; pub struct CheckboxStory { check1: Selection, @@ -11,6 +18,7 @@ pub struct CheckboxStory { check3: Selection, select1: bool, select2: bool, + masked: bool, } impl CheckboxStory { @@ -21,6 +29,7 @@ impl CheckboxStory { check3: Selection::Selected, select1: false, select2: true, + masked: false, } } @@ -39,6 +48,25 @@ impl Render for CheckboxStory { v_flex() .p_4() .gap_6() + .child( + h_flex() + .items_center() + .gap_1() + .child(Label::new("9,182,1 USD").text_2xl().masked(self.masked)) + .child( + Button::new("btn-mask", cx) + .style(ButtonStyle::Ghost) + .icon(if self.masked { + IconName::EyeOff + } else { + IconName::Eye + }) + .on_click(cx.listener(|this, _, _| { + this.masked = !this.masked; + })), + ), + ) + .child(Label::new("500 USD").text_xl().masked(self.masked)) .child( v_flex().items_start().justify_start().gap_6().child( h_flex() diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index dd0c7c2c..c9f35f40 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -5,9 +5,9 @@ use crate::{ Clickable, Disableable, Icon, Selectable, Size, StyledExt, }; use gpui::{ - div, prelude::FluentBuilder as _, px, ClickEvent, DefiniteLength, Div, ElementId, FocusHandle, - Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, - SharedString, StatefulInteractiveElement as _, Styled, WindowContext, + div, prelude::FluentBuilder as _, px, AnyElement, ClickEvent, DefiniteLength, Div, ElementId, + FocusHandle, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, + RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext, }; pub enum ButtonRounded { @@ -40,6 +40,7 @@ pub struct Button { focus_handle: FocusHandle, icon: Option, label: Option, + children: Vec, disabled: bool, selected: bool, width: Option, @@ -70,6 +71,7 @@ impl Button { tooltip: None, on_click: None, loading: false, + children: Vec::new(), } } @@ -171,6 +173,12 @@ impl Styled for Button { } } +impl ParentElement for Button { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + impl RenderOnce for Button { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let style: ButtonStyle = self.style; @@ -186,7 +194,7 @@ impl RenderOnce for Button { .when_some(self.width, |this, width| this.w(width)) .when_some(self.height, |this, height| this.h(height)) .map(|this| { - if self.label.is_none() { + if self.label.is_none() && self.children.is_empty() { // Icon Button match self.size { Size::Size(px) => this.size(px), @@ -268,6 +276,7 @@ impl RenderOnce for Button { this.child(Indicator::new().size(self.size).color(text_color)) }) .when_some(self.label, |this, label| this.child(label)) + .children(self.children) .map(|this| match self.size { Size::XSmall => this.text_xs(), Size::Small => this.text_sm(), diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 1bb563b7..de3323e6 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -27,6 +27,8 @@ pub enum IconName { ArrowRight, ChevronLeft, ChevronRight, + Eye, + EyeOff, } impl IconName { @@ -53,6 +55,8 @@ impl IconName { IconName::ArrowRight => "icons/arrow-right.svg", IconName::ChevronLeft => "icons/chevron-left.svg", IconName::ChevronRight => "icons/chevron-right.svg", + IconName::Eye => "icons/eye.svg", + IconName::EyeOff => "icons/eye-off.svg", } .into() } diff --git a/crates/ui/src/label.rs b/crates/ui/src/label.rs index 823e4048..49382099 100644 --- a/crates/ui/src/label.rs +++ b/crates/ui/src/label.rs @@ -19,6 +19,7 @@ pub struct Label { label: SharedString, multiple_lines: bool, align: TextAlign, + marked: bool, } impl Label { @@ -28,6 +29,7 @@ impl Label { label: label.into(), multiple_lines: true, align: TextAlign::default(), + marked: false, } } @@ -40,6 +42,11 @@ impl Label { self.align = align; self } + + pub fn masked(mut self, masked: bool) -> Self { + self.marked = masked; + self + } } impl Styled for Label { @@ -56,6 +63,12 @@ impl RenderOnce for Label { self.label }; + let text_display = if self.marked { + "*".repeat(text.chars().count()) + } else { + text.to_string() + }; + h_flex() .map(|this| match self.align { TextAlign::Left => this.justify_start(), @@ -63,6 +76,6 @@ impl RenderOnce for Label { TextAlign::Right => this.justify_end(), }) .text_color(cx.theme().foreground) - .child(self.base.child(text)) + .child(self.base.child(text_display)) } } diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index 588fc353..330c3d64 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -60,7 +60,7 @@ enum PopupMode { pub struct Popover { id: ElementId, anchor: AnchorCorner, - trigger: Option AnyElement + 'static>>, + trigger: Option AnyElement + 'static>>, content: Option View + 'static>>, mouse_button: MouseButton, mode: PopupMode, @@ -103,7 +103,9 @@ where where T: Selectable + IntoElement + 'static, { - self.trigger = Some(Box::new(|_| trigger.into_any_element())); + self.trigger = Some(Box::new(|is_open, _| { + trigger.selected(is_open).into_any_element() + })); self } @@ -118,7 +120,7 @@ where self } - fn render_trigger(&mut self, cx: &mut WindowContext) -> impl IntoElement { + fn render_trigger(&mut self, is_open: bool, cx: &mut WindowContext) -> impl IntoElement { let base = div().id("popover-trigger"); if self.trigger.is_none() { @@ -127,7 +129,7 @@ where let trigger = self.trigger.take().unwrap(); - base.child((trigger)(cx)).into_element() + base.child((trigger)(is_open, cx)).into_element() } fn resolved_corner(&self, bounds: Bounds) -> Point { @@ -214,13 +216,13 @@ impl Element for Popover { let mode = self.mode; self.with_element_state(id.unwrap(), cx, |view, element_state, cx| { - let mut trigger_element = view.render_trigger(cx).into_any_element(); - let trigger_layout_id = trigger_element.request_layout(cx); - let mut popover_layout_id = None; let mut popover_element = None; + let mut is_open = false; if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() { + is_open = true; + let mut anchored = anchored().snap_to_window().anchor(view.anchor); if let Some(trigger_bounds) = element_state.trigger_bounds { anchored = anchored.position(view.resolved_corner(trigger_bounds)); @@ -270,6 +272,9 @@ impl Element for Popover { popover_element = Some(element); } + let mut trigger_element = view.render_trigger(is_open, cx).into_any_element(); + let trigger_layout_id = trigger_element.request_layout(cx); + let layout_id = cx.request_layout( Style::default(), popover_layout_id.into_iter().chain(Some(trigger_layout_id)),