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:
parent
5ac98a83b5
commit
8930acf648
5 changed files with 127 additions and 81 deletions
|
|
@ -6,6 +6,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
|
badge::Badge,
|
||||||
blue_500,
|
blue_500,
|
||||||
breadcrumb::{Breadcrumb, BreadcrumbItem},
|
breadcrumb::{Breadcrumb, BreadcrumbItem},
|
||||||
divider::Divider,
|
divider::Divider,
|
||||||
|
|
@ -16,7 +17,7 @@ use gpui_component::{
|
||||||
SidebarToggleButton,
|
SidebarToggleButton,
|
||||||
},
|
},
|
||||||
switch::Switch,
|
switch::Switch,
|
||||||
v_flex, white, ActiveTheme, Icon, IconName, Side,
|
v_flex, white, ActiveTheme, Icon, IconName, Side, Sizable,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ pub struct SidebarStory {
|
||||||
collapsed: bool,
|
collapsed: bool,
|
||||||
side: Side,
|
side: Side,
|
||||||
focus_handle: gpui::FocusHandle,
|
focus_handle: gpui::FocusHandle,
|
||||||
|
checked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SidebarStory {
|
impl SidebarStory {
|
||||||
|
|
@ -50,6 +52,7 @@ impl SidebarStory {
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
side: Side::Left,
|
side: Side::Left,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
checked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,22 +311,43 @@ impl Render for SidebarStory {
|
||||||
SidebarMenuItem::new(item.label())
|
SidebarMenuItem::new(item.label())
|
||||||
.icon(item.icon())
|
.icon(item.icon())
|
||||||
.active(self.active_items.contains_key(item))
|
.active(self.active_items.contains_key(item))
|
||||||
.children(item.items().into_iter().map(|sub_item| {
|
.children(item.items().into_iter().enumerate().map(
|
||||||
SidebarMenuItem::new(sub_item.label())
|
|(ix, sub_item)| {
|
||||||
.active(self.active_subitem == Some(sub_item))
|
SidebarMenuItem::new(sub_item.label())
|
||||||
.on_click(cx.listener(sub_item.handler(&item)))
|
.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()))
|
.on_click(cx.listener(item.handler()))
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
SidebarGroup::new("Projects").child(SidebarMenu::new().children(
|
SidebarGroup::new("Projects").child(SidebarMenu::new().children(
|
||||||
groups[1].iter().map(|item| {
|
groups[1].iter().enumerate().map(|(ix, item)| {
|
||||||
SidebarMenuItem::new(item.label())
|
SidebarMenuItem::new(item.label())
|
||||||
.icon(item.icon())
|
.icon(item.icon())
|
||||||
.active(self.last_active_item == *item)
|
.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))
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -149,26 +149,28 @@ impl RenderOnce for Checkbox {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.when(self.label.is_some() || !self.children.is_empty(), |this| {
|
||||||
v_flex()
|
this.child(
|
||||||
.w_full()
|
v_flex()
|
||||||
.line_height(relative(1.2))
|
.w_full()
|
||||||
.gap_1()
|
.line_height(relative(1.2))
|
||||||
.map(|this| {
|
.gap_1()
|
||||||
if let Some(label) = self.label {
|
.map(|this| {
|
||||||
this.child(
|
if let Some(label) = self.label {
|
||||||
div()
|
this.child(
|
||||||
.size_full()
|
div()
|
||||||
.text_color(cx.theme().foreground)
|
.size_full()
|
||||||
.line_height(relative(1.))
|
.text_color(cx.theme().foreground)
|
||||||
.child(label),
|
.line_height(relative(1.))
|
||||||
)
|
.child(label),
|
||||||
} else {
|
)
|
||||||
this
|
} else {
|
||||||
}
|
this
|
||||||
})
|
}
|
||||||
.children(self.children),
|
})
|
||||||
)
|
.children(self.children),
|
||||||
|
)
|
||||||
|
})
|
||||||
.when(self.disabled, |this| {
|
.when(self.disabled, |this| {
|
||||||
this.cursor_not_allowed()
|
this.cursor_not_allowed()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
|
@ -177,6 +179,7 @@ impl RenderOnce for Checkbox {
|
||||||
self.on_click.filter(|_| !self.disabled),
|
self.on_click.filter(|_| !self.disabled),
|
||||||
|this, on_click| {
|
|this, on_click| {
|
||||||
this.on_click(move |_, window, cx| {
|
this.on_click(move |_, window, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
let checked = !self.checked;
|
let checked = !self.checked;
|
||||||
on_click(&checked, window, cx);
|
on_click(&checked, window, cx);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{h_flex, v_flex, ActiveTheme as _, Collapsible, Icon, IconName, StyledExt};
|
use crate::{h_flex, v_flex, ActiveTheme as _, Collapsible, Icon, IconName, StyledExt};
|
||||||
use gpui::{
|
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,
|
InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString,
|
||||||
StatefulInteractiveElement as _, Styled as _, Window,
|
StatefulInteractiveElement as _, Styled as _, Window,
|
||||||
};
|
};
|
||||||
|
|
@ -64,6 +64,7 @@ pub struct SidebarMenuItem {
|
||||||
active: bool,
|
active: bool,
|
||||||
collapsed: bool,
|
collapsed: bool,
|
||||||
children: Vec<Self>,
|
children: Vec<Self>,
|
||||||
|
suffix: Option<AnyElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SidebarMenuItem {
|
impl SidebarMenuItem {
|
||||||
|
|
@ -77,6 +78,7 @@ impl SidebarMenuItem {
|
||||||
active: false,
|
active: false,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
|
suffix: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +120,12 @@ impl SidebarMenuItem {
|
||||||
self
|
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 {
|
fn is_submenu(&self) -> bool {
|
||||||
self.children.len() > 0
|
self.children.len() > 0
|
||||||
}
|
}
|
||||||
|
|
@ -129,64 +137,71 @@ impl SidebarMenuItem {
|
||||||
false
|
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 handler = self.handler.clone();
|
||||||
let is_collapsed = self.collapsed;
|
let is_collapsed = self.collapsed;
|
||||||
let is_active = self.active;
|
let is_active = self.active;
|
||||||
let is_open = self.is_open();
|
let is_open = self.is_open();
|
||||||
let is_submenu = self.is_submenu();
|
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()
|
div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.w_full()
|
.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| {
|
.when(is_submenu && is_open && !is_collapsed, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|
@ -194,8 +209,8 @@ impl RenderOnce for SidebarMenuItem {
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
.border_color(cx.theme().sidebar_border)
|
.border_color(cx.theme().sidebar_border)
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.mx_3p5()
|
.ml_3p5()
|
||||||
.px_2p5()
|
.pl_2p5()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.children(
|
.children(
|
||||||
self.children
|
self.children
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ impl Element for Switch {
|
||||||
|
|
||||||
let (bg, toggle_bg) = match self.checked {
|
let (bg, toggle_bg) = match self.checked {
|
||||||
true => (cx.theme().primary, cx.theme().background),
|
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 {
|
let (bg, toggle_bg) = match self.disabled {
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,8 @@ pub struct ThemeColor {
|
||||||
pub success_hover: Hsla,
|
pub success_hover: Hsla,
|
||||||
/// Success active background color.
|
/// Success active background color.
|
||||||
pub success_active: Hsla,
|
pub success_active: Hsla,
|
||||||
|
/// Switch background color.
|
||||||
|
pub switch: Hsla,
|
||||||
/// Tab background color.
|
/// Tab background color.
|
||||||
pub tab: Hsla,
|
pub tab: Hsla,
|
||||||
/// Tab active background color.
|
/// Tab active background color.
|
||||||
|
|
@ -292,7 +294,7 @@ impl ThemeColor {
|
||||||
secondary_hover: hsl(240.0, 5.9, 98.),
|
secondary_hover: hsl(240.0, 5.9, 98.),
|
||||||
selection: hsl(211.0, 97.0, 85.0),
|
selection: hsl(211.0, 97.0, 85.0),
|
||||||
sidebar: hsl(0.0, 0.0, 98.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_accent_foreground: hsl(240.0, 5.9, 10.0),
|
||||||
sidebar_border: hsl(220.0, 13.0, 91.0),
|
sidebar_border: hsl(220.0, 13.0, 91.0),
|
||||||
sidebar_foreground: hsl(240.0, 5.3, 26.1),
|
sidebar_foreground: hsl(240.0, 5.3, 26.1),
|
||||||
|
|
@ -305,6 +307,7 @@ impl ThemeColor {
|
||||||
success_active: crate::green_600(),
|
success_active: crate::green_600(),
|
||||||
success_hover: crate::green_500().opacity(0.9),
|
success_hover: crate::green_500().opacity(0.9),
|
||||||
success_foreground: crate::gray_50(),
|
success_foreground: crate::gray_50(),
|
||||||
|
switch: hsl(240.0, 5.9, 90.0),
|
||||||
tab: gpui::transparent_black(),
|
tab: gpui::transparent_black(),
|
||||||
tab_active: hsl(0.0, 0.0, 100.0),
|
tab_active: hsl(0.0, 0.0, 100.0),
|
||||||
tab_active_foreground: hsl(240.0, 10., 3.9),
|
tab_active_foreground: hsl(240.0, 10., 3.9),
|
||||||
|
|
@ -397,6 +400,7 @@ impl ThemeColor {
|
||||||
success_active: crate::green_800().darken(0.2),
|
success_active: crate::green_800().darken(0.2),
|
||||||
success_foreground: crate::green_50(),
|
success_foreground: crate::green_50(),
|
||||||
success_hover: crate::green_800().opacity(0.8),
|
success_hover: crate::green_800().opacity(0.8),
|
||||||
|
switch: hsl(0., 0., 31.),
|
||||||
tab: gpui::transparent_black(),
|
tab: gpui::transparent_black(),
|
||||||
tab_active: hsl(0.0, 0.0, 8.0),
|
tab_active: hsl(0.0, 0.0, 8.0),
|
||||||
tab_active_foreground: hsl(0., 0., 78.),
|
tab_active_foreground: hsl(0., 0., 78.),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue