gpui-component/crates/story/src/sidebar_story.rs
2025-04-17 17:12:48 +08:00

417 lines
16 KiB
Rust

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,
};
use gpui_component::{
badge::Badge,
blue_500,
breadcrumb::{Breadcrumb, BreadcrumbItem},
divider::Divider,
h_flex,
popup_menu::PopupMenuExt,
sidebar::{
Sidebar, SidebarFooter, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem,
SidebarToggleButton,
},
switch::Switch,
v_flex, white, ActiveTheme, Icon, IconName, Side, Sizable,
};
use serde::Deserialize;
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct SelectCompany(SharedString);
impl_internal_actions!(sidebar_story, [SelectCompany]);
pub struct SidebarStory {
active_items: HashMap<Item, bool>,
last_active_item: Item,
active_subitem: Option<SubItem>,
collapsed: bool,
side: Side,
focus_handle: gpui::FocusHandle,
checked: bool,
}
impl SidebarStory {
pub fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::new(window, cx))
}
fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
let mut active_items = HashMap::new();
active_items.insert(Item::Playground, true);
Self {
active_items,
last_active_item: Item::Playground,
active_subitem: None,
collapsed: false,
side: Side::Left,
focus_handle: cx.focus_handle(),
checked: false,
}
}
fn render_content(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex().child(
h_flex().gap_2().child(
Switch::new("side")
.label("Placement Right")
.checked(self.side.is_right())
.on_click(cx.listener(|this, checked: &bool, _, cx| {
this.side = if *checked { Side::Right } else { Side::Left };
cx.notify();
})),
),
)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Item {
Playground,
Models,
Documentation,
Settings,
DesignEngineering,
SalesAndMarketing,
Travel,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum SubItem {
History,
Starred,
General,
Team,
Billing,
Limits,
Settings,
Genesis,
Explorer,
Quantum,
Introduction,
GetStarted,
Tutorial,
Changelog,
}
impl Item {
pub fn label(&self) -> &'static str {
match self {
Self::Playground => "Playground",
Self::Models => "Models",
Self::Documentation => "Documentation",
Self::Settings => "Settings",
Self::DesignEngineering => "Design Engineering",
Self::SalesAndMarketing => "Sales and Marketing",
Self::Travel => "Travel",
}
}
pub fn icon(&self) -> IconName {
match self {
Self::Playground => IconName::SquareTerminal,
Self::Models => IconName::Bot,
Self::Documentation => IconName::BookOpen,
Self::Settings => IconName::Settings2,
Self::DesignEngineering => IconName::Frame,
Self::SalesAndMarketing => IconName::ChartPie,
Self::Travel => IconName::Map,
}
}
pub fn handler(
&self,
) -> impl Fn(&mut SidebarStory, &ClickEvent, &mut Window, &mut Context<SidebarStory>) + 'static
{
let item = *self;
move |this, _, _, cx| {
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();
}
}
pub fn items(&self) -> Vec<SubItem> {
match self {
Self::Playground => vec![SubItem::History, SubItem::Starred, SubItem::Settings],
Self::Models => vec![SubItem::Genesis, SubItem::Explorer, SubItem::Quantum],
Self::Documentation => vec![
SubItem::Introduction,
SubItem::GetStarted,
SubItem::Tutorial,
SubItem::Changelog,
],
Self::Settings => vec![
SubItem::General,
SubItem::Team,
SubItem::Billing,
SubItem::Limits,
],
_ => Vec::new(),
}
}
}
impl SubItem {
pub fn label(&self) -> &'static str {
match self {
Self::History => "History",
Self::Starred => "Starred",
Self::Settings => "Settings",
Self::Genesis => "Genesis",
Self::Explorer => "Explorer",
Self::Quantum => "Quantum",
Self::Introduction => "Introduction",
Self::GetStarted => "Get Started",
Self::Tutorial => "Tutorial",
Self::Changelog => "Changelog",
Self::Team => "Team",
Self::Billing => "Billing",
Self::Limits => "Limits",
Self::General => "General",
}
}
pub fn handler(
&self,
item: &Item,
) -> impl Fn(&mut SidebarStory, &ClickEvent, &mut Window, &mut Context<SidebarStory>) + 'static
{
let item = *item;
let subitem = *self;
move |this, _, _, cx| {
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();
}
}
}
impl super::Story for SidebarStory {
fn title() -> &'static str {
"Sidebar"
}
fn description() -> &'static str {
"A composable, themeable and customizable sidebar component."
}
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render + Focusable> {
Self::view(window, cx)
}
}
impl Focusable for SidebarStory {
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for SidebarStory {
fn render(
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let groups: [Vec<Item>; 2] = [
vec![
Item::Playground,
Item::Models,
Item::Documentation,
Item::Settings,
],
vec![
Item::DesignEngineering,
Item::SalesAndMarketing,
Item::Travel,
],
];
h_flex()
.rounded(cx.theme().radius)
.border_1()
.border_color(cx.theme().border)
.h_full()
.when(self.side.is_right(), |this| this.flex_row_reverse())
.child(
Sidebar::new(self.side)
.collapsed(self.collapsed)
.header(
SidebarHeader::new()
.w_full()
.child(
div()
.flex()
.items_center()
.justify_center()
.rounded(cx.theme().radius)
.bg(blue_500())
.text_color(white())
.size_8()
.flex_shrink_0()
.when(!self.collapsed, |this| {
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))
}),
)
.when(!self.collapsed, |this| {
this.child(
v_flex()
.gap_0()
.text_sm()
.flex_1()
.line_height(relative(1.25))
.overflow_hidden()
.text_ellipsis()
.child("Company Name")
.child(div().child("Enterprise").text_xs()),
)
})
.when(!self.collapsed, |this| {
this.child(
Icon::new(IconName::ChevronsUpDown).size_4().flex_shrink_0(),
)
})
.popup_menu(|menu, _, _| {
menu.menu(
"Twitter Inc.",
Box::new(SelectCompany(SharedString::from("twitter"))),
)
.menu(
"Meta Platforms",
Box::new(SelectCompany(SharedString::from("meta"))),
)
.menu(
"Google Inc.",
Box::new(SelectCompany(SharedString::from("google"))),
)
}),
)
.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().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().enumerate().map(|(ix, item)| {
SidebarMenuItem::new(item.label())
.icon(item.icon())
.active(self.last_active_item == *item)
.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))
}),
)),
)
.footer(
SidebarFooter::new()
.justify_between()
.child(
h_flex()
.gap_2()
.child(IconName::CircleUser)
.when(!self.collapsed, |this| this.child("Jason Lee")),
)
.when(!self.collapsed, |this| {
this.child(Icon::new(IconName::ChevronsUpDown).size_4())
}),
),
)
.child(
v_flex()
.size_full()
.gap_4()
.p_4()
.child(
h_flex()
.items_center()
.gap_3()
.when(self.side.is_right(), |this| {
this.flex_row_reverse().justify_between()
})
.child(
SidebarToggleButton::left()
.side(self.side)
.collapsed(self.collapsed)
.on_click(cx.listener(|this, _, _, cx| {
this.collapsed = !this.collapsed;
cx.notify();
})),
)
.child(Divider::vertical().h_4())
.child(
Breadcrumb::new()
.item(BreadcrumbItem::new("0", "Home").on_click(cx.listener(
|this, _, _, cx| {
this.last_active_item = Item::Playground;
cx.notify();
},
)))
.item(
BreadcrumbItem::new("1", self.last_active_item.label())
.on_click(cx.listener(|this, _, _, cx| {
this.active_subitem = None;
cx.notify();
})),
)
.when_some(self.active_subitem, |this, subitem| {
this.item(BreadcrumbItem::new("2", subitem.label()))
}),
),
)
.child(self.render_content(window, cx)),
)
}
}