sidebar: Fix Sidebar details, something like icon size, remove view_id argument. (#774)

Also updated the Sidebar example to support expand multiple root items.

## Break Change

- Sidebar create not need assign parent view_id now.
This commit is contained in:
Jason Lee 2025-04-03 15:02:05 +08:00 committed by GitHub
parent b016d9fff4
commit d186bf7c9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 81 additions and 89 deletions

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use gpui::{ use gpui::{
div, impl_internal_actions, prelude::FluentBuilder, relative, App, AppContext, ClickEvent, div, impl_internal_actions, prelude::FluentBuilder, relative, App, AppContext, ClickEvent,
Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window, Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
@ -14,7 +16,7 @@ use gpui_component::{
SidebarToggleButton, SidebarToggleButton,
}, },
switch::Switch, switch::Switch,
v_flex, white, ActiveTheme, Collapsible, Icon, IconName, Side, v_flex, white, ActiveTheme, Icon, IconName, Side,
}; };
use serde::Deserialize; use serde::Deserialize;
@ -24,7 +26,8 @@ pub struct SelectCompany(SharedString);
impl_internal_actions!(sidebar_story, [SelectCompany]); impl_internal_actions!(sidebar_story, [SelectCompany]);
pub struct SidebarStory { pub struct SidebarStory {
active_item: Item, active_items: HashMap<Item, bool>,
last_active_item: Item,
active_subitem: Option<SubItem>, active_subitem: Option<SubItem>,
collapsed: bool, collapsed: bool,
side: Side, side: Side,
@ -37,8 +40,12 @@ impl SidebarStory {
} }
fn new(_: &mut Window, cx: &mut Context<Self>) -> Self { fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
let mut active_items = HashMap::new();
active_items.insert(Item::Playground, true);
Self { Self {
active_item: Item::Playground, active_items,
last_active_item: Item::Playground,
active_subitem: None, active_subitem: None,
collapsed: false, collapsed: false,
side: Side::Left, side: Side::Left,
@ -61,7 +68,7 @@ impl SidebarStory {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Item { enum Item {
Playground, Playground,
Models, Models,
@ -121,8 +128,13 @@ impl Item {
{ {
let item = *self; let item = *self;
move |this, _, _, cx| { move |this, _, _, cx| {
this.active_item = item; if this.active_items.contains_key(&item) {
this.active_subitem = None; this.active_items.remove(&item);
} else {
this.active_items.insert(item, true);
}
this.last_active_item = item;
cx.notify(); cx.notify();
} }
} }
@ -176,7 +188,13 @@ impl SubItem {
let item = *item; let item = *item;
let subitem = *self; let subitem = *self;
move |this, _, _, cx| { move |this, _, _, cx| {
this.active_item = item; println!(
"Clicked on item: {}, child: {}",
item.label(),
subitem.label()
);
this.active_items.insert(item, true);
this.last_active_item = item;
this.active_subitem = Some(subitem); this.active_subitem = Some(subitem);
cx.notify(); cx.notify();
} }
@ -219,12 +237,6 @@ impl Render for SidebarStory {
], ],
]; ];
let sidebar = if self.side.is_left() {
Sidebar::left(&cx.entity())
} else {
Sidebar::right(&cx.entity())
};
h_flex() h_flex()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.border_1() .border_1()
@ -232,11 +244,10 @@ impl Render for SidebarStory {
.h_full() .h_full()
.when(self.side.is_right(), |this| this.flex_row_reverse()) .when(self.side.is_right(), |this| this.flex_row_reverse())
.child( .child(
sidebar Sidebar::new(self.side)
.collapsed(self.collapsed) .collapsed(self.collapsed)
.header( .header(
SidebarHeader::new() SidebarHeader::new()
.collapsed(self.collapsed)
.w_full() .w_full()
.child( .child(
div() div()
@ -249,13 +260,13 @@ impl Render for SidebarStory {
.size_8() .size_8()
.flex_shrink_0() .flex_shrink_0()
.when(!self.collapsed, |this| { .when(!self.collapsed, |this| {
this.child(Icon::new(IconName::GalleryVerticalEnd).size_4()) this.child(Icon::new(IconName::GalleryVerticalEnd))
}) })
.when(self.collapsed, |this| { .when(self.collapsed, |this| {
this.size_4() this.size_4()
.bg(cx.theme().transparent) .bg(cx.theme().transparent)
.text_color(cx.theme().foreground) .text_color(cx.theme().foreground)
.child(Icon::new(IconName::GalleryVerticalEnd).size_5()) .child(Icon::new(IconName::GalleryVerticalEnd))
}), }),
) )
.when(!self.collapsed, |this| { .when(!self.collapsed, |this| {
@ -291,9 +302,33 @@ impl Render for SidebarStory {
) )
}), }),
) )
.child(
SidebarGroup::new("Platform").child(SidebarMenu::new().children(
groups[0].iter().map(|item| {
SidebarMenuItem::new(item.label())
.icon(item.icon())
.active(self.active_items.contains_key(item))
.children(item.items().into_iter().map(|sub_item| {
SidebarMenuItem::new(sub_item.label())
.active(self.active_subitem == Some(sub_item))
.on_click(cx.listener(sub_item.handler(&item)))
}))
.on_click(cx.listener(item.handler()))
}),
)),
)
.child(
SidebarGroup::new("Projects").child(SidebarMenu::new().children(
groups[1].iter().map(|item| {
SidebarMenuItem::new(item.label())
.icon(item.icon())
.active(self.last_active_item == *item)
.on_click(cx.listener(item.handler()))
}),
)),
)
.footer( .footer(
SidebarFooter::new() SidebarFooter::new()
.collapsed(self.collapsed)
.justify_between() .justify_between()
.child( .child(
h_flex() h_flex()
@ -302,55 +337,8 @@ impl Render for SidebarStory {
.when(!self.collapsed, |this| this.child("Jason Lee")), .when(!self.collapsed, |this| this.child("Jason Lee")),
) )
.when(!self.collapsed, |this| { .when(!self.collapsed, |this| {
this.child( this.child(Icon::new(IconName::ChevronsUpDown).size_4())
Icon::new(IconName::ChevronsUpDown).size_4().flex_shrink_0(),
)
}), }),
)
.child(
SidebarGroup::new("Platform").child(SidebarMenu::new().children({
let mut items = Vec::with_capacity(groups[0].len());
for item in groups[0].iter() {
let item = *item;
items.push(
SidebarMenuItem::new(item.label())
.icon(item.icon().into())
.active(self.active_item == item)
.children({
let mut sub_items =
Vec::with_capacity(item.items().len());
for sub_item in item.items() {
sub_items.push(
SidebarMenuItem::new(sub_item.label())
.active(
self.active_subitem == Some(sub_item),
)
.on_click(
cx.listener(sub_item.handler(&item)),
),
);
}
sub_items
})
.on_click(cx.listener(item.handler())),
);
}
items
})),
)
.child(
SidebarGroup::new("Projects").child(SidebarMenu::new().children({
let mut items = Vec::with_capacity(groups[1].len());
for item in groups[1].iter() {
items.push(
SidebarMenuItem::new(item.label())
.icon(item.icon().into())
.active(self.active_item == *item)
.on_click(cx.listener(item.handler())),
);
}
items
})),
), ),
) )
.child( .child(
@ -379,12 +367,12 @@ impl Render for SidebarStory {
Breadcrumb::new() Breadcrumb::new()
.item(BreadcrumbItem::new("0", "Home").on_click(cx.listener( .item(BreadcrumbItem::new("0", "Home").on_click(cx.listener(
|this, _, _, cx| { |this, _, _, cx| {
this.active_item = Item::Playground; this.last_active_item = Item::Playground;
cx.notify(); cx.notify();
}, },
))) )))
.item( .item(
BreadcrumbItem::new("1", self.active_item.label()) BreadcrumbItem::new("1", self.last_active_item.label())
.on_click(cx.listener(|this, _, _, cx| { .on_click(cx.listener(|this, _, _, cx| {
this.active_subitem = None; this.active_subitem = None;
cx.notify(); cx.notify();

View file

@ -290,7 +290,8 @@ impl RenderOnce for Icon {
let mut base = self.base; let mut base = self.base;
*base.style() = self.style; *base.style() = self.style;
base.text_color(text_color) base.flex_shrink_0()
.text_color(text_color)
.when(!has_base_size, |this| this.size(text_size)) .when(!has_base_size, |this| this.size(text_size))
.when_some(self.size, |this, size| match size { .when_some(self.size, |this, size| match size {
Size::Size(px) => this.size(px), Size::Size(px) => this.size(px),
@ -318,7 +319,8 @@ impl Render for Icon {
let mut base = svg().flex_none(); let mut base = svg().flex_none();
*base.style() = self.style.clone(); *base.style() = self.style.clone();
base.text_color(text_color) base.flex_shrink_0()
.text_color(text_color)
.when(!has_base_size, |this| this.size(text_size)) .when(!has_base_size, |this| this.size(text_size))
.when_some(self.size, |this, size| match size { .when_some(self.size, |this, size| match size {
Size::Size(px) => this.size(px), Size::Size(px) => this.size(px),

View file

@ -33,6 +33,7 @@ impl Selectable for SidebarHeader {
&self.id &self.id
} }
} }
impl Collapsible for SidebarHeader { impl Collapsible for SidebarHeader {
fn is_collapsed(&self) -> bool { fn is_collapsed(&self) -> bool {
self.collapsed self.collapsed

View file

@ -81,8 +81,8 @@ impl SidebarMenuItem {
} }
/// Set the icon for the menu item /// Set the icon for the menu item
pub fn icon(mut self, icon: Icon) -> Self { pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon); self.icon = Some(icon.into());
self self
} }
@ -138,7 +138,7 @@ impl SidebarMenuItem {
let is_submenu = self.is_submenu(); let is_submenu = self.is_submenu();
h_flex() h_flex()
.id(self.id.clone()) .id("item")
.overflow_hidden() .overflow_hidden()
.flex_shrink_0() .flex_shrink_0()
.p_2() .p_2()
@ -155,9 +155,12 @@ impl SidebarMenuItem {
.bg(cx.theme().sidebar_accent) .bg(cx.theme().sidebar_accent)
.text_color(cx.theme().sidebar_accent_foreground) .text_color(cx.theme().sidebar_accent_foreground)
}) })
.when_some(self.icon.clone(), |this, icon| this.child(icon.size_4())) .when_some(self.icon.clone(), |this, icon| this.child(icon))
.when(is_collapsed, |this| { .when(is_collapsed, |this| {
this.justify_center().size_7().mx_auto() this.justify_center().when(is_active, |this| {
this.bg(cx.theme().sidebar_accent)
.text_color(cx.theme().sidebar_accent_foreground)
})
}) })
.when(!is_collapsed, |this| { .when(!is_collapsed, |this| {
this.h_7() this.h_7()
@ -181,6 +184,7 @@ impl RenderOnce for SidebarMenuItem {
let is_collapsed = self.collapsed; let is_collapsed = self.collapsed;
div() div()
.id(self.id.clone())
.w_full() .w_full()
.child(self.render_menu_item(window, cx)) .child(self.render_menu_item(window, cx))
.when(is_submenu && is_open && !is_collapsed, |this| { .when(is_submenu && is_open && !is_collapsed, |this| {

View file

@ -5,9 +5,8 @@ use crate::{
v_flex, ActiveTheme, Collapsible, Icon, IconName, Side, Sizable, StyledExt, v_flex, ActiveTheme, Collapsible, Icon, IconName, Side, Sizable, StyledExt,
}; };
use gpui::{ use gpui::{
div, prelude::FluentBuilder, px, AnyElement, App, ClickEvent, Entity, EntityId, div, prelude::FluentBuilder, px, AnyElement, App, ClickEvent, InteractiveElement as _,
InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, RenderOnce, Styled, IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window,
Window,
}; };
use std::rc::Rc; use std::rc::Rc;
@ -26,8 +25,6 @@ const COLLAPSED_WIDTH: Pixels = px(48.);
/// A sidebar /// A sidebar
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Sidebar<E: Collapsible + IntoElement + 'static> { pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
/// The parent view id
view_id: EntityId,
content: Vec<E>, content: Vec<E>,
/// header view /// header view
header: Option<AnyElement>, header: Option<AnyElement>,
@ -41,9 +38,8 @@ pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
} }
impl<E: Collapsible + IntoElement> Sidebar<E> { impl<E: Collapsible + IntoElement> Sidebar<E> {
fn new(view_id: EntityId, side: Side) -> Self { pub fn new(side: Side) -> Self {
Self { Self {
view_id,
content: vec![], content: vec![],
header: None, header: None,
footer: None, footer: None,
@ -54,12 +50,12 @@ impl<E: Collapsible + IntoElement> Sidebar<E> {
} }
} }
pub fn left<V: Render + 'static>(view: &Entity<V>) -> Self { pub fn left() -> Self {
Self::new(view.entity_id(), Side::Left) Self::new(Side::Left)
} }
pub fn right<V: Render + 'static>(view: &Entity<V>) -> Self { pub fn right() -> Self {
Self::new(view.entity_id(), Side::Right) Self::new(Side::Right)
} }
/// Set the width of the sidebar /// Set the width of the sidebar
@ -117,7 +113,7 @@ pub struct SidebarToggleButton {
impl SidebarToggleButton { impl SidebarToggleButton {
fn new(side: Side) -> Self { fn new(side: Side) -> Self {
Self { Self {
btn: Button::new("sidebar-collapse").ghost().small(), btn: Button::new("collapse").ghost().small(),
collapsed: false, collapsed: false,
side, side,
on_click: None, on_click: None,
@ -181,7 +177,8 @@ impl RenderOnce for SidebarToggleButton {
} }
impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> { impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> {
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let view_id = window.current_view();
v_flex() v_flex()
.id("sidebar") .id("sidebar")
.w(self.width) .w(self.width)
@ -210,7 +207,7 @@ impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> {
.map(|(ix, c)| div().id(ix).child(c.collapsed(self.collapsed))), .map(|(ix, c)| div().id(ix).child(c.collapsed(self.collapsed))),
) )
.gap_2() .gap_2()
.scrollable(self.view_id, ScrollbarAxis::Vertical), .scrollable(view_id, ScrollbarAxis::Vertical),
), ),
) )
.when_some(self.footer.take(), |this, footer| { .when_some(self.footer.take(), |this, footer| {