Add TabBar
This commit is contained in:
parent
7fe4e16d5c
commit
08a99217a2
13 changed files with 250 additions and 52 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
|
|||
5
crates/ui/src/clickable.rs
Normal file
5
crates/ui/src/clickable.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
use gpui::{ClickEvent, WindowContext};
|
||||
|
||||
pub trait Clickable {
|
||||
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use gpui::{
|
|||
|
||||
use crate::{
|
||||
button::{Button, ButtonSize, ButtonStyle},
|
||||
disableable::{Clickable, Disableable as _},
|
||||
Clickable, Disableable as _,
|
||||
};
|
||||
|
||||
use super::story_case;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
5
crates/ui/src/tab.rs
Normal 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
87
crates/ui/src/tab/tab.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
71
crates/ui/src/tab/tab_bar.rs
Normal file
71
crates/ui/src/tab/tab_bar.rs
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 _;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue