diff --git a/crates/story/src/sidebar_story.rs b/crates/story/src/sidebar_story.rs index 7a245860..2b756790 100644 --- a/crates/story/src/sidebar_story.rs +++ b/crates/story/src/sidebar_story.rs @@ -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)) }), )), ) diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index 936dbb50..a3a1c58f 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -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); }) diff --git a/crates/ui/src/sidebar/menu.rs b/crates/ui/src/sidebar/menu.rs index 76ea9f30..ef2f164b 100644 --- a/crates/ui/src/sidebar/menu.rs +++ b/crates/ui/src/sidebar/menu.rs @@ -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, + suffix: Option, } 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 diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index e8a5792c..13f66568 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -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 { diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 71893b6e..139346be 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -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.),