gpui-component/crates/story/src/sidebar_story.rs
Jason Lee 851099f890
sidebar: Refactor Sidebar API to avoid map call will segmentation fault on release. (#628)
Close #471

```
    Finished `release` profile [optimized] target(s) in 9.99s
     Running `target/release/examples/sidebar`
[1]    30606 segmentation fault  cargo run --example sidebar --release
```

This may cause by used `map` break by maybe Rust compiler bug, I
detected that if I remove a `map`, then the crash will gone.

So, this changes to refactor the Sidebar API to avoid use `map`.
2025-02-14 17:18:39 +08:00

401 lines
15 KiB
Rust

use gpui::{
div, impl_internal_actions, prelude::FluentBuilder, relative, App, AppContext, ClickEvent,
Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use gpui_component::{
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, Collapsible, Icon, IconName, Side,
};
use serde::Deserialize;
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct SelectCompany(SharedString);
impl_internal_actions!(sidebar_story, [SelectCompany]);
pub struct SidebarStory {
active_item: Item,
active_subitem: Option<SubItem>,
collapsed: bool,
side: Side,
focus_handle: gpui::FocusHandle,
}
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 {
Self {
active_item: Item::Playground,
active_subitem: None,
collapsed: false,
side: Side::Left,
focus_handle: cx.focus_handle(),
}
}
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)]
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| {
this.active_item = item;
this.active_subitem = None;
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| {
this.active_item = item;
this.active_subitem = Some(subitem);
cx.notify();
}
}
}
impl super::Story for SidebarStory {
fn title() -> &'static str {
"Sidebar"
}
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,
],
];
let sidebar = if self.side.is_left() {
Sidebar::left(&cx.entity())
} else {
Sidebar::right(&cx.entity())
};
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
.collapsed(self.collapsed)
.header(
SidebarHeader::new()
.collapsed(self.collapsed)
.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).size_4())
})
.when(self.collapsed, |this| {
this.size_4()
.bg(cx.theme().transparent)
.text_color(cx.theme().foreground)
.child(Icon::new(IconName::GalleryVerticalEnd).size_5())
}),
)
.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"))),
)
}),
)
.footer(
SidebarFooter::new()
.collapsed(self.collapsed)
.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().flex_shrink_0(),
)
}),
)
.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(
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.active_item = Item::Playground;
cx.notify();
},
)))
.item(
BreadcrumbItem::new("1", self.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)),
)
}
}