Rework ListItem
and ListHeader
to use slot-based APIs (#3635)
This PR reworks the `ListItem` and `ListHeader` components to use slot-based APIs, making them less opinionated about their contents. Splitting this out of the collab UI styling PR so we can land it to avoid conflicts. Co-authored-by: Nate <nate@zed.dev> Release Notes: - N/A
This commit is contained in:
parent
5c8257585a
commit
ee509e043d
10 changed files with 267 additions and 109 deletions
|
@ -1156,7 +1156,7 @@ impl CollabPanel {
|
||||||
let tooltip = format!("Follow {}", user.github_login);
|
let tooltip = format!("Follow {}", user.github_login);
|
||||||
|
|
||||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
ListItem::new(SharedString::from(user.github_login.clone()))
|
||||||
.left_child(Avatar::new(user.avatar_uri.clone()))
|
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
@ -1212,7 +1212,7 @@ impl CollabPanel {
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
}))
|
}))
|
||||||
.left_child(render_tree_branch(is_last, cx))
|
.start_slot(render_tree_branch(is_last, cx))
|
||||||
.child(IconButton::new(0, Icon::Folder))
|
.child(IconButton::new(0, Icon::Folder))
|
||||||
.child(Label::new(project_name.clone()))
|
.child(Label::new(project_name.clone()))
|
||||||
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
|
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
|
||||||
|
@ -1305,7 +1305,7 @@ impl CollabPanel {
|
||||||
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
|
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
|
||||||
|
|
||||||
ListItem::new(("screen", id))
|
ListItem::new(("screen", id))
|
||||||
.left_child(render_tree_branch(is_last, cx))
|
.start_slot(render_tree_branch(is_last, cx))
|
||||||
.child(IconButton::new(0, Icon::Screen))
|
.child(IconButton::new(0, Icon::Screen))
|
||||||
.child(Label::new("Screen"))
|
.child(Label::new("Screen"))
|
||||||
.when_some(peer_id, |this, _| {
|
.when_some(peer_id, |this, _| {
|
||||||
|
@ -1372,7 +1372,7 @@ impl CollabPanel {
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.open_channel_notes(channel_id, cx);
|
this.open_channel_notes(channel_id, cx);
|
||||||
}))
|
}))
|
||||||
.left_child(render_tree_branch(false, cx))
|
.start_slot(render_tree_branch(false, cx))
|
||||||
.child(IconButton::new(0, Icon::File))
|
.child(IconButton::new(0, Icon::File))
|
||||||
.child(Label::new("notes"))
|
.child(Label::new("notes"))
|
||||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||||
|
@ -1387,7 +1387,7 @@ impl CollabPanel {
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.join_channel_chat(channel_id, cx);
|
this.join_channel_chat(channel_id, cx);
|
||||||
}))
|
}))
|
||||||
.left_child(render_tree_branch(true, cx))
|
.start_slot(render_tree_branch(true, cx))
|
||||||
.child(IconButton::new(0, Icon::MessageBubbles))
|
.child(IconButton::new(0, Icon::MessageBubbles))
|
||||||
.child(Label::new("chat"))
|
.child(Label::new("chat"))
|
||||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||||
|
@ -2318,7 +2318,7 @@ impl CollabPanel {
|
||||||
} else {
|
} else {
|
||||||
el.child(
|
el.child(
|
||||||
ListHeader::new(text)
|
ListHeader::new(text)
|
||||||
.when_some(button, |el, button| el.meta(button))
|
.when_some(button, |el, button| el.end_slot(button))
|
||||||
.selected(is_selected),
|
.selected(is_selected),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2381,7 +2381,7 @@ impl CollabPanel {
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.left_child(
|
.start_slot(
|
||||||
// todo!() handle contacts with no avatar
|
// todo!() handle contacts with no avatar
|
||||||
Avatar::new(contact.user.avatar_uri.clone())
|
Avatar::new(contact.user.avatar_uri.clone())
|
||||||
.availability_indicator(if online { Some(!busy) } else { None }),
|
.availability_indicator(if online { Some(!busy) } else { None }),
|
||||||
|
@ -2460,7 +2460,7 @@ impl CollabPanel {
|
||||||
.child(Label::new(github_login.clone()))
|
.child(Label::new(github_login.clone()))
|
||||||
.child(h_stack().children(controls)),
|
.child(h_stack().children(controls)),
|
||||||
)
|
)
|
||||||
.left_avatar(user.avatar_uri.clone())
|
.start_slot::<Avatar>(user.avatar_uri.clone().map(|avatar| Avatar::new(avatar)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_contact_placeholder(
|
fn render_contact_placeholder(
|
||||||
|
@ -2568,7 +2568,11 @@ impl CollabPanel {
|
||||||
ListItem::new(channel_id as usize)
|
ListItem::new(channel_id as usize)
|
||||||
.indent_level(depth)
|
.indent_level(depth)
|
||||||
.indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle
|
.indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle
|
||||||
.left_icon(if is_public { Icon::Public } else { Icon::Hash })
|
.start_slot(
|
||||||
|
IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
.selected(is_selected || is_active)
|
.selected(is_selected || is_active)
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
|
@ -2962,7 +2966,11 @@ impl CollabPanel {
|
||||||
let item = ListItem::new("channel-editor")
|
let item = ListItem::new("channel-editor")
|
||||||
.inset(false)
|
.inset(false)
|
||||||
.indent_level(depth)
|
.indent_level(depth)
|
||||||
.left_icon(Icon::Hash);
|
.start_slot(
|
||||||
|
IconElement::new(Icon::Hash)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(pending_name) = self
|
if let Some(pending_name) = self
|
||||||
.channel_editing_state
|
.channel_editing_state
|
||||||
|
|
|
@ -271,7 +271,6 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.track_scroll(self.scroll_handle.clone())
|
.track_scroll(self.scroll_handle.clone())
|
||||||
.p_1()
|
|
||||||
)
|
)
|
||||||
.max_h_72()
|
.max_h_72()
|
||||||
.overflow_hidden(),
|
.overflow_hidden(),
|
||||||
|
|
|
@ -255,6 +255,9 @@ impl Render for ContextMenu {
|
||||||
};
|
};
|
||||||
|
|
||||||
ListItem::new(label.clone())
|
ListItem::new(label.clone())
|
||||||
|
.inset(true)
|
||||||
|
.selected(Some(ix) == self.selected_index)
|
||||||
|
.on_click(move |_, cx| handler(cx))
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
@ -265,8 +268,6 @@ impl Render for ContextMenu {
|
||||||
.map(|binding| div().ml_1().child(binding))
|
.map(|binding| div().ml_1().child(binding))
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.selected(Some(ix) == self.selected_index)
|
|
||||||
.on_click(move |_, cx| handler(cx))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label};
|
use crate::{h_stack, prelude::*, Disclosure, Label};
|
||||||
use gpui::{AnyElement, ClickEvent, Div};
|
use gpui::{AnyElement, ClickEvent, Div};
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ListHeader {
|
pub struct ListHeader {
|
||||||
|
/// The label of the header.
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
left_icon: Option<Icon>,
|
/// A slot for content that appears before the label, like an icon or avatar.
|
||||||
meta: SmallVec<[AnyElement; 2]>,
|
start_slot: Option<AnyElement>,
|
||||||
|
/// A slot for content that appears after the label, usually on the other side of the header.
|
||||||
|
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||||
|
end_slot: Option<AnyElement>,
|
||||||
|
/// A slot for content that appears on hover after the label
|
||||||
|
/// It will obscure the `end_slot` when visible.
|
||||||
|
end_hover_slot: Option<AnyElement>,
|
||||||
toggle: Option<bool>,
|
toggle: Option<bool>,
|
||||||
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
|
@ -17,8 +23,9 @@ impl ListHeader {
|
||||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
left_icon: None,
|
start_slot: None,
|
||||||
meta: SmallVec::new(),
|
end_slot: None,
|
||||||
|
end_hover_slot: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
toggle: None,
|
toggle: None,
|
||||||
on_toggle: None,
|
on_toggle: None,
|
||||||
|
@ -39,13 +46,23 @@ impl ListHeader {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self {
|
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.left_icon = left_icon.into();
|
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn meta(mut self, meta: impl IntoElement) -> Self {
|
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.meta.push(meta.into_any_element());
|
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||||
|
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inset(mut self, inset: bool) -> Self {
|
||||||
|
self.inset = inset;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,9 +78,9 @@ impl RenderOnce for ListHeader {
|
||||||
type Rendered = Div;
|
type Rendered = Div;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
h_stack().w_full().relative().child(
|
h_stack().w_full().relative().group("list_header").child(
|
||||||
div()
|
div()
|
||||||
.h_5()
|
.h_7()
|
||||||
.when(self.inset, |this| this.px_2())
|
.when(self.inset, |this| this.px_2())
|
||||||
.when(self.selected, |this| {
|
.when(self.selected, |this| {
|
||||||
this.bg(cx.theme().colors().ghost_element_selected)
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
|
@ -77,24 +94,30 @@ impl RenderOnce for ListHeader {
|
||||||
.child(
|
.child(
|
||||||
h_stack()
|
h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
.children(
|
||||||
|
self.toggle
|
||||||
|
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
.children(self.left_icon.map(|i| {
|
.children(self.start_slot)
|
||||||
IconElement::new(i)
|
|
||||||
.color(Color::Muted)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
}))
|
|
||||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||||
)
|
|
||||||
.children(
|
|
||||||
self.toggle
|
|
||||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(h_stack().gap_2().items_center().children(self.meta)),
|
.child(h_stack().children(self.end_slot))
|
||||||
|
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.invisible()
|
||||||
|
.group_hover("list_header", |this| this.visible())
|
||||||
|
.absolute()
|
||||||
|
.right_0()
|
||||||
|
.child(end_hover_slot),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize};
|
use crate::{prelude::*, Disclosure};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels,
|
px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
|
||||||
Stateful,
|
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
@ -9,11 +8,16 @@ use smallvec::SmallVec;
|
||||||
pub struct ListItem {
|
pub struct ListItem {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
// TODO: Reintroduce this
|
|
||||||
// disclosure_control_style: DisclosureControlVisibility,
|
|
||||||
indent_level: usize,
|
indent_level: usize,
|
||||||
indent_step_size: Pixels,
|
indent_step_size: Pixels,
|
||||||
left_slot: Option<AnyElement>,
|
/// A slot for content that appears before the children, like an icon or avatar.
|
||||||
|
start_slot: Option<AnyElement>,
|
||||||
|
/// A slot for content that appears after the children, usually on the other side of the header.
|
||||||
|
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||||
|
end_slot: Option<AnyElement>,
|
||||||
|
/// A slot for content that appears on hover after the children
|
||||||
|
/// It will obscure the `end_slot` when visible.
|
||||||
|
end_hover_slot: Option<AnyElement>,
|
||||||
toggle: Option<bool>,
|
toggle: Option<bool>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
|
@ -30,7 +34,9 @@ impl ListItem {
|
||||||
selected: false,
|
selected: false,
|
||||||
indent_level: 0,
|
indent_level: 0,
|
||||||
indent_step_size: px(12.),
|
indent_step_size: px(12.),
|
||||||
left_slot: None,
|
start_slot: None,
|
||||||
|
end_slot: None,
|
||||||
|
end_hover_slot: None,
|
||||||
toggle: None,
|
toggle: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
|
@ -87,23 +93,18 @@ impl ListItem {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
|
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.left_slot = Some(left_content.into_any_element());
|
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_icon(mut self, left_icon: Icon) -> Self {
|
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.left_slot = Some(
|
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||||
IconElement::new(left_icon)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Muted)
|
|
||||||
.into_any_element(),
|
|
||||||
);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
|
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||||
self.left_slot = Some(Avatar::new(left_avatar).into_any_element());
|
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,49 +126,105 @@ impl RenderOnce for ListItem {
|
||||||
type Rendered = Stateful<Div>;
|
type Rendered = Stateful<Div>;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
div()
|
h_stack()
|
||||||
.id(self.id)
|
.id("item_container")
|
||||||
|
.w_full()
|
||||||
.relative()
|
.relative()
|
||||||
// TODO: Add focus state
|
// When an item is inset draw the indent spacing outside of the item
|
||||||
// .when(self.state == InteractionState::Focused, |this| {
|
.when(self.inset, |this| {
|
||||||
// this.border()
|
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||||
// .border_color(cx.theme().colors().border_focused)
|
.px_1()
|
||||||
// })
|
|
||||||
.when(self.inset, |this| this.rounded_md())
|
|
||||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
|
||||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
|
||||||
.when(self.selected, |this| {
|
|
||||||
this.bg(cx.theme().colors().ghost_element_selected)
|
|
||||||
})
|
})
|
||||||
.when_some(self.on_click, |this, on_click| {
|
.when(!self.inset, |this| {
|
||||||
this.cursor_pointer().on_click(move |event, cx| {
|
this
|
||||||
// HACK: GPUI currently fires `on_click` with any mouse button,
|
// TODO: Add focus state
|
||||||
// but we only care about the left button.
|
// .when(self.state == InteractionState::Focused, |this| {
|
||||||
if event.down.button == MouseButton::Left {
|
// this.border()
|
||||||
(on_click)(event, cx)
|
// .border_color(cx.theme().colors().border_focused)
|
||||||
}
|
// })
|
||||||
})
|
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||||
|
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
|
||||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
|
||||||
(on_mouse_down)(event, cx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_stack()
|
||||||
.when(self.inset, |this| this.px_2())
|
.id(self.id)
|
||||||
.ml(self.indent_level as f32 * self.indent_step_size)
|
.w_full()
|
||||||
.flex()
|
|
||||||
.gap_1()
|
|
||||||
.items_center()
|
|
||||||
.relative()
|
.relative()
|
||||||
|
.gap_1()
|
||||||
|
.px_2()
|
||||||
|
.group("list_item")
|
||||||
|
.when(self.inset, |this| {
|
||||||
|
this
|
||||||
|
// TODO: Add focus state
|
||||||
|
// .when(self.state == InteractionState::Focused, |this| {
|
||||||
|
// this.border()
|
||||||
|
// .border_color(cx.theme().colors().border_focused)
|
||||||
|
// })
|
||||||
|
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||||
|
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.when_some(self.on_click, |this, on_click| {
|
||||||
|
this.cursor_pointer().on_click(move |event, cx| {
|
||||||
|
// HACK: GPUI currently fires `on_click` with any mouse button,
|
||||||
|
// but we only care about the left button.
|
||||||
|
if event.down.button == MouseButton::Left {
|
||||||
|
(on_click)(event, cx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||||
|
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||||
|
(on_mouse_down)(event, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
||||||
|
.map(|this| {
|
||||||
|
if self.inset {
|
||||||
|
this.rounded_md()
|
||||||
|
} else {
|
||||||
|
// When an item is not inset draw the indent spacing inside of the item
|
||||||
|
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||||
|
}
|
||||||
|
})
|
||||||
.children(
|
.children(
|
||||||
self.toggle
|
self.toggle
|
||||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||||
)
|
)
|
||||||
.children(self.left_slot)
|
.child(
|
||||||
.children(self.children),
|
h_stack()
|
||||||
|
.flex_1()
|
||||||
|
.gap_1()
|
||||||
|
.children(self.start_slot)
|
||||||
|
.children(self.children),
|
||||||
|
)
|
||||||
|
.when_some(self.end_slot, |this, end_slot| {
|
||||||
|
this.justify_between().child(
|
||||||
|
h_stack()
|
||||||
|
.when(self.end_hover_slot.is_some(), |this| {
|
||||||
|
this.visible()
|
||||||
|
.group_hover("list_item", |this| this.invisible())
|
||||||
|
})
|
||||||
|
.child(end_slot),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||||
|
this.child(
|
||||||
|
h_stack()
|
||||||
|
.h_full()
|
||||||
|
.absolute()
|
||||||
|
.right_2()
|
||||||
|
.top_0()
|
||||||
|
.invisible()
|
||||||
|
.group_hover("list_item", |this| this.visible())
|
||||||
|
.child(end_hover_slot),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,10 @@ impl RenderOnce for ListSeparator {
|
||||||
type Rendered = Div;
|
type Rendered = Div;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
div().h_px().w_full().bg(cx.theme().colors().border_variant)
|
div()
|
||||||
|
.h_px()
|
||||||
|
.w_full()
|
||||||
|
.my_1()
|
||||||
|
.bg(cx.theme().colors().border_variant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{h_stack, Icon, IconElement, IconSize, Label};
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ListSubHeader {
|
pub struct ListSubHeader {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
left_icon: Option<Icon>,
|
start_slot: Option<Icon>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,13 +14,13 @@ impl ListSubHeader {
|
||||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
left_icon: None,
|
start_slot: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||||
self.left_icon = left_icon;
|
self.start_slot = left_icon;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ impl RenderOnce for ListSubHeader {
|
||||||
.flex()
|
.flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
.children(self.left_icon.map(|i| {
|
.children(self.start_slot.map(|i| {
|
||||||
IconElement::new(i)
|
IconElement::new(i)
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
|
|
|
@ -15,19 +15,19 @@ impl Render for ListHeaderStory {
|
||||||
.child(Story::label("Default"))
|
.child(Story::label("Default"))
|
||||||
.child(ListHeader::new("Section 1"))
|
.child(ListHeader::new("Section 1"))
|
||||||
.child(Story::label("With left icon"))
|
.child(Story::label("With left icon"))
|
||||||
.child(ListHeader::new("Section 2").left_icon(Icon::Bell))
|
.child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
|
||||||
.child(Story::label("With left icon and meta"))
|
.child(Story::label("With left icon and meta"))
|
||||||
.child(
|
.child(
|
||||||
ListHeader::new("Section 3")
|
ListHeader::new("Section 3")
|
||||||
.left_icon(Icon::BellOff)
|
.start_slot(IconElement::new(Icon::BellOff))
|
||||||
.meta(IconButton::new("action_1", Icon::Bolt)),
|
.end_slot(IconButton::new("action_1", Icon::Bolt)),
|
||||||
)
|
)
|
||||||
.child(Story::label("With multiple meta"))
|
.child(Story::label("With multiple meta"))
|
||||||
.child(
|
.child(
|
||||||
ListHeader::new("Section 4")
|
ListHeader::new("Section 4")
|
||||||
.meta(IconButton::new("action_1", Icon::Bolt))
|
.end_slot(IconButton::new("action_1", Icon::Bolt))
|
||||||
.meta(IconButton::new("action_2", Icon::ExclamationTriangle))
|
.end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
|
||||||
.meta(IconButton::new("action_3", Icon::Plus)),
|
.end_slot(IconButton::new("action_3", Icon::Plus)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use gpui::{Div, Render};
|
use gpui::{Div, Render};
|
||||||
use story::Story;
|
use story::Story;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::{prelude::*, Avatar};
|
||||||
use crate::{Icon, ListItem};
|
use crate::{Icon, ListItem};
|
||||||
|
|
||||||
pub struct ListItemStory;
|
pub struct ListItemStory;
|
||||||
|
@ -9,24 +9,80 @@ pub struct ListItemStory;
|
||||||
impl Render for ListItemStory {
|
impl Render for ListItemStory {
|
||||||
type Element = Div;
|
type Element = Div;
|
||||||
|
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
Story::container()
|
Story::container()
|
||||||
|
.bg(cx.theme().colors().background)
|
||||||
.child(Story::title_for::<ListItem>())
|
.child(Story::title_for::<ListItem>())
|
||||||
.child(Story::label("Default"))
|
.child(Story::label("Default"))
|
||||||
.child(ListItem::new("hello_world").child("Hello, world!"))
|
.child(ListItem::new("hello_world").child("Hello, world!"))
|
||||||
.child(Story::label("With left icon"))
|
.child(Story::label("Inset"))
|
||||||
.child(
|
.child(
|
||||||
ListItem::new("with_left_icon")
|
ListItem::new("hello_world")
|
||||||
|
.inset(true)
|
||||||
|
.start_slot(
|
||||||
|
IconElement::new(Icon::Bell)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
.child("Hello, world!")
|
.child("Hello, world!")
|
||||||
.left_icon(Icon::Bell),
|
.end_slot(
|
||||||
|
IconElement::new(Icon::Bell)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(Story::label("With left avatar"))
|
.child(Story::label("With start slot icon"))
|
||||||
|
.child(
|
||||||
|
ListItem::new("with start slot_icon")
|
||||||
|
.child("Hello, world!")
|
||||||
|
.start_slot(
|
||||||
|
IconElement::new(Icon::Bell)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(Story::label("With start slot avatar"))
|
||||||
|
.child(
|
||||||
|
ListItem::new("with_start slot avatar")
|
||||||
|
.child("Hello, world!")
|
||||||
|
.start_slot(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.child(Story::label("With end slot"))
|
||||||
.child(
|
.child(
|
||||||
ListItem::new("with_left_avatar")
|
ListItem::new("with_left_avatar")
|
||||||
.child("Hello, world!")
|
.child("Hello, world!")
|
||||||
.left_avatar(SharedString::from(
|
.end_slot(Avatar::new(SharedString::from(
|
||||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
)),
|
))),
|
||||||
|
)
|
||||||
|
.child(Story::label("With end hover slot"))
|
||||||
|
.child(
|
||||||
|
ListItem::new("with_left_avatar")
|
||||||
|
.child("Hello, world!")
|
||||||
|
.end_slot(
|
||||||
|
h_stack()
|
||||||
|
.gap_2()
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
)))
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
)))
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
)))
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
)))
|
||||||
|
.child(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.end_hover_slot(Avatar::new(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
|
))),
|
||||||
)
|
)
|
||||||
.child(Story::label("With `on_click`"))
|
.child(Story::label("With `on_click`"))
|
||||||
.child(
|
.child(
|
||||||
|
|
|
@ -118,16 +118,26 @@ pub trait StyledExt: Styled + Sized {
|
||||||
elevated(self, cx, ElevationIndex::ModalSurface)
|
elevated(self, cx, ElevationIndex::ModalSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The theme's primary border color.
|
||||||
|
fn border_primary(self, cx: &mut WindowContext) -> Self {
|
||||||
|
self.border_color(cx.theme().colors().border)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The theme's secondary or muted border color.
|
||||||
|
fn border_muted(self, cx: &mut WindowContext) -> Self {
|
||||||
|
self.border_color(cx.theme().colors().border_variant)
|
||||||
|
}
|
||||||
|
|
||||||
fn debug_bg_red(self) -> Self {
|
fn debug_bg_red(self) -> Self {
|
||||||
self.bg(gpui::red())
|
self.bg(hsla(0. / 360., 1., 0.5, 1.))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_bg_green(self) -> Self {
|
fn debug_bg_green(self) -> Self {
|
||||||
self.bg(gpui::green())
|
self.bg(hsla(120. / 360., 1., 0.5, 1.))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_bg_blue(self) -> Self {
|
fn debug_bg_blue(self) -> Self {
|
||||||
self.bg(gpui::blue())
|
self.bg(hsla(240. / 360., 1., 0.5, 1.))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_bg_yellow(self) -> Self {
|
fn debug_bg_yellow(self) -> Self {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue