use crate::{prelude::FluentBuilder as _, *};
use std::sync::Arc;
#[derive(Clone)]
pub struct SidebarItem {
pub id: SharedString,
pub label: SharedString,
pub icon: Option<Icon>,
pub badge: Option<SharedString>,
pub selected_background: Option<Hsla>,
pub selected_foreground: Option<Hsla>,
}
impl SidebarItem {
pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
label: label.into(),
icon: None,
badge: None,
selected_background: None,
selected_foreground: None,
}
}
pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn with_badge(mut self, badge: impl Into<SharedString>) -> Self {
self.badge = Some(badge.into());
self
}
pub fn with_selected_background(mut self, color: impl Into<Hsla>) -> Self {
self.selected_background = Some(color.into());
self
}
pub fn with_selected_foreground(mut self, color: impl Into<Hsla>) -> Self {
self.selected_foreground = Some(color.into());
self
}
}
#[derive(Clone)]
pub struct SidebarSection {
pub title: SharedString,
pub items: Vec<SidebarItem>,
pub collapsed: bool,
}
impl SidebarSection {
pub fn new(title: impl Into<SharedString>, items: Vec<SidebarItem>) -> Self {
Self {
title: title.into(),
items,
collapsed: false,
}
}
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
self
}
}
#[derive(IntoElement)]
pub struct Sidebar {
items: Vec<SidebarItem>,
sections: Vec<SidebarSection>,
selected_id: Option<SharedString>,
collapsible: bool,
collapsed: bool,
on_change: Option<Arc<dyn Fn(SharedString, &mut Window, &mut App) + Send + Sync + 'static>>,
on_toggle_collapsed: Option<Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync + 'static>>,
on_toggle_section:
Option<Arc<dyn Fn(SharedString, bool, &mut Window, &mut App) + Send + Sync + 'static>>,
style: StyleRefinement,
}
impl Sidebar {
pub fn new() -> Self {
Self {
items: Vec::new(),
sections: Vec::new(),
selected_id: None,
collapsible: false,
collapsed: false,
on_change: None,
on_toggle_collapsed: None,
on_toggle_section: None,
style: StyleRefinement::default(),
}
}
pub fn item(mut self, item: SidebarItem) -> Self {
self.items.push(item);
self
}
pub fn items(mut self, items: Vec<SidebarItem>) -> Self {
self.items = items;
self
}
pub fn section(mut self, section: SidebarSection) -> Self {
self.sections.push(section);
self
}
pub fn sections(mut self, sections: Vec<SidebarSection>) -> Self {
self.sections = sections;
self
}
pub fn selected(mut self, id: impl Into<SharedString>) -> Self {
self.selected_id = Some(id.into());
self
}
pub fn collapsible(mut self, collapsible: bool) -> Self {
self.collapsible = collapsible;
self
}
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
self
}
pub fn on_change<F>(mut self, f: F) -> Self
where
F: Fn(SharedString, &mut Window, &mut App) + Send + Sync + 'static,
{
self.on_change = Some(Arc::new(f));
self
}
pub fn on_toggle_collapsed<F>(mut self, f: F) -> Self
where
F: Fn(bool, &mut Window, &mut App) + Send + Sync + 'static,
{
self.on_toggle_collapsed = Some(Arc::new(f));
self
}
pub fn on_toggle_section<F>(mut self, f: F) -> Self
where
F: Fn(SharedString, bool, &mut Window, &mut App) + Send + Sync + 'static,
{
self.on_toggle_section = Some(Arc::new(f));
self
}
}
impl Default for Sidebar {
fn default() -> Self {
Self::new()
}
}
impl Styled for Sidebar {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Sidebar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let border = theme.tokens.border;
let muted_foreground = theme.tokens.muted_foreground;
let popover = theme.tokens.popover;
let sidebar_accent = theme.tokens.sidebar_accent;
let sidebar_accent_foreground = theme.tokens.sidebar_accent_foreground;
let collapsed = self.collapsible && self.collapsed;
let selected_id = self.selected_id;
let on_change = self.on_change;
let user_style = self.style;
let mut root = div()
.flex()
.flex_col()
.h_full()
.w(if collapsed { px(56.0) } else { px(220.0) })
.bg(popover)
.border_r(px(1.0))
.border_color(border)
.py(px(8.0))
.gap(px(2.0));
let item_row = |item: &SidebarItem,
selected_id: &Option<SharedString>,
on_change: &Option<
Arc<dyn Fn(SharedString, &mut Window, &mut App) + Send + Sync + 'static>,
>,
collapsed: bool| {
let is_selected = selected_id.as_ref() == Some(&item.id);
let id = item.id.clone();
let label = item.label.clone();
let icon = item.icon.clone();
let badge = item.badge.clone();
let on_change = on_change.clone();
let row_bg = item.selected_background.unwrap_or(sidebar_accent.color);
let row_fg = item
.selected_foreground
.unwrap_or(sidebar_accent_foreground.color);
div()
.id(id.clone())
.flex()
.items_center()
.gap(px(8.0))
.px(px(12.0))
.py(px(8.0))
.rounded_md()
.cursor_pointer()
.when(is_selected, |this| this.bg(row_bg))
.when(!is_selected, |this| {
this.hover(|this| this.bg(row_bg.opacity(0.5)))
})
.when_some(icon, |this, icon| this.child(icon))
.when(!collapsed, |this| {
this.child(
div()
.flex_1()
.text_sm()
.text_color(if is_selected {
row_fg
} else {
muted_foreground.color
})
.child(label),
)
})
.when_some(badge, |this, badge| {
this.child(
div()
.text_xs()
.px(px(6.0))
.rounded_full()
.bg(if is_selected {
row_fg.opacity(0.15)
} else {
row_bg.opacity(0.6)
})
.text_color(row_fg)
.child(badge),
)
})
.on_click(move |_, window, cx| {
if let Some(ref cb) = on_change {
cb(id.clone(), window, cx);
}
})
};
for item in &self.items {
root = root.child(item_row(item, &selected_id, &on_change, collapsed));
}
for (section_ix, section) in self.sections.iter().enumerate() {
let title = section.title.clone();
let section_collapsed = section.collapsed;
let on_toggle_section = self.on_toggle_section.clone();
root = root.child(
div()
.id(ElementId::named_usize("sidebar-section", section_ix))
.flex()
.items_center()
.gap(px(4.0))
.px(px(12.0))
.py(px(6.0))
.cursor_pointer()
.child(
Icon::new(if section_collapsed {
IconName::ChevronRight
} else {
IconName::ChevronDown
})
.xsmall()
.text_color(cx.theme().muted_foreground),
)
.when(!collapsed, |this| {
this.child(
div()
.text_xs()
.text_color(muted_foreground.color)
.child(title.clone()),
)
})
.on_click(move |_, window, cx| {
if let Some(ref cb) = on_toggle_section {
cb(title.clone(), !section_collapsed, window, cx);
}
}),
);
if !section_collapsed {
for item in §ion.items {
root = root.child(item_row(item, &selected_id, &on_change, collapsed));
}
}
}
if self.collapsible {
let on_toggle_collapsed = self.on_toggle_collapsed;
root = root.child(
div().flex_1().flex().flex_col().justify_end().child(
div()
.flex()
.items_center()
.justify_center()
.p(px(8.0))
.child(
Button::new("sidebar-toggle")
.ghost()
.small()
.icon(if collapsed {
IconName::ChevronRight
} else {
IconName::ChevronLeft
})
.on_click(move |_, window, cx| {
if let Some(ref cb) = on_toggle_collapsed {
let next = !collapsed;
cb(next, window, cx);
}
}),
),
),
);
}
root.map(|mut this| {
this.style().refine(&user_style);
this
})
}
}