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::{
div, impl_internal_actions, prelude::FluentBuilder, relative, App, AppContext, ClickEvent,
Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
@ -14,7 +16,7 @@ use gpui_component::{
SidebarToggleButton,
},
switch::Switch,
v_flex, white, ActiveTheme, Collapsible, Icon, IconName, Side,
v_flex, white, ActiveTheme, Icon, IconName, Side,
};
use serde::Deserialize;
@ -24,7 +26,8 @@ pub struct SelectCompany(SharedString);
impl_internal_actions!(sidebar_story, [SelectCompany]);
pub struct SidebarStory {
active_item: Item,
active_items: HashMap<Item, bool>,
last_active_item: Item,
active_subitem: Option<SubItem>,
collapsed: bool,
side: Side,
@ -37,8 +40,12 @@ impl SidebarStory {
}
fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
let mut active_items = HashMap::new();
active_items.insert(Item::Playground, true);
Self {
active_item: Item::Playground,
active_items,
last_active_item: Item::Playground,
active_subitem: None,
collapsed: false,
side: Side::Left,
@ -61,7 +68,7 @@ impl SidebarStory {
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Item {
Playground,
Models,
@ -121,8 +128,13 @@ impl Item {
{
let item = *self;
move |this, _, _, cx| {
this.active_item = item;
this.active_subitem = None;
if this.active_items.contains_key(&item) {
this.active_items.remove(&item);
} else {
this.active_items.insert(item, true);
}
this.last_active_item = item;
cx.notify();
}
}
@ -176,7 +188,13 @@ impl SubItem {
let item = *item;
let subitem = *self;
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);
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()
.rounded(cx.theme().radius)
.border_1()
@ -232,11 +244,10 @@ impl Render for SidebarStory {
.h_full()
.when(self.side.is_right(), |this| this.flex_row_reverse())
.child(
sidebar
Sidebar::new(self.side)
.collapsed(self.collapsed)
.header(
SidebarHeader::new()
.collapsed(self.collapsed)
.w_full()
.child(
div()
@ -249,13 +260,13 @@ impl Render for SidebarStory {
.size_8()
.flex_shrink_0()
.when(!self.collapsed, |this| {
this.child(Icon::new(IconName::GalleryVerticalEnd).size_4())
this.child(Icon::new(IconName::GalleryVerticalEnd))
})
.when(self.collapsed, |this| {
this.size_4()
.bg(cx.theme().transparent)
.text_color(cx.theme().foreground)
.child(Icon::new(IconName::GalleryVerticalEnd).size_5())
.child(Icon::new(IconName::GalleryVerticalEnd))
}),
)
.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(
SidebarFooter::new()
.collapsed(self.collapsed)
.justify_between()
.child(
h_flex()
@ -302,55 +337,8 @@ impl Render for SidebarStory {
.when(!self.collapsed, |this| this.child("Jason Lee")),
)
.when(!self.collapsed, |this| {
this.child(
Icon::new(IconName::ChevronsUpDown).size_4().flex_shrink_0(),
)
this.child(Icon::new(IconName::ChevronsUpDown).size_4())
}),
)
.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(
@ -379,12 +367,12 @@ impl Render for SidebarStory {
Breadcrumb::new()
.item(BreadcrumbItem::new("0", "Home").on_click(cx.listener(
|this, _, _, cx| {
this.active_item = Item::Playground;
this.last_active_item = Item::Playground;
cx.notify();
},
)))
.item(
BreadcrumbItem::new("1", self.active_item.label())
BreadcrumbItem::new("1", self.last_active_item.label())
.on_click(cx.listener(|this, _, _, cx| {
this.active_subitem = None;
cx.notify();

View file

@ -290,7 +290,8 @@ impl RenderOnce for Icon {
let mut base = self.base;
*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_some(self.size, |this, size| match size {
Size::Size(px) => this.size(px),
@ -318,7 +319,8 @@ impl Render for Icon {
let mut base = svg().flex_none();
*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_some(self.size, |this, size| match size {
Size::Size(px) => this.size(px),

View file

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

View file

@ -81,8 +81,8 @@ impl SidebarMenuItem {
}
/// Set the icon for the menu item
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
@ -138,7 +138,7 @@ impl SidebarMenuItem {
let is_submenu = self.is_submenu();
h_flex()
.id(self.id.clone())
.id("item")
.overflow_hidden()
.flex_shrink_0()
.p_2()
@ -155,9 +155,12 @@ impl SidebarMenuItem {
.bg(cx.theme().sidebar_accent)
.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| {
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| {
this.h_7()
@ -181,6 +184,7 @@ impl RenderOnce for SidebarMenuItem {
let is_collapsed = self.collapsed;
div()
.id(self.id.clone())
.w_full()
.child(self.render_menu_item(window, cx))
.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,
};
use gpui::{
div, prelude::FluentBuilder, px, AnyElement, App, ClickEvent, Entity, EntityId,
InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, RenderOnce, Styled,
Window,
div, prelude::FluentBuilder, px, AnyElement, App, ClickEvent, InteractiveElement as _,
IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window,
};
use std::rc::Rc;
@ -26,8 +25,6 @@ const COLLAPSED_WIDTH: Pixels = px(48.);
/// A sidebar
#[derive(IntoElement)]
pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
/// The parent view id
view_id: EntityId,
content: Vec<E>,
/// header view
header: Option<AnyElement>,
@ -41,9 +38,8 @@ pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
}
impl<E: Collapsible + IntoElement> Sidebar<E> {
fn new(view_id: EntityId, side: Side) -> Self {
pub fn new(side: Side) -> Self {
Self {
view_id,
content: vec![],
header: None,
footer: None,
@ -54,12 +50,12 @@ impl<E: Collapsible + IntoElement> Sidebar<E> {
}
}
pub fn left<V: Render + 'static>(view: &Entity<V>) -> Self {
Self::new(view.entity_id(), Side::Left)
pub fn left() -> Self {
Self::new(Side::Left)
}
pub fn right<V: Render + 'static>(view: &Entity<V>) -> Self {
Self::new(view.entity_id(), Side::Right)
pub fn right() -> Self {
Self::new(Side::Right)
}
/// Set the width of the sidebar
@ -117,7 +113,7 @@ pub struct SidebarToggleButton {
impl SidebarToggleButton {
fn new(side: Side) -> Self {
Self {
btn: Button::new("sidebar-collapse").ghost().small(),
btn: Button::new("collapse").ghost().small(),
collapsed: false,
side,
on_click: None,
@ -181,7 +177,8 @@ impl RenderOnce for SidebarToggleButton {
}
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()
.id("sidebar")
.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))),
)
.gap_2()
.scrollable(self.view_id, ScrollbarAxis::Vertical),
.scrollable(view_id, ScrollbarAxis::Vertical),
),
)
.when_some(self.footer.take(), |this, footer| {