tab: Add Pill, Segmented, Underline style Tab and complete the Tab API. (#676)
<img width="985" alt="image" src="https://github.com/user-attachments/assets/3d3860ff-ac7c-459a-bb6e-d0ea09b8eac8" /> <img width="936" alt="image" src="https://github.com/user-attachments/assets/abb39b8f-4139-4f41-9c12-2a22f767bec0" /> <img width="985" alt="image" src="https://github.com/user-attachments/assets/9fccce5f-8da5-4c78-8679-1a85dcfc56f0" /> Run example: ``` cargo run --example tabs ```
This commit is contained in:
parent
76efa2715b
commit
1aa43b1bfe
8 changed files with 824 additions and 43 deletions
35
crates/story/examples/tabs.rs
Normal file
35
crates/story/examples/tabs.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use gpui::*;
|
||||
use story::{Assets, TabsStory};
|
||||
|
||||
pub struct Example {
|
||||
root: Entity<TabsStory>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let root = TabsStory::view(window, cx);
|
||||
|
||||
Self { root }
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self::new(window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().p_4().size_full().child(self.root.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let app = Application::new().with_assets(Assets);
|
||||
|
||||
app.run(move |cx| {
|
||||
story::init(cx);
|
||||
cx.activate(true);
|
||||
|
||||
story::create_new_window("Tabs Example", Example::view, cx);
|
||||
});
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ mod scrollable_story;
|
|||
mod sidebar_story;
|
||||
mod switch_story;
|
||||
mod table_story;
|
||||
mod tabs_story;
|
||||
mod text_story;
|
||||
mod title_bar;
|
||||
mod tooltip_story;
|
||||
|
|
@ -28,6 +29,7 @@ pub use button_story::ButtonStory;
|
|||
pub use calendar_story::CalendarStory;
|
||||
pub use dropdown_story::DropdownStory;
|
||||
pub use form_story::FormStory;
|
||||
pub use tabs_story::TabsStory;
|
||||
|
||||
use gpui::{
|
||||
actions, div, impl_internal_actions, prelude::FluentBuilder as _, px, size, AnyElement,
|
||||
|
|
|
|||
200
crates/story/src/tabs_story.rs
Normal file
200
crates/story/src/tabs_story.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
use gpui::{
|
||||
App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Window,
|
||||
};
|
||||
|
||||
use gpui_component::{
|
||||
button::{Button, ButtonGroup, ButtonVariants},
|
||||
h_flex,
|
||||
tab::{Tab, TabBar},
|
||||
v_flex, IconName, Selectable as _, Sizable, Size,
|
||||
};
|
||||
|
||||
use crate::section;
|
||||
|
||||
pub struct TabsStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
active_tab_ix: usize,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl super::Story for TabsStory {
|
||||
fn title() -> &'static str {
|
||||
"Tabs"
|
||||
}
|
||||
|
||||
fn description() -> &'static str {
|
||||
"A set of layered sections of content—known as tab panels—that are displayed one at a time."
|
||||
}
|
||||
|
||||
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render + Focusable> {
|
||||
Self::view(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl TabsStory {
|
||||
pub fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self::new(window, cx))
|
||||
}
|
||||
|
||||
fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
active_tab_ix: 0,
|
||||
size: Size::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_active_tab(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.active_tab_ix = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.size = size;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for TabsStory {
|
||||
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TabsStory {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
ButtonGroup::new("toggle-size")
|
||||
.child(
|
||||
Button::new("xsmall")
|
||||
.label("XSmall")
|
||||
.selected(self.size == Size::XSmall),
|
||||
)
|
||||
.child(
|
||||
Button::new("small")
|
||||
.label("Small")
|
||||
.selected(self.size == Size::Small),
|
||||
)
|
||||
.child(
|
||||
Button::new("medium")
|
||||
.label("Medium")
|
||||
.selected(self.size == Size::Medium),
|
||||
)
|
||||
.child(
|
||||
Button::new("large")
|
||||
.label("Large")
|
||||
.selected(self.size == Size::Large),
|
||||
)
|
||||
.on_click(cx.listener(|this, selecteds: &Vec<usize>, window, cx| {
|
||||
let size = match selecteds[0] {
|
||||
0 => Size::XSmall,
|
||||
1 => Size::Small,
|
||||
2 => Size::Medium,
|
||||
3 => Size::Large,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
this.set_size(size, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
section("Normal Tabs", cx).child(
|
||||
TabBar::new("normal-tabs")
|
||||
.w_full()
|
||||
.with_size(self.size)
|
||||
.selected_index(self.active_tab_ix)
|
||||
.on_click(cx.listener(|this, ix: &usize, window, cx| {
|
||||
this.set_active_tab(*ix, window, cx);
|
||||
}))
|
||||
.prefix(
|
||||
h_flex()
|
||||
.mx_1()
|
||||
.child(
|
||||
Button::new("back")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::ArrowLeft),
|
||||
)
|
||||
.child(
|
||||
Button::new("forward")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::ArrowRight),
|
||||
),
|
||||
)
|
||||
.child(Tab::new("tab-account", "Account"))
|
||||
.child(Tab::new("tab-profile", "Profile").disabled(true))
|
||||
.child(Tab::new("tab-documents", "Documents"))
|
||||
.child(Tab::new("tab-mail", "Mail"))
|
||||
.child(Tab::new("tab-appearance", "Appearance"))
|
||||
.child(Tab::new("tab-settings", "Settings"))
|
||||
.suffix(
|
||||
h_flex()
|
||||
.mx_1()
|
||||
.child(Button::new("inbox").ghost().xsmall().icon(IconName::Inbox))
|
||||
.child(
|
||||
Button::new("more")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Ellipsis),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Pills Tabs", cx).child(
|
||||
TabBar::new("pills-tabs")
|
||||
.w_full()
|
||||
.pill()
|
||||
.with_size(self.size)
|
||||
.selected_index(self.active_tab_ix)
|
||||
.on_click(cx.listener(|this, ix: &usize, window, cx| {
|
||||
this.set_active_tab(*ix, window, cx);
|
||||
}))
|
||||
.child(Tab::new("tab-account", "Account"))
|
||||
.child(Tab::new("tab-profile", "Profile").disabled(true))
|
||||
.child(Tab::new("tab-documents", "Documents"))
|
||||
.child(Tab::new("tab-mail", "Mail"))
|
||||
.child(Tab::new("tab-appearance", "Appearance"))
|
||||
.child(Tab::new("tab-settings", "Settings")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Segmented Tabs", cx).child(
|
||||
TabBar::new("segmented-tabs")
|
||||
.w_full()
|
||||
.segmented()
|
||||
.with_size(self.size)
|
||||
.selected_index(self.active_tab_ix)
|
||||
.on_click(cx.listener(|this, ix: &usize, window, cx| {
|
||||
this.set_active_tab(*ix, window, cx);
|
||||
}))
|
||||
.child(Tab::new("tab-account", "Account"))
|
||||
.child(Tab::new("tab-profile", "Profile").disabled(true))
|
||||
.child(Tab::new("tab-documents", "Documents"))
|
||||
.child(Tab::new("tab-mail", "Mail"))
|
||||
.child(Tab::new("tab-appearance", "Appearance"))
|
||||
.child(Tab::new("tab-settings", "Settings")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Underline Tabs", cx).child(
|
||||
TabBar::new("underline-tabs")
|
||||
.w_full()
|
||||
.underline()
|
||||
.with_size(self.size)
|
||||
.selected_index(self.active_tab_ix)
|
||||
.on_click(cx.listener(|this, ix: &usize, window, cx| {
|
||||
this.set_active_tab(*ix, window, cx);
|
||||
}))
|
||||
.child(Tab::new("tab-account", "Account"))
|
||||
.child(Tab::new("tab-profile", "Profile").disabled(true))
|
||||
.child(Tab::new("tab-documents", "Documents"))
|
||||
.child(Tab::new("tab-mail", "Mail"))
|
||||
.child(Tab::new("tab-appearance", "Appearance"))
|
||||
.child(Tab::new("tab-settings", "Settings")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -651,6 +651,7 @@ impl TabPanel {
|
|||
let tabs_count = self.panels.len();
|
||||
|
||||
TabBar::new("tab-bar")
|
||||
.mt(-px(1.))
|
||||
.track_scroll(self.tab_bar_scroll_handle.clone())
|
||||
.when(
|
||||
left_dock_button.is_some() || bottom_dock_button.is_some(),
|
||||
|
|
@ -672,6 +673,9 @@ impl TabPanel {
|
|||
)
|
||||
},
|
||||
)
|
||||
.on_click(cx.listener(move |view, ix: &usize, window, cx| {
|
||||
view.set_active_ix(*ix, window, cx);
|
||||
}))
|
||||
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
|
||||
let mut active = state.active_panel.as_ref() == Some(panel);
|
||||
let disabled = self.collapsed;
|
||||
|
|
@ -691,10 +695,7 @@ impl TabPanel {
|
|||
.selected(active)
|
||||
.disabled(disabled)
|
||||
.when(!disabled, |this| {
|
||||
this.on_click(cx.listener(move |view, _, window, cx| {
|
||||
view.set_active_ix(ix, window, cx);
|
||||
}))
|
||||
.when(state.draggable, |this| {
|
||||
this.when(state.draggable, |this| {
|
||||
this.on_drag(
|
||||
DragPanel::new(panel.clone(), view.clone()),
|
||||
|drag, _, _, cx| {
|
||||
|
|
@ -720,7 +721,7 @@ impl TabPanel {
|
|||
}),
|
||||
)
|
||||
}))
|
||||
.child(
|
||||
.last_empty_space(
|
||||
// empty space to allow move to last tab right
|
||||
div()
|
||||
.id("tab-bar-empty-space")
|
||||
|
|
|
|||
|
|
@ -58,6 +58,18 @@ pub trait StyledExt: Styled + Sized {
|
|||
.pr(paddings.right.into())
|
||||
}
|
||||
|
||||
/// Apply margins to the element.
|
||||
fn margins<L>(self, margins: impl Into<Edges<L>>) -> Self
|
||||
where
|
||||
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug,
|
||||
{
|
||||
let margins = margins.into();
|
||||
self.mt(margins.top.into())
|
||||
.mb(margins.bottom.into())
|
||||
.ml(margins.left.into())
|
||||
.mr(margins.right.into())
|
||||
}
|
||||
|
||||
/// Render a border with a width of 1px, color red
|
||||
fn debug_red(self) -> Self {
|
||||
if cfg!(debug_assertions) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,324 @@
|
|||
use crate::{ActiveTheme, Selectable};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{ActiveTheme, Selectable, Sizable, Size, StyledExt};
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement as _,
|
||||
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window,
|
||||
div, px, AnyElement, App, ClickEvent, Div, Edges, ElementId, Hsla, InteractiveElement,
|
||||
IntoElement, ParentElement as _, Pixels, RenderOnce, Stateful, StatefulInteractiveElement,
|
||||
Styled, Window,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TabVariant {
|
||||
#[default]
|
||||
Tab,
|
||||
Pill,
|
||||
Segmented,
|
||||
Underline,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct TabStyle {
|
||||
borders: Edges<Pixels>,
|
||||
border_color: Hsla,
|
||||
bg: Hsla,
|
||||
fg: Hsla,
|
||||
radius: Pixels,
|
||||
shadow: bool,
|
||||
inner_bg: Hsla,
|
||||
inner_radius: Pixels,
|
||||
}
|
||||
|
||||
impl Default for TabStyle {
|
||||
fn default() -> Self {
|
||||
TabStyle {
|
||||
borders: Edges::all(px(0.)),
|
||||
border_color: gpui::transparent_white(),
|
||||
bg: gpui::transparent_white(),
|
||||
fg: gpui::transparent_white(),
|
||||
radius: px(0.),
|
||||
shadow: false,
|
||||
inner_bg: gpui::transparent_white(),
|
||||
inner_radius: px(0.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TabVariant {
|
||||
fn inner_height(&self, size: Size) -> Pixels {
|
||||
match size {
|
||||
Size::Small | Size::XSmall => match self {
|
||||
TabVariant::Tab => px(25.),
|
||||
TabVariant::Pill => px(26.),
|
||||
TabVariant::Segmented => px(26.),
|
||||
TabVariant::Underline => px(22.),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Tab => px(30.),
|
||||
TabVariant::Pill => px(31.),
|
||||
TabVariant::Segmented => px(30.),
|
||||
TabVariant::Underline => px(25.),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
|
||||
match size {
|
||||
Size::Small | Size::XSmall => match self {
|
||||
TabVariant::Tab => Edges {
|
||||
left: px(10.),
|
||||
right: px(10.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => Edges {
|
||||
left: px(14.),
|
||||
right: px(14.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => Edges {
|
||||
left: px(8.),
|
||||
right: px(8.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => Edges {
|
||||
left: px(10.),
|
||||
right: px(10.),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Tab => Edges {
|
||||
left: px(12.),
|
||||
right: px(12.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => Edges {
|
||||
left: px(16.),
|
||||
right: px(16.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => Edges {
|
||||
left: px(10.),
|
||||
right: px(10.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => Edges {
|
||||
left: px(12.),
|
||||
right: px(12.),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_margins(&self, size: Size) -> Edges<Pixels> {
|
||||
match size {
|
||||
Size::Small | Size::XSmall => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
bottom: px(4.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normal(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().foreground,
|
||||
bg: cx.theme().transparent,
|
||||
borders: Edges {
|
||||
top: px(1.),
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().transparent,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().foreground,
|
||||
bg: cx.theme().transparent,
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().border,
|
||||
radius: px(99.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().foreground,
|
||||
bg: cx.theme().transparent,
|
||||
radius: cx.theme().radius,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().foreground,
|
||||
bg: cx.theme().transparent,
|
||||
radius: px(0.),
|
||||
inner_bg: cx.theme().transparent,
|
||||
inner_radius: cx.theme().radius,
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().transparent,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: cx.theme().transparent,
|
||||
borders: Edges {
|
||||
top: px(1.),
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().transparent,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().accent_foreground,
|
||||
bg: cx.theme().accent,
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().border,
|
||||
radius: px(99.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: cx.theme().transparent,
|
||||
radius: cx.theme().radius,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: cx.theme().transparent,
|
||||
radius: px(0.),
|
||||
inner_bg: cx.theme().accent,
|
||||
inner_radius: cx.theme().radius,
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().transparent,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn selected(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: cx.theme().tab_active,
|
||||
borders: Edges {
|
||||
top: px(1.),
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().accent_foreground,
|
||||
bg: cx.theme().transparent,
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().primary,
|
||||
radius: px(99.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: cx.theme().background,
|
||||
radius: cx.theme().radius,
|
||||
shadow: true,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: cx.theme().transparent,
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().primary,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().muted_foreground,
|
||||
bg: cx.theme().tab,
|
||||
border_color: if selected {
|
||||
cx.theme().border
|
||||
} else {
|
||||
cx.theme().transparent
|
||||
},
|
||||
borders: Edges {
|
||||
top: px(1.),
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().muted_foreground,
|
||||
bg: cx.theme().transparent,
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: if selected {
|
||||
cx.theme().primary
|
||||
} else {
|
||||
cx.theme().border
|
||||
},
|
||||
radius: px(99.),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().muted_foreground,
|
||||
bg: if selected {
|
||||
cx.theme().background
|
||||
} else {
|
||||
cx.theme().transparent
|
||||
},
|
||||
radius: cx.theme().radius,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().muted_foreground,
|
||||
bg: cx.theme().transparent,
|
||||
radius: cx.theme().radius,
|
||||
border_color: if selected {
|
||||
cx.theme().border
|
||||
} else {
|
||||
cx.theme().transparent
|
||||
},
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Tab {
|
||||
id: ElementId,
|
||||
|
|
@ -12,8 +326,11 @@ pub struct Tab {
|
|||
label: AnyElement,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
variant: TabVariant,
|
||||
size: Size,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
|
|
@ -21,15 +338,42 @@ impl Tab {
|
|||
let id: ElementId = id.into();
|
||||
Self {
|
||||
id: id.clone(),
|
||||
base: div().id(id).gap_1().py_1p5().px_3().h(px(30.)),
|
||||
base: div().id(id).gap_1(),
|
||||
label: label.into_any_element(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
variant: TabVariant::default(),
|
||||
size: Size::default(),
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set Tab Variant.
|
||||
pub fn variant(mut self, variant: TabVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Pill variant.
|
||||
pub fn pill(mut self) -> Self {
|
||||
self.variant = TabVariant::Pill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Segmented variant.
|
||||
pub fn segmented(mut self) -> Self {
|
||||
self.variant = TabVariant::Segmented;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Underline variant.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.variant = TabVariant::Underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the left side of the tab
|
||||
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
|
||||
self.prefix = Some(prefix.into());
|
||||
|
|
@ -47,6 +391,15 @@ impl Tab {
|
|||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click handler for the tab.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Arc::new(on_click));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Tab {
|
||||
|
|
@ -74,14 +427,32 @@ impl Styled for Tab {
|
|||
}
|
||||
}
|
||||
|
||||
impl Sizable for Tab {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Tab {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (text_color, bg_color) = match (self.selected, self.disabled) {
|
||||
(true, false) => (cx.theme().tab_active_foreground, cx.theme().tab_active),
|
||||
(false, false) => (cx.theme().muted_foreground, cx.theme().tab),
|
||||
// disabled
|
||||
(true, true) => (cx.theme().muted_foreground, cx.theme().tab_active),
|
||||
(false, true) => (cx.theme().muted_foreground, cx.theme().tab),
|
||||
let mut tab_style = if self.selected {
|
||||
self.variant.selected(cx)
|
||||
} else {
|
||||
self.variant.normal(cx)
|
||||
};
|
||||
let mut hover_style = self.variant.hovered(cx);
|
||||
if self.disabled {
|
||||
tab_style = self.variant.disabled(self.selected, cx);
|
||||
hover_style = self.variant.disabled(self.selected, cx);
|
||||
}
|
||||
let inner_paddings = self.variant.inner_paddings(self.size);
|
||||
let inner_margins = self.variant.inner_margins(self.size);
|
||||
let inner_height = self.variant.inner_height(self.size);
|
||||
|
||||
let height = match self.size {
|
||||
Size::Small | Size::XSmall => px(24.),
|
||||
_ => px(31.),
|
||||
};
|
||||
|
||||
self.base
|
||||
|
|
@ -90,16 +461,53 @@ impl RenderOnce for Tab {
|
|||
.flex_shrink_0()
|
||||
.cursor_pointer()
|
||||
.overflow_hidden()
|
||||
.text_color(text_color)
|
||||
.bg(bg_color)
|
||||
.border_x_1()
|
||||
.border_color(cx.theme().transparent)
|
||||
.when(self.selected, |this| this.border_color(cx.theme().border))
|
||||
.text_sm()
|
||||
.when_some(self.prefix, |this, prefix| {
|
||||
this.child(prefix).text_color(text_color)
|
||||
.line_height(height)
|
||||
.h(height)
|
||||
.overflow_hidden()
|
||||
.text_color(tab_style.fg)
|
||||
.bg(tab_style.bg)
|
||||
.border_l(tab_style.borders.left)
|
||||
.border_r(tab_style.borders.right)
|
||||
.border_t(tab_style.borders.top)
|
||||
.border_b(tab_style.borders.bottom)
|
||||
.border_color(tab_style.border_color)
|
||||
.rounded(tab_style.radius)
|
||||
.when(tab_style.shadow, |this| this.shadow_sm())
|
||||
.when(!self.selected && !self.disabled, |this| {
|
||||
this.hover(|this| {
|
||||
this.text_color(hover_style.fg)
|
||||
.bg(hover_style.bg)
|
||||
.border_l(hover_style.borders.left)
|
||||
.border_r(hover_style.borders.right)
|
||||
.border_t(hover_style.borders.top)
|
||||
.border_b(hover_style.borders.bottom)
|
||||
.border_color(hover_style.border_color)
|
||||
.rounded(tab_style.radius)
|
||||
})
|
||||
})
|
||||
.child(div().text_ellipsis().child(self.label))
|
||||
.text_sm()
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
div()
|
||||
.h(inner_height)
|
||||
.line_height(inner_height)
|
||||
.paddings(inner_paddings)
|
||||
.margins(inner_margins)
|
||||
.text_ellipsis()
|
||||
.flex_shrink_0()
|
||||
.child(self.label)
|
||||
.bg(tab_style.inner_bg)
|
||||
.rounded(tab_style.inner_radius)
|
||||
.hover(|this| {
|
||||
this.bg(hover_style.inner_bg)
|
||||
.rounded(hover_style.inner_radius)
|
||||
}),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||
.when(!self.disabled, |this| {
|
||||
this.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
use crate::{h_flex, ActiveTheme};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{h_flex, ActiveTheme, Selectable, Sizable, Size, StyledExt};
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, AnyElement, App, Div, ElementId, IntoElement, ParentElement, RenderOnce, ScrollHandle,
|
||||
StatefulInteractiveElement as _, Styled, Window,
|
||||
div, rems, AbsoluteLength, AnyElement, App, Div, Edges, ElementId, IntoElement, ParentElement,
|
||||
RenderOnce, ScrollHandle, StatefulInteractiveElement as _, Styled, Window,
|
||||
};
|
||||
use gpui::{px, InteractiveElement};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::{Tab, TabVariant};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
base: Div,
|
||||
|
|
@ -14,7 +18,12 @@ pub struct TabBar {
|
|||
scroll_handle: ScrollHandle,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
children: SmallVec<[Tab; 2]>,
|
||||
last_empty_space: Option<AnyElement>,
|
||||
selected_index: Option<usize>,
|
||||
variant: TabVariant,
|
||||
size: Size,
|
||||
on_click: Option<Arc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
|
|
@ -26,9 +35,38 @@ impl TabBar {
|
|||
scroll_handle: ScrollHandle::new(),
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
variant: TabVariant::default(),
|
||||
size: Size::default(),
|
||||
last_empty_space: None,
|
||||
selected_index: None,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the Tab variant, all children will inherit the variant.
|
||||
pub fn variant(mut self, variant: TabVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Pill, all children will inherit the variant.
|
||||
pub fn pill(mut self) -> Self {
|
||||
self.variant = TabVariant::Pill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Segmented, all children will inherit the variant.
|
||||
pub fn segmented(mut self) -> Self {
|
||||
self.variant = TabVariant::Segmented;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Underline, all children will inherit the variant.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.variant = TabVariant::Underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Track the scroll of the TabBar
|
||||
pub fn track_scroll(mut self, scroll_handle: ScrollHandle) -> Self {
|
||||
self.scroll_handle = scroll_handle;
|
||||
|
|
@ -46,11 +84,37 @@ impl TabBar {
|
|||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for TabBar {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
/// Add children of the TabBar, all children will inherit the variant.
|
||||
pub fn children(mut self, children: impl IntoIterator<Item = Tab>) -> Self {
|
||||
self.children.extend(children);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add child of the TabBar, tab will inherit the variant.
|
||||
pub fn child(mut self, child: Tab) -> Self {
|
||||
self.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the selected index of the TabBar.
|
||||
pub fn selected_index(mut self, index: usize) -> Self {
|
||||
self.selected_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the last empty space element of the TabBar
|
||||
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
|
||||
self.last_empty_space = Some(last_empty_space.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the on_click callback of the TabBar, the first parameter is the index of the clicked tab.
|
||||
///
|
||||
/// When this is set, the children's on_click will be ignored.
|
||||
pub fn on_click(mut self, on_click: impl Fn(&usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_click = Some(Arc::new(on_click));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +124,38 @@ impl Styled for TabBar {
|
|||
}
|
||||
}
|
||||
|
||||
impl Sizable for TabBar {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let default_gap = match self.size {
|
||||
Size::Small | Size::XSmall => px(8.),
|
||||
_ => px(12.),
|
||||
};
|
||||
let (bg, paddings, gap) = match self.variant {
|
||||
TabVariant::Tab => {
|
||||
let padding = Edges::all(AbsoluteLength::Pixels(px(0.)));
|
||||
(cx.theme().tab_bar, padding, px(0.))
|
||||
}
|
||||
TabVariant::Pill => {
|
||||
let padding = Edges::all(AbsoluteLength::Rems(rems(0.25)));
|
||||
(cx.theme().transparent, padding, default_gap)
|
||||
}
|
||||
TabVariant::Segmented => {
|
||||
let padding = Edges::all(AbsoluteLength::Rems(rems(0.25)));
|
||||
(cx.theme().accent, padding, default_gap / 2.)
|
||||
}
|
||||
TabVariant::Underline => {
|
||||
let padding = Edges::all(AbsoluteLength::Pixels(px(0.)));
|
||||
(cx.theme().transparent, padding, default_gap / 2.)
|
||||
}
|
||||
};
|
||||
|
||||
self.base
|
||||
.id(self.id)
|
||||
.group("tab-bar")
|
||||
|
|
@ -69,16 +163,26 @@ impl RenderOnce for TabBar {
|
|||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.bg(cx.theme().tab_bar)
|
||||
.bg(bg)
|
||||
.paddings(paddings)
|
||||
.text_color(cx.theme().tab_foreground)
|
||||
.child(
|
||||
div()
|
||||
.id("border-b")
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.size_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border),
|
||||
.when(
|
||||
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
|
||||
|this| {
|
||||
this.child(
|
||||
div()
|
||||
.id("border-b")
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.size_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(
|
||||
self.variant == TabVariant::Pill || self.variant == TabVariant::Segmented,
|
||||
|this| this.rounded(cx.theme().radius),
|
||||
)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
|
|
@ -87,7 +191,26 @@ impl RenderOnce for TabBar {
|
|||
.flex_grow()
|
||||
.overflow_x_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.children(self.children),
|
||||
.gap(gap)
|
||||
.children(
|
||||
self.children
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(move |(ix, child)| {
|
||||
child
|
||||
.variant(self.variant)
|
||||
.with_size(self.size)
|
||||
.when_some(self.selected_index, |this, selected_ix| {
|
||||
this.selected(selected_ix == ix)
|
||||
})
|
||||
.when_some(self.on_click.clone(), move |this, on_click| {
|
||||
this.on_click(move |_, window, cx| {
|
||||
on_click(&ix, window, cx)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
.children(self.last_empty_space),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ pub struct ThemeColor {
|
|||
impl ThemeColor {
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
accent: hsl(240.0, 5.0, 96.0),
|
||||
accent: hsl(240.0, 4.8, 93.9),
|
||||
accent_foreground: hsl(240.0, 5.9, 10.0),
|
||||
accordion: hsl(0.0, 0.0, 100.0),
|
||||
accordion_active: hsl(240.0, 5.9, 90.0),
|
||||
|
|
|
|||
Loading…
Reference in a new issue