Add to masked Label and support children to Button (#50)

https://github.com/user-attachments/assets/9583ce15-b801-45f9-aafb-54f0ecdf7dc9

- Add masked mode to Label.
- Add to support any children to Button.
- Fix Popover to support selected state style.
This commit is contained in:
Jason Lee 2024-07-19 18:54:16 +08:00 committed by GitHub
parent a69d30be6a
commit e54d1f600b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 95 additions and 13 deletions

6
assets/icons/eye-off.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-off">
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/>
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/>
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/>
<path d="m2 2 20 20"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

4
assets/icons/eye.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/>
<circle cx="12" cy="12" r="3"/>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View file

@ -84,6 +84,19 @@ impl Render for ButtonStory {
.icon(Icon::new(IconName::Maximize)) .icon(Icon::new(IconName::Maximize))
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.on_click(Self::on_click), .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),
), ),
), ),
) )

View file

@ -3,7 +3,14 @@ use gpui::{
WindowContext, 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 { pub struct CheckboxStory {
check1: Selection, check1: Selection,
@ -11,6 +18,7 @@ pub struct CheckboxStory {
check3: Selection, check3: Selection,
select1: bool, select1: bool,
select2: bool, select2: bool,
masked: bool,
} }
impl CheckboxStory { impl CheckboxStory {
@ -21,6 +29,7 @@ impl CheckboxStory {
check3: Selection::Selected, check3: Selection::Selected,
select1: false, select1: false,
select2: true, select2: true,
masked: false,
} }
} }
@ -39,6 +48,25 @@ impl Render for CheckboxStory {
v_flex() v_flex()
.p_4() .p_4()
.gap_6() .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( .child(
v_flex().items_start().justify_start().gap_6().child( v_flex().items_start().justify_start().gap_6().child(
h_flex() h_flex()

View file

@ -5,9 +5,9 @@ use crate::{
Clickable, Disableable, Icon, Selectable, Size, StyledExt, Clickable, Disableable, Icon, Selectable, Size, StyledExt,
}; };
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, ClickEvent, DefiniteLength, Div, ElementId, FocusHandle, div, prelude::FluentBuilder as _, px, AnyElement, ClickEvent, DefiniteLength, Div, ElementId,
Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, FocusHandle, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
SharedString, StatefulInteractiveElement as _, Styled, WindowContext, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext,
}; };
pub enum ButtonRounded { pub enum ButtonRounded {
@ -40,6 +40,7 @@ pub struct Button {
focus_handle: FocusHandle, focus_handle: FocusHandle,
icon: Option<Icon>, icon: Option<Icon>,
label: Option<SharedString>, label: Option<SharedString>,
children: Vec<AnyElement>,
disabled: bool, disabled: bool,
selected: bool, selected: bool,
width: Option<DefiniteLength>, width: Option<DefiniteLength>,
@ -70,6 +71,7 @@ impl Button {
tooltip: None, tooltip: None,
on_click: None, on_click: None,
loading: false, loading: false,
children: Vec::new(),
} }
} }
@ -171,6 +173,12 @@ impl Styled for Button {
} }
} }
impl ParentElement for Button {
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for Button { impl RenderOnce for Button {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let style: ButtonStyle = self.style; 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.width, |this, width| this.w(width))
.when_some(self.height, |this, height| this.h(height)) .when_some(self.height, |this, height| this.h(height))
.map(|this| { .map(|this| {
if self.label.is_none() { if self.label.is_none() && self.children.is_empty() {
// Icon Button // Icon Button
match self.size { match self.size {
Size::Size(px) => this.size(px), Size::Size(px) => this.size(px),
@ -268,6 +276,7 @@ impl RenderOnce for Button {
this.child(Indicator::new().size(self.size).color(text_color)) this.child(Indicator::new().size(self.size).color(text_color))
}) })
.when_some(self.label, |this, label| this.child(label)) .when_some(self.label, |this, label| this.child(label))
.children(self.children)
.map(|this| match self.size { .map(|this| match self.size {
Size::XSmall => this.text_xs(), Size::XSmall => this.text_xs(),
Size::Small => this.text_sm(), Size::Small => this.text_sm(),

View file

@ -27,6 +27,8 @@ pub enum IconName {
ArrowRight, ArrowRight,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Eye,
EyeOff,
} }
impl IconName { impl IconName {
@ -53,6 +55,8 @@ impl IconName {
IconName::ArrowRight => "icons/arrow-right.svg", IconName::ArrowRight => "icons/arrow-right.svg",
IconName::ChevronLeft => "icons/chevron-left.svg", IconName::ChevronLeft => "icons/chevron-left.svg",
IconName::ChevronRight => "icons/chevron-right.svg", IconName::ChevronRight => "icons/chevron-right.svg",
IconName::Eye => "icons/eye.svg",
IconName::EyeOff => "icons/eye-off.svg",
} }
.into() .into()
} }

View file

@ -19,6 +19,7 @@ pub struct Label {
label: SharedString, label: SharedString,
multiple_lines: bool, multiple_lines: bool,
align: TextAlign, align: TextAlign,
marked: bool,
} }
impl Label { impl Label {
@ -28,6 +29,7 @@ impl Label {
label: label.into(), label: label.into(),
multiple_lines: true, multiple_lines: true,
align: TextAlign::default(), align: TextAlign::default(),
marked: false,
} }
} }
@ -40,6 +42,11 @@ impl Label {
self.align = align; self.align = align;
self self
} }
pub fn masked(mut self, masked: bool) -> Self {
self.marked = masked;
self
}
} }
impl Styled for Label { impl Styled for Label {
@ -56,6 +63,12 @@ impl RenderOnce for Label {
self.label self.label
}; };
let text_display = if self.marked {
"*".repeat(text.chars().count())
} else {
text.to_string()
};
h_flex() h_flex()
.map(|this| match self.align { .map(|this| match self.align {
TextAlign::Left => this.justify_start(), TextAlign::Left => this.justify_start(),
@ -63,6 +76,6 @@ impl RenderOnce for Label {
TextAlign::Right => this.justify_end(), TextAlign::Right => this.justify_end(),
}) })
.text_color(cx.theme().foreground) .text_color(cx.theme().foreground)
.child(self.base.child(text)) .child(self.base.child(text_display))
} }
} }

View file

@ -60,7 +60,7 @@ enum PopupMode {
pub struct Popover<M: ManagedView> { pub struct Popover<M: ManagedView> {
id: ElementId, id: ElementId,
anchor: AnchorCorner, anchor: AnchorCorner,
trigger: Option<Box<dyn FnOnce(&WindowContext) -> AnyElement + 'static>>, trigger: Option<Box<dyn FnOnce(bool, &WindowContext) -> AnyElement + 'static>>,
content: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>, content: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
mouse_button: MouseButton, mouse_button: MouseButton,
mode: PopupMode, mode: PopupMode,
@ -103,7 +103,9 @@ where
where where
T: Selectable + IntoElement + 'static, 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 self
} }
@ -118,7 +120,7 @@ where
self 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"); let base = div().id("popover-trigger");
if self.trigger.is_none() { if self.trigger.is_none() {
@ -127,7 +129,7 @@ where
let trigger = self.trigger.take().unwrap(); 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<Pixels>) -> Point<Pixels> { fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
@ -214,13 +216,13 @@ impl<M: ManagedView> Element for Popover<M> {
let mode = self.mode; let mode = self.mode;
self.with_element_state(id.unwrap(), cx, |view, element_state, cx| { 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_layout_id = None;
let mut popover_element = None; let mut popover_element = None;
let mut is_open = false;
if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() { 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); let mut anchored = anchored().snap_to_window().anchor(view.anchor);
if let Some(trigger_bounds) = element_state.trigger_bounds { if let Some(trigger_bounds) = element_state.trigger_bounds {
anchored = anchored.position(view.resolved_corner(trigger_bounds)); anchored = anchored.position(view.resolved_corner(trigger_bounds));
@ -270,6 +272,9 @@ impl<M: ManagedView> Element for Popover<M> {
popover_element = Some(element); 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( let layout_id = cx.request_layout(
Style::default(), Style::default(),
popover_layout_id.into_iter().chain(Some(trigger_layout_id)), popover_layout_id.into_iter().chain(Some(trigger_layout_id)),