Add TabBar

This commit is contained in:
Jason Lee 2024-06-25 19:07:05 +08:00
parent 7fe4e16d5c
commit 08a99217a2
13 changed files with 250 additions and 52 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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<dyn Fn(&Selection, &mut WindowContext) + 'static>;
#[derive(IntoElement)]
pub struct Checkbox {
id: ElementId,
checked: Selection,
disabled: bool,
label: Option<SharedString>,
on_click: Option<Box<dyn Fn(&Selection, &mut WindowContext) + 'static>>,
on_click: Option<OnClick>,
}
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()
})
},
)

View file

@ -0,0 +1,5 @@
use gpui::{ClickEvent, WindowContext};
pub trait Clickable {
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
}

View file

@ -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;
}

View file

@ -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::*;

View file

@ -5,7 +5,7 @@ use gpui::{
use crate::{
button::{Button, ButtonSize, ButtonStyle},
disableable::{Clickable, Disableable as _},
Clickable, Disableable as _,
};
use super::story_case;

View file

@ -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<Self>) -> 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)

View file

@ -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<ButtonStory>,
input_story: View<InputStory>,
checkbox_story: View<CheckboxStory>,
}
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<Self>) -> 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<Self>,
) -> 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<Self>) -> 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()),
})
}
}

5
crates/ui/src/tab.rs Normal file
View file

@ -0,0 +1,5 @@
mod tab;
mod tab_bar;
pub use tab::*;
pub use tab_bar::*;

87
crates/ui/src/tab/tab.rs Normal file
View file

@ -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<Div>,
label: SharedString,
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
disabled: bool,
selected: bool,
}
impl Tab {
pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> 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<AnyElement>) -> Self {
self.prefix = Some(prefix.into());
self
}
/// Set the right side of the tab
pub fn suffix(mut self, suffix: impl Into<AnyElement>) -> 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))
}
}

View file

@ -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<ScrollHandle>,
children: SmallVec<[AnyElement; 2]>,
}
impl TabBar {
pub fn new(id: impl Into<SharedString>) -> 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<Item = AnyElement>) {
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),
)
}
}

View file

@ -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 _;