mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-16 15:11:25 +00:00
Rework ListHeader
to be more open (#3467)
This PR reworks the `ListHeader` component to be more open. The `meta` method can now be used to append meta items of any element to the `ListHeader`, and they will be rendered with the appropriate spacing between them. Release Notes: - N/A
This commit is contained in:
parent
bd6fa66a7c
commit
e5a5b1e84c
9 changed files with 112 additions and 99 deletions
|
@ -2511,7 +2511,7 @@ impl CollabPanel {
|
||||||
} else {
|
} else {
|
||||||
el.child(
|
el.child(
|
||||||
ListHeader::new(text)
|
ListHeader::new(text)
|
||||||
.when_some(button, |el, button| el.right_button(button))
|
.when_some(button, |el, button| el.meta(button))
|
||||||
.selected(is_selected),
|
.selected(is_selected),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ pub enum ComponentStory {
|
||||||
Keybinding,
|
Keybinding,
|
||||||
Label,
|
Label,
|
||||||
List,
|
List,
|
||||||
|
ListHeader,
|
||||||
ListItem,
|
ListItem,
|
||||||
Scroll,
|
Scroll,
|
||||||
Text,
|
Text,
|
||||||
|
@ -44,6 +45,7 @@ impl ComponentStory {
|
||||||
Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
|
Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
|
||||||
Self::Label => cx.build_view(|_| ui::LabelStory).into(),
|
Self::Label => cx.build_view(|_| ui::LabelStory).into(),
|
||||||
Self::List => cx.build_view(|_| ui::ListStory).into(),
|
Self::List => cx.build_view(|_| ui::ListStory).into(),
|
||||||
|
Self::ListHeader => cx.build_view(|_| ui::ListHeaderStory).into(),
|
||||||
Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
|
Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
|
||||||
Self::Scroll => ScrollStory::view(cx).into(),
|
Self::Scroll => ScrollStory::view(cx).into(),
|
||||||
Self::Text => TextStory::view(cx).into(),
|
Self::Text => TextStory::view(cx).into(),
|
||||||
|
|
|
@ -1,73 +1,11 @@
|
||||||
|
mod list;
|
||||||
mod list_header;
|
mod list_header;
|
||||||
mod list_item;
|
mod list_item;
|
||||||
mod list_separator;
|
mod list_separator;
|
||||||
mod list_sub_header;
|
mod list_sub_header;
|
||||||
|
|
||||||
use gpui::{AnyElement, Div};
|
pub use list::*;
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::{v_stack, Label};
|
|
||||||
|
|
||||||
pub use list_header::*;
|
pub use list_header::*;
|
||||||
pub use list_item::*;
|
pub use list_item::*;
|
||||||
pub use list_separator::*;
|
pub use list_separator::*;
|
||||||
pub use list_sub_header::*;
|
pub use list_sub_header::*;
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct List {
|
|
||||||
/// Message to display when the list is empty
|
|
||||||
/// Defaults to "No items"
|
|
||||||
empty_message: SharedString,
|
|
||||||
header: Option<ListHeader>,
|
|
||||||
toggle: Option<bool>,
|
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl List {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
empty_message: "No items".into(),
|
|
||||||
header: None,
|
|
||||||
toggle: None,
|
|
||||||
children: SmallVec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
|
|
||||||
self.empty_message = empty_message.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn header(mut self, header: ListHeader) -> Self {
|
|
||||||
self.header = Some(header);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
|
|
||||||
self.toggle = toggle.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for List {
|
|
||||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
|
||||||
&mut self.children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for List {
|
|
||||||
type Rendered = Div;
|
|
||||||
|
|
||||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
|
||||||
v_stack()
|
|
||||||
.w_full()
|
|
||||||
.py_1()
|
|
||||||
.children(self.header.map(|header| header))
|
|
||||||
.map(|this| match (self.children.is_empty(), self.toggle) {
|
|
||||||
(false, _) => this.children(self.children),
|
|
||||||
(true, Some(false)) => this,
|
|
||||||
(true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
60
crates/ui2/src/components/list/list.rs
Normal file
60
crates/ui2/src/components/list/list.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use gpui::{AnyElement, Div};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::{prelude::*, v_stack, Label, ListHeader};
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct List {
|
||||||
|
/// Message to display when the list is empty
|
||||||
|
/// Defaults to "No items"
|
||||||
|
empty_message: SharedString,
|
||||||
|
header: Option<ListHeader>,
|
||||||
|
toggle: Option<bool>,
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl List {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
empty_message: "No items".into(),
|
||||||
|
header: None,
|
||||||
|
toggle: None,
|
||||||
|
children: SmallVec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
|
||||||
|
self.empty_message = empty_message.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(mut self, header: impl Into<Option<ListHeader>>) -> Self {
|
||||||
|
self.header = header.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
|
||||||
|
self.toggle = toggle.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for List {
|
||||||
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for List {
|
||||||
|
type Rendered = Div;
|
||||||
|
|
||||||
|
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||||
|
v_stack().w_full().py_1().children(self.header).map(|this| {
|
||||||
|
match (self.children.is_empty(), self.toggle) {
|
||||||
|
(false, _) => this.children(self.children),
|
||||||
|
(true, Some(false)) => this,
|
||||||
|
(true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +1,16 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{ClickEvent, Div};
|
use gpui::{AnyElement, ClickEvent, Div};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label};
|
use crate::{h_stack, Disclosure, Icon, IconElement, IconSize, Label};
|
||||||
|
|
||||||
pub enum ListHeaderMeta {
|
|
||||||
Tools(Vec<IconButton>),
|
|
||||||
// TODO: This should be a button
|
|
||||||
Button(Label),
|
|
||||||
Text(Label),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ListHeader {
|
pub struct ListHeader {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
left_icon: Option<Icon>,
|
left_icon: Option<Icon>,
|
||||||
meta: Option<ListHeaderMeta>,
|
meta: SmallVec<[AnyElement; 2]>,
|
||||||
toggle: Option<bool>,
|
toggle: Option<bool>,
|
||||||
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
|
@ -28,7 +22,7 @@ impl ListHeader {
|
||||||
Self {
|
Self {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
left_icon: None,
|
left_icon: None,
|
||||||
meta: None,
|
meta: SmallVec::new(),
|
||||||
inset: false,
|
inset: false,
|
||||||
toggle: None,
|
toggle: None,
|
||||||
on_toggle: None,
|
on_toggle: None,
|
||||||
|
@ -49,17 +43,13 @@ impl ListHeader {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self {
|
||||||
self.left_icon = left_icon;
|
self.left_icon = left_icon.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn right_button(self, button: IconButton) -> Self {
|
pub fn meta(mut self, meta: impl IntoElement) -> Self {
|
||||||
self.meta(Some(ListHeaderMeta::Tools(vec![button])))
|
self.meta.push(meta.into_any_element());
|
||||||
}
|
|
||||||
|
|
||||||
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
|
|
||||||
self.meta = meta;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,18 +65,6 @@ 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 {
|
||||||
let meta = match self.meta {
|
|
||||||
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
|
||||||
h_stack()
|
|
||||||
.gap_2()
|
|
||||||
.items_center()
|
|
||||||
.children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
|
|
||||||
),
|
|
||||||
Some(ListHeaderMeta::Button(label)) => div().child(label),
|
|
||||||
Some(ListHeaderMeta::Text(label)) => div().child(label),
|
|
||||||
None => div(),
|
|
||||||
};
|
|
||||||
|
|
||||||
h_stack().w_full().relative().child(
|
h_stack().w_full().relative().child(
|
||||||
div()
|
div()
|
||||||
.h_5()
|
.h_5()
|
||||||
|
@ -120,7 +98,7 @@ impl RenderOnce for ListHeader {
|
||||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(meta),
|
.child(h_stack().gap_2().items_center().children(self.meta)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ mod icon_button;
|
||||||
mod keybinding;
|
mod keybinding;
|
||||||
mod label;
|
mod label;
|
||||||
mod list;
|
mod list;
|
||||||
|
mod list_header;
|
||||||
mod list_item;
|
mod list_item;
|
||||||
|
|
||||||
pub use avatar::*;
|
pub use avatar::*;
|
||||||
|
@ -20,4 +21,5 @@ pub use icon_button::*;
|
||||||
pub use keybinding::*;
|
pub use keybinding::*;
|
||||||
pub use label::*;
|
pub use label::*;
|
||||||
pub use list::*;
|
pub use list::*;
|
||||||
|
pub use list_header::*;
|
||||||
pub use list_item::*;
|
pub use list_item::*;
|
||||||
|
|
|
@ -22,12 +22,12 @@ impl Render for ListStory {
|
||||||
.child(Story::label("With sections"))
|
.child(Story::label("With sections"))
|
||||||
.child(
|
.child(
|
||||||
List::new()
|
List::new()
|
||||||
.child(ListHeader::new("Fruits"))
|
.header(ListHeader::new("Produce"))
|
||||||
|
.child(ListSubHeader::new("Fruits"))
|
||||||
.child(ListItem::new("apple").child("Apple"))
|
.child(ListItem::new("apple").child("Apple"))
|
||||||
.child(ListItem::new("banana").child("Banana"))
|
.child(ListItem::new("banana").child("Banana"))
|
||||||
.child(ListItem::new("cherry").child("Cherry"))
|
.child(ListItem::new("cherry").child("Cherry"))
|
||||||
.child(ListSeparator)
|
.child(ListSeparator)
|
||||||
.child(ListHeader::new("Vegetables"))
|
|
||||||
.child(ListSubHeader::new("Root Vegetables"))
|
.child(ListSubHeader::new("Root Vegetables"))
|
||||||
.child(ListItem::new("carrot").child("Carrot"))
|
.child(ListItem::new("carrot").child("Carrot"))
|
||||||
.child(ListItem::new("potato").child("Potato"))
|
.child(ListItem::new("potato").child("Potato"))
|
||||||
|
|
33
crates/ui2/src/components/stories/list_header.rs
Normal file
33
crates/ui2/src/components/stories/list_header.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use gpui::{Div, Render};
|
||||||
|
use story::Story;
|
||||||
|
|
||||||
|
use crate::{prelude::*, IconButton};
|
||||||
|
use crate::{Icon, ListHeader};
|
||||||
|
|
||||||
|
pub struct ListHeaderStory;
|
||||||
|
|
||||||
|
impl Render for ListHeaderStory {
|
||||||
|
type Element = Div;
|
||||||
|
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
Story::container()
|
||||||
|
.child(Story::title_for::<ListHeader>())
|
||||||
|
.child(Story::label("Default"))
|
||||||
|
.child(ListHeader::new("Section 1"))
|
||||||
|
.child(Story::label("With left icon"))
|
||||||
|
.child(ListHeader::new("Section 2").left_icon(Icon::Bell))
|
||||||
|
.child(Story::label("With left icon and meta"))
|
||||||
|
.child(
|
||||||
|
ListHeader::new("Section 3")
|
||||||
|
.left_icon(Icon::BellOff)
|
||||||
|
.meta(IconButton::new("action_1", Icon::Bolt)),
|
||||||
|
)
|
||||||
|
.child(Story::label("With multiple meta"))
|
||||||
|
.child(
|
||||||
|
ListHeader::new("Section 4")
|
||||||
|
.meta(IconButton::new("action_1", Icon::Bolt))
|
||||||
|
.meta(IconButton::new("action_2", Icon::ExclamationTriangle))
|
||||||
|
.meta(IconButton::new("action_3", Icon::Plus)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue