From 08a99217a209c366cfcbe1c9ed6b3371e5ad6b9f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 25 Jun 2024 19:07:05 +0800 Subject: [PATCH] Add TabBar --- README.md | 5 +- crates/ui/src/button.rs | 4 +- crates/ui/src/checkbox.rs | 17 +++++- crates/ui/src/clickable.rs | 5 ++ crates/ui/src/disableable.rs | 10 --- crates/ui/src/lib.rs | 21 ++++--- crates/ui/src/story/button_story.rs | 2 +- crates/ui/src/story/checkbox_story.rs | 36 ++++++----- crates/ui/src/story/mod.rs | 36 +++++++---- crates/ui/src/tab.rs | 5 ++ crates/ui/src/tab/tab.rs | 87 +++++++++++++++++++++++++++ crates/ui/src/tab/tab_bar.rs | 71 ++++++++++++++++++++++ crates/workspace/src/lib.rs | 3 +- 13 files changed, 250 insertions(+), 52 deletions(-) create mode 100644 crates/ui/src/clickable.rs create mode 100644 crates/ui/src/tab.rs create mode 100644 crates/ui/src/tab/tab.rs create mode 100644 crates/ui/src/tab/tab_bar.rs diff --git a/README.md b/README.md index 150e7915..18a99569 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ https://github.com/huacnlee/gpui-app/assets/5518/ad103f02-697a-40ed-a876-8b13e42 ## TODO +- [x] Theming - [ ] TextField - [x] Ctrl+a, e to move cursor to start/end - [x] Copy, Cut, Paste by keyboard @@ -23,7 +24,9 @@ https://github.com/huacnlee/gpui-app/assets/5518/ad103f02-697a-40ed-a876-8b13e42 - [ ] Radio & RadioGroup - [ ] Dropdown - [ ] Combobox -- [ ] Tabs +- [x] Tabs + - [x] Tab + - [x] TabBar - [ ] Dockpanel & Splitter - [ ] List - [ ] Table diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index eba58718..5d4bc89c 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -5,9 +5,9 @@ use gpui::{ }; use crate::{ - disableable::{Clickable, Disableable, Selectable}, label::Label, - theme::{ActiveTheme, Colorize as _, Theme, ThemeMode}, + theme::{ActiveTheme, Colorize as _, ThemeMode}, + Clickable, Disableable, Selectable, }; pub enum ButtonRounded { diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index 75ba87ac..f11f5be4 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -7,18 +7,20 @@ use gpui::{ use crate::{ disableable::Disableable, label::Label, - selectable::Selection, + selectable::{Selectable, Selection}, stock::{h_flex, v_flex}, theme::{ActiveTheme, Colorize as _}, }; +type OnClick = Box; + #[derive(IntoElement)] pub struct Checkbox { id: ElementId, checked: Selection, disabled: bool, label: Option, - on_click: Option>, + on_click: Option, } impl Checkbox { @@ -55,6 +57,16 @@ impl Disableable for Checkbox { } } +impl Selectable for Checkbox { + fn selected(self, selected: bool) -> Self { + self.checked(if selected { + Selection::Selected + } else { + Selection::Unselected + }) + } +} + impl RenderOnce for Checkbox { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let theme = cx.theme(); @@ -120,6 +132,7 @@ impl RenderOnce for Checkbox { |this, on_click| { this.on_click(move |_, cx| { on_click(&self.checked.inverse(), cx); + cx.refresh() }) }, ) diff --git a/crates/ui/src/clickable.rs b/crates/ui/src/clickable.rs new file mode 100644 index 00000000..b25f6b0e --- /dev/null +++ b/crates/ui/src/clickable.rs @@ -0,0 +1,5 @@ +use gpui::{ClickEvent, WindowContext}; + +pub trait Clickable { + fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self; +} diff --git a/crates/ui/src/disableable.rs b/crates/ui/src/disableable.rs index 168ac134..7f107fd5 100644 --- a/crates/ui/src/disableable.rs +++ b/crates/ui/src/disableable.rs @@ -1,13 +1,3 @@ -use gpui::{ClickEvent, WindowContext}; - pub trait Disableable { fn disabled(self, disabled: bool) -> Self; } - -pub trait Selectable { - fn selected(self, selected: bool) -> Self; -} - -pub trait Clickable { - fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self; -} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 476b2b16..9e7d20cd 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1,17 +1,24 @@ +mod clickable; +mod disableable; +mod icon; +mod platform; +mod prelude; +mod selectable; +mod stock; +mod styled_ext; + pub mod button; pub mod checkbox; -pub mod disableable; pub mod label; -mod prelude; -pub mod selectable; -mod stock; pub mod story; -mod styled_ext; pub mod text_field; pub mod theme; pub mod title_bar; pub use styled_ext::StyledExt; -mod icon; -mod platform; +pub mod tab; + +pub use clickable::Clickable; +pub use disableable::Disableable; +pub use selectable::Selectable; pub use icon::*; diff --git a/crates/ui/src/story/button_story.rs b/crates/ui/src/story/button_story.rs index 513870ed..8d02c31e 100644 --- a/crates/ui/src/story/button_story.rs +++ b/crates/ui/src/story/button_story.rs @@ -5,7 +5,7 @@ use gpui::{ use crate::{ button::{Button, ButtonSize, ButtonStyle}, - disableable::{Clickable, Disableable as _}, + Clickable, Disableable as _, }; use super::story_case; diff --git a/crates/ui/src/story/checkbox_story.rs b/crates/ui/src/story/checkbox_story.rs index 0871eed4..91eae66f 100644 --- a/crates/ui/src/story/checkbox_story.rs +++ b/crates/ui/src/story/checkbox_story.rs @@ -1,5 +1,6 @@ use gpui::{ - div, ClickEvent, IntoElement, ParentElement, Render, Styled, ViewContext, WindowContext, + div, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, Styled, ViewContext, + WindowContext, }; use crate::{ @@ -11,11 +12,22 @@ use crate::{ use super::story_case; -pub struct CheckboxStory {} +#[derive(IntoElement)] +pub struct CheckboxStory { + check1: Checkbox, + check1_1: Checkbox, +} impl CheckboxStory { - pub(crate) fn new(_cx: &mut WindowContext) -> Self { - Self {} + pub(crate) fn new(cx: &mut WindowContext) -> Self { + Self { + check1: Checkbox::new("check1", cx) + .checked(Selection::Unselected) + .on_click(Self::on_click), + check1_1: Checkbox::new("check1_1", cx) + .checked(Selection::Indeterminate) + .on_click(Self::on_click), + } } #[allow(unused)] @@ -24,8 +36,8 @@ impl CheckboxStory { } } -impl Render for CheckboxStory { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { +impl RenderOnce for CheckboxStory { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { story_case( "Checkbox", "A control that allows the user to toggle between checked and not checked.", @@ -35,16 +47,8 @@ impl Render for CheckboxStory { h_flex() .items_center() .gap_4() - .child( - Checkbox::new("check1", cx) - .checked(Selection::Unselected) - .on_click(Self::on_click), - ) - .child( - Checkbox::new("check1_1", cx) - .checked(Selection::Indeterminate) - .on_click(Self::on_click), - ) + .child(self.check1) + .child(self.check1_1) .child( Checkbox::new("check1_2", cx) .checked(Selection::Selected) diff --git a/crates/ui/src/story/mod.rs b/crates/ui/src/story/mod.rs index f1cd4ec4..132ee702 100644 --- a/crates/ui/src/story/mod.rs +++ b/crates/ui/src/story/mod.rs @@ -1,17 +1,26 @@ use core::fmt; -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + sync::Arc, +}; use checkbox_story::CheckboxStory; use gpui::{ div, prelude::FluentBuilder as _, px, AnyElement, IntoElement, ParentElement, Render, - RenderOnce, SharedString, Styled as _, View, ViewContext, VisualContext, WindowContext, + RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext, + VisualContext, WindowContext, }; mod button_story; mod checkbox_story; mod input_story; -use crate::{button::Button, disableable::Clickable as _, label::Label}; +use crate::{ + button::Button, + label::Label, + tab::{Tab, TabBar}, + Selectable, +}; use button_story::ButtonStory; use input_story::InputStory; @@ -83,7 +92,6 @@ pub struct Stories { button_story: View, input_story: View, - checkbox_story: View, } impl Stories { @@ -92,7 +100,6 @@ impl Stories { active: StoryType::Button, button_story: cx.new_view(|cx| ButtonStory {}), input_story: cx.new_view(|cx| InputStory::new(cx)), - checkbox_story: cx.new_view(|cx| CheckboxStory::new(cx)), } } @@ -106,13 +113,18 @@ impl Stories { } fn render_story_buttons(&self, cx: &mut ViewContext) -> impl IntoElement { + let tabs = vec![ + self.swith_button("story-button", StoryType::Button, cx), + self.swith_button("story-input", StoryType::Input, cx), + self.swith_button("story-checkbox", StoryType::Checkbox, cx), + ]; + div() .flex() .items_center() .gap_4() - .child(self.swith_button("story-button", StoryType::Button, cx)) - .child(self.swith_button("story-input", StoryType::Input, cx)) - .child(self.swith_button("story-checkbox", StoryType::Checkbox, cx)) + .w_full() + .child(TabBar::new("story-tabs").children(tabs)) } fn swith_button( @@ -122,18 +134,20 @@ impl Stories { cx: &mut ViewContext, ) -> impl IntoElement { let name = format!("{}", ty); + let is_active = ty == self.active; - Button::new(SharedString::from(id.to_string()), name) + Tab::new(SharedString::from(id.to_string()), name) + .selected(is_active) .on_click(cx.listener(move |this, _, cx| { this.set_active(ty, cx); })) - .style(crate::button::ButtonStyle::Secondary) } } impl Render for Stories { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() + .w_full() .flex() .flex_col() .gap_4() @@ -141,7 +155,7 @@ impl Render for Stories { .map(|this| match self.active { StoryType::Button => this.child(self.button_story.clone()), StoryType::Input => this.child(self.input_story.clone()), - StoryType::Checkbox => this.child(self.checkbox_story.clone()), + StoryType::Checkbox => this.child(CheckboxStory::new(cx).into_any_element()), }) } } diff --git a/crates/ui/src/tab.rs b/crates/ui/src/tab.rs new file mode 100644 index 00000000..1906121b --- /dev/null +++ b/crates/ui/src/tab.rs @@ -0,0 +1,5 @@ +mod tab; +mod tab_bar; + +pub use tab::*; +pub use tab_bar::*; diff --git a/crates/ui/src/tab/tab.rs b/crates/ui/src/tab/tab.rs new file mode 100644 index 00000000..dc1cdde6 --- /dev/null +++ b/crates/ui/src/tab/tab.rs @@ -0,0 +1,87 @@ +use crate::selectable::Selectable; +use crate::theme::{ActiveTheme, Colorize}; +use gpui::prelude::FluentBuilder as _; +use gpui::{ + div, AnyElement, Div, IntoElement, ParentElement as _, RenderOnce, SharedString, Stateful, + StatefulInteractiveElement, WindowContext, +}; +use gpui::{InteractiveElement, Styled as _}; + +#[derive(IntoElement)] +pub struct Tab { + base: Stateful
, + label: SharedString, + prefix: Option, + suffix: Option, + disabled: bool, + selected: bool, +} + +impl Tab { + pub fn new(id: impl Into, label: impl Into) -> Self { + Self { + base: div().id(id.into()), + label: label.into(), + disabled: false, + selected: false, + prefix: None, + suffix: None, + } + } + + /// Set the left side of the tab + pub fn prefix(mut self, prefix: impl Into) -> Self { + self.prefix = Some(prefix.into()); + self + } + + /// Set the right side of the tab + pub fn suffix(mut self, suffix: impl Into) -> Self { + self.suffix = Some(suffix.into()); + self + } +} + +impl Selectable for Tab { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + +impl InteractiveElement for Tab { + fn interactivity(&mut self) -> &mut gpui::Interactivity { + self.base.interactivity() + } +} + +impl StatefulInteractiveElement for Tab {} + +impl RenderOnce for Tab { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let theme = cx.theme(); + + self.base + .flex() + .items_center() + .gap_2() + .h_full() + .cursor_pointer() + .py_1() + .px_3() + .min_w_16() + .text_color(theme.muted_foreground) + .bg(theme.muted) + .when(self.selected, |this| { + this.text_color(theme.foreground) + .bg(theme.background) + .rounded_sm() + }) + .when(self.disabled, |this| { + this.text_color(theme.foreground.opacity(0.5)) + }) + .when_some(self.prefix, |this, prefix| this.child(prefix)) + .child(self.label.clone()) + .when_some(self.suffix, |this, suffix| this.child(suffix)) + } +} diff --git a/crates/ui/src/tab/tab_bar.rs b/crates/ui/src/tab/tab_bar.rs new file mode 100644 index 00000000..825a7942 --- /dev/null +++ b/crates/ui/src/tab/tab_bar.rs @@ -0,0 +1,71 @@ +use crate::stock::h_flex; +use crate::theme::ActiveTheme; +use gpui::prelude::FluentBuilder as _; +use gpui::{ + div, AnyElement, Div, IntoElement, ParentElement, RenderOnce, ScrollHandle, SharedString, + StatefulInteractiveElement as _, WindowContext, +}; +use gpui::{InteractiveElement, Styled as _}; +use smallvec::SmallVec; + +#[derive(IntoElement)] +pub struct TabBar { + base: Div, + id: SharedString, + scroll_handle: Option, + children: SmallVec<[AnyElement; 2]>, +} + +impl TabBar { + pub fn new(id: impl Into) -> Self { + Self { + base: div(), + id: id.into(), + children: SmallVec::new(), + scroll_handle: None, + } + } + + #[allow(unused)] + pub fn track_scroll(mut self, scroll_handle: ScrollHandle) -> Self { + self.scroll_handle = Some(scroll_handle); + self + } +} + +impl ParentElement for TabBar { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl RenderOnce for TabBar { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let theme = cx.theme(); + + self.base + .id(self.id) + .group("tab_bar") + .flex() + .flex_none() + .items_center() + .w_full() + .h_10() + .p_1() + .bg(theme.muted) + .text_color(theme.muted_foreground) + .rounded_md() + .relative() + .overflow_x_hidden() + .child( + h_flex() + .id("tabs") + .flex_grow() + .overflow_x_scroll() + .when_some(self.scroll_handle, |cx, scroll_handle| { + cx.track_scroll(&scroll_handle) + }) + .children(self.children), + ) + } +} diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index db69aecc..5413689f 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -4,12 +4,11 @@ use prelude::FluentBuilder as _; use std::sync::Arc; use ui::{ button::Button, - disableable::Clickable as _, label::Label, story::Stories, theme::{ActiveTheme, Theme}, title_bar::TitleBar, - StyledExt as _, + Clickable as _, StyledExt as _, }; use util::ResultExt as _;