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)),