diff --git a/README.md b/README.md index 4813bfd5..f147dd09 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs - [x] WebView - [x] Accordion - [x] Sidebar +- [x] Breadcrumb ## Showcase diff --git a/crates/story/src/sidebar_story.rs b/crates/story/src/sidebar_story.rs index f8b4c2dd..474e6188 100644 --- a/crates/story/src/sidebar_story.rs +++ b/crates/story/src/sidebar_story.rs @@ -5,6 +5,7 @@ use gpui::{ use serde::Deserialize; use ui::{ + breadcrumb::{Breadcrumb, BreadcrumbItem}, divider::Divider, h_flex, popup_menu::PopupMenuExt, @@ -103,6 +104,7 @@ impl Item { let item = *self; move |this, _, cx| { this.active_item = item; + this.active_subitem = None; cx.notify(); } } @@ -332,10 +334,25 @@ impl Render for SidebarStory { })), ) .child(Divider::vertical().h_4()) - .child(self.active_item.label()) - .when_some(self.active_subitem, |this, subitem| { - this.child(Divider::vertical().h_4()).child(subitem.label()) - }), + .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("This content"), ) diff --git a/crates/ui/src/breadcrumb.rs b/crates/ui/src/breadcrumb.rs new file mode 100644 index 00000000..4751e930 --- /dev/null +++ b/crates/ui/src/breadcrumb.rs @@ -0,0 +1,118 @@ +use std::rc::Rc; + +use gpui::{ + div, prelude::FluentBuilder as _, ClickEvent, ElementId, InteractiveElement as _, IntoElement, + ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, WindowContext, +}; + +use crate::{h_flex, theme::ActiveTheme, Icon, IconName}; + +#[derive(IntoElement)] +pub struct Breadcrumb { + items: Vec, +} + +#[derive(IntoElement)] +pub struct BreadcrumbItem { + id: ElementId, + text: SharedString, + on_click: Option>, + disabled: bool, + is_last: bool, +} + +impl BreadcrumbItem { + pub fn new(id: impl Into, text: impl Into) -> Self { + Self { + id: id.into(), + text: text.into(), + on_click: None, + disabled: false, + is_last: false, + } + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_click( + mut self, + on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + ) -> Self { + self.on_click = Some(Rc::new(on_click)); + self + } + + /// For internal use only. + fn is_last(mut self, is_last: bool) -> Self { + self.is_last = is_last; + self + } +} + +impl RenderOnce for BreadcrumbItem { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + div() + .id(self.id) + .child(self.text) + .text_color(cx.theme().muted_foreground) + .when(self.is_last, |this| this.text_color(cx.theme().foreground)) + .when(self.disabled, |this| { + this.text_color(cx.theme().muted_foreground) + }) + .when(!self.disabled, |this| { + this.when_some(self.on_click, |this, on_click| { + this.cursor_pointer().on_click(move |event, cx| { + on_click(event, cx); + }) + }) + }) + } +} + +impl Breadcrumb { + pub fn new() -> Self { + Self { items: Vec::new() } + } + + /// Add an item to the breadcrumb. + pub fn item(mut self, item: BreadcrumbItem) -> Self { + self.items.push(item); + self + } +} + +#[derive(IntoElement)] +struct BreadcrumbSeparator; +impl RenderOnce for BreadcrumbSeparator { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + Icon::new(IconName::ChevronRight) + .text_color(cx.theme().muted_foreground) + .size_3p5() + .into_any_element() + } +} + +impl RenderOnce for Breadcrumb { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let items_count = self.items.len(); + + let mut children = vec![]; + for (ix, item) in self.items.into_iter().enumerate() { + let is_last = ix == items_count - 1; + + children.push(item.is_last(is_last).into_any_element()); + if !is_last { + children.push(BreadcrumbSeparator.into_any_element()); + } + } + + h_flex() + .gap_1p5() + .text_sm() + .text_color(cx.theme().muted_foreground) + .children(children) + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 948289be..debb92f8 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -10,6 +10,7 @@ mod title_bar; pub mod accordion; pub mod animation; +pub mod breadcrumb; pub mod button; pub mod button_group; pub mod checkbox; diff --git a/crates/ui/src/link.rs b/crates/ui/src/link.rs index fcd1bc3f..82db78cd 100644 --- a/crates/ui/src/link.rs +++ b/crates/ui/src/link.rs @@ -10,6 +10,7 @@ use crate::theme::ActiveTheme as _; pub struct Link { base: Stateful
, href: Option, + disabled: bool, on_click: Option>, } @@ -19,6 +20,7 @@ impl Link { base: div().id(id), href: None, on_click: None, + disabled: false, } } @@ -34,6 +36,11 @@ impl Link { self.on_click = Some(Box::new(handler)); self } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } } impl Styled for Link {