sidebar: Add suffix for menu item (#792)

| Light | Dark |
| - | - |
| <img width="913" alt="SCR-20250415-mkdr"
src="https://github.com/user-attachments/assets/5c4931e6-4d03-48ec-91c6-ade2d86de75a"
/> | <img width="913" alt="SCR-20250415-mkec"
src="https://github.com/user-attachments/assets/f85af984-ecc3-4cd7-913c-c4e93384dbf1"
/> |
This commit is contained in:
Floyd Wang 2025-04-15 14:02:52 +08:00 committed by GitHub
parent 5ac98a83b5
commit 8930acf648
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 127 additions and 81 deletions

View file

@ -6,6 +6,7 @@ use gpui::{
};
use gpui_component::{
badge::Badge,
blue_500,
breadcrumb::{Breadcrumb, BreadcrumbItem},
divider::Divider,
@ -16,7 +17,7 @@ use gpui_component::{
SidebarToggleButton,
},
switch::Switch,
v_flex, white, ActiveTheme, Icon, IconName, Side,
v_flex, white, ActiveTheme, Icon, IconName, Side, Sizable,
};
use serde::Deserialize;
@ -32,6 +33,7 @@ pub struct SidebarStory {
collapsed: bool,
side: Side,
focus_handle: gpui::FocusHandle,
checked: bool,
}
impl SidebarStory {
@ -50,6 +52,7 @@ impl SidebarStory {
collapsed: false,
side: Side::Left,
focus_handle: cx.focus_handle(),
checked: false,
}
}
@ -308,22 +311,43 @@ impl Render for SidebarStory {
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)))
}))
.children(item.items().into_iter().enumerate().map(
|(ix, sub_item)| {
SidebarMenuItem::new(sub_item.label())
.active(self.active_subitem == Some(sub_item))
.when(ix == 0, |this| {
this.suffix(
Switch::new("switch")
.xsmall()
.checked(self.checked)
.on_click(cx.listener(
|this, checked, _, _| {
this.checked = *checked
},
)),
)
})
.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| {
groups[1].iter().enumerate().map(|(ix, item)| {
SidebarMenuItem::new(item.label())
.icon(item.icon())
.active(self.last_active_item == *item)
.on_click(cx.listener(item.handler()))
.when(ix == 0, |this| {
this.suffix(
Badge::new().dot().count(1).child(
div().p_0p5().child(Icon::new(IconName::Bell)),
),
)
})
.when(ix == 1, |this| this.suffix(IconName::Settings2))
}),
)),
)

View file

@ -149,26 +149,28 @@ impl RenderOnce for Checkbox {
}),
),
)
.child(
v_flex()
.w_full()
.line_height(relative(1.2))
.gap_1()
.map(|this| {
if let Some(label) = self.label {
this.child(
div()
.size_full()
.text_color(cx.theme().foreground)
.line_height(relative(1.))
.child(label),
)
} else {
this
}
})
.children(self.children),
)
.when(self.label.is_some() || !self.children.is_empty(), |this| {
this.child(
v_flex()
.w_full()
.line_height(relative(1.2))
.gap_1()
.map(|this| {
if let Some(label) = self.label {
this.child(
div()
.size_full()
.text_color(cx.theme().foreground)
.line_height(relative(1.))
.child(label),
)
} else {
this
}
})
.children(self.children),
)
})
.when(self.disabled, |this| {
this.cursor_not_allowed()
.text_color(cx.theme().muted_foreground)
@ -177,6 +179,7 @@ impl RenderOnce for Checkbox {
self.on_click.filter(|_| !self.disabled),
|this, on_click| {
this.on_click(move |_, window, cx| {
cx.stop_propagation();
let checked = !self.checked;
on_click(&checked, window, cx);
})

View file

@ -1,6 +1,6 @@
use crate::{h_flex, v_flex, ActiveTheme as _, Collapsible, Icon, IconName, StyledExt};
use gpui::{
div, percentage, prelude::FluentBuilder as _, App, ClickEvent, ElementId,
div, percentage, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, ElementId,
InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString,
StatefulInteractiveElement as _, Styled as _, Window,
};
@ -64,6 +64,7 @@ pub struct SidebarMenuItem {
active: bool,
collapsed: bool,
children: Vec<Self>,
suffix: Option<AnyElement>,
}
impl SidebarMenuItem {
@ -77,6 +78,7 @@ impl SidebarMenuItem {
active: false,
collapsed: false,
children: Vec::new(),
suffix: None,
}
}
@ -118,6 +120,12 @@ impl SidebarMenuItem {
self
}
/// Set the suffix for the menu item.
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
self.suffix = Some(suffix.into_any_element());
self
}
fn is_submenu(&self) -> bool {
self.children.len() > 0
}
@ -129,64 +137,71 @@ impl SidebarMenuItem {
false
}
}
}
fn render_menu_item(&self, _: &Window, cx: &App) -> impl IntoElement {
impl RenderOnce for SidebarMenuItem {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let handler = self.handler.clone();
let is_collapsed = self.collapsed;
let is_active = self.active;
let is_open = self.is_open();
let is_submenu = self.is_submenu();
h_flex()
.id("item")
.overflow_hidden()
.flex_shrink_0()
.p_2()
.gap_2()
.items_center()
.rounded(cx.theme().radius)
.text_sm()
.hover(|this| {
this.bg(cx.theme().sidebar_accent)
.text_color(cx.theme().sidebar_accent_foreground)
})
.when(is_active && !is_submenu, |this| {
this.font_medium()
.bg(cx.theme().sidebar_accent)
.text_color(cx.theme().sidebar_accent_foreground)
})
.when_some(self.icon.clone(), |this, icon| this.child(icon))
.when(is_collapsed, |this| {
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()
.child(div().flex_1().child(self.label.clone()))
.when(is_submenu, |this| {
this.child(
Icon::new(IconName::ChevronRight)
.size_4()
.when(is_open, |this| this.rotate(percentage(90. / 360.))),
)
})
})
.on_click(move |ev, window, cx| handler(ev, window, cx))
}
}
impl RenderOnce for SidebarMenuItem {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let is_submenu = self.is_submenu();
let is_open = self.is_open();
let is_collapsed = self.collapsed;
div()
.id(self.id.clone())
.w_full()
.child(self.render_menu_item(window, cx))
.child(
h_flex()
.size_full()
.id("item")
.overflow_x_hidden()
.flex_shrink_0()
.p_2()
.gap_x_2()
.rounded(cx.theme().radius)
.text_sm()
.hover(|this| {
this.bg(cx.theme().sidebar_accent)
.text_color(cx.theme().sidebar_accent_foreground)
})
.when(is_active && !is_submenu, |this| {
this.font_medium()
.bg(cx.theme().sidebar_accent)
.text_color(cx.theme().sidebar_accent_foreground)
})
.when_some(self.icon.clone(), |this, icon| this.child(icon))
.when(is_collapsed, |this| {
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()
.child(
h_flex()
.flex_1()
.gap_x_2()
.justify_between()
.overflow_x_hidden()
.child(
h_flex()
.flex_1()
.overflow_x_hidden()
.child(self.label.clone()),
)
.when_some(self.suffix, |this, suffix| this.child(suffix)),
)
.when(is_submenu, |this| {
this.child(
Icon::new(IconName::ChevronRight)
.size_4()
.when(is_open, |this| this.rotate(percentage(90. / 360.))),
)
})
})
.on_click(move |ev, window, cx| handler(ev, window, cx)),
)
.when(is_submenu && is_open && !is_collapsed, |this| {
this.child(
v_flex()
@ -194,8 +209,8 @@ impl RenderOnce for SidebarMenuItem {
.border_l_1()
.border_color(cx.theme().sidebar_border)
.gap_1()
.mx_3p5()
.px_2p5()
.ml_3p5()
.pl_2p5()
.py_0p5()
.children(
self.children

View file

@ -112,7 +112,7 @@ impl Element for Switch {
let (bg, toggle_bg) = match self.checked {
true => (cx.theme().primary, cx.theme().background),
false => (cx.theme().input, cx.theme().background),
false => (cx.theme().switch, cx.theme().background),
};
let (bg, toggle_bg) = match self.disabled {

View file

@ -187,6 +187,8 @@ pub struct ThemeColor {
pub success_hover: Hsla,
/// Success active background color.
pub success_active: Hsla,
/// Switch background color.
pub switch: Hsla,
/// Tab background color.
pub tab: Hsla,
/// Tab active background color.
@ -292,7 +294,7 @@ impl ThemeColor {
secondary_hover: hsl(240.0, 5.9, 98.),
selection: hsl(211.0, 97.0, 85.0),
sidebar: hsl(0.0, 0.0, 98.0),
sidebar_accent: hsl(240.0, 4.8, 92.),
sidebar_accent: hsl(240.0, 4.8, 95.9),
sidebar_accent_foreground: hsl(240.0, 5.9, 10.0),
sidebar_border: hsl(220.0, 13.0, 91.0),
sidebar_foreground: hsl(240.0, 5.3, 26.1),
@ -305,6 +307,7 @@ impl ThemeColor {
success_active: crate::green_600(),
success_hover: crate::green_500().opacity(0.9),
success_foreground: crate::gray_50(),
switch: hsl(240.0, 5.9, 90.0),
tab: gpui::transparent_black(),
tab_active: hsl(0.0, 0.0, 100.0),
tab_active_foreground: hsl(240.0, 10., 3.9),
@ -397,6 +400,7 @@ impl ThemeColor {
success_active: crate::green_800().darken(0.2),
success_foreground: crate::green_50(),
success_hover: crate::green_800().opacity(0.8),
switch: hsl(0., 0., 31.),
tab: gpui::transparent_black(),
tab_active: hsl(0.0, 0.0, 8.0),
tab_active_foreground: hsl(0., 0., 78.),