tab: Improve TabBar API. (#678)

- Fixed scroll support for TabBar.
- Remove `id` argument from Tab, it will set from TabBar iter.
- Add `id` to TabBar to ensure scroll_handle state will store correct.
- Update `label` argument of Tab, to use `impl Into<SharedString>` type
for better use.
- Added `child` to Tab for render custom element as label.
This commit is contained in:
Jason Lee 2025-03-03 19:48:28 +08:00 committed by GitHub
parent bc89ce2536
commit c4331c9419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 110 additions and 64 deletions

View file

@ -100,7 +100,7 @@ impl Render for TabsStory {
)
.child(
section("Tabs", cx).child(
TabBar::new()
TabBar::new("tabs")
.w_full()
.with_size(self.size)
.selected_index(self.active_tab_ix)
@ -123,12 +123,14 @@ impl Render for TabsStory {
.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"))
.child(Tab::new("Account"))
.child(Tab::new("Profile").disabled(true))
.child(Tab::new("Documents"))
.child(Tab::new("Mail"))
.child(Tab::new("Appearance"))
.child(Tab::new("Settings"))
.child(Tab::new("About"))
.child(Tab::new("License"))
.suffix(
h_flex()
.mx_1()
@ -144,7 +146,7 @@ impl Render for TabsStory {
)
.child(
section("Pill Tabs", cx).child(
TabBar::new()
TabBar::new("pill")
.w_full()
.pill()
.with_size(self.size)
@ -152,17 +154,19 @@ impl Render for TabsStory {
.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(Tab::new("Account"))
.child(Tab::new("Profile").disabled(true))
.child(Tab::new("Documents"))
.child(Tab::new("Mail"))
.child(Tab::new("Appearance"))
.child(Tab::new("Settings"))
.child(Tab::new("About"))
.child(Tab::new("License")),
),
)
.child(
section("Segmented Tabs", cx).child(
TabBar::new()
TabBar::new("segmented")
.w_full()
.segmented()
.with_size(self.size)
@ -177,13 +181,17 @@ impl Render for TabsStory {
"Mail",
"Appearance",
"Settings",
"About",
"License",
]),
),
)
.child(
section("Underline Tabs", cx).child(
TabBar::new()
TabBar::new("underline")
.w_full()
.px_2()
.mx_3()
.underline()
.with_size(self.size)
.selected_index(self.active_tab_ix)
@ -195,7 +203,9 @@ impl Render for TabsStory {
.child("Documents")
.child("Mail")
.child("Appearance")
.child("Settings"),
.child("Settings")
.child("About")
.child("License"),
),
)
}

View file

@ -650,7 +650,7 @@ impl TabPanel {
let tabs_count = self.panels.len();
TabBar::new()
TabBar::new("tab-bar")
.mt(-px(1.))
.track_scroll(self.tab_bar_scroll_handle.clone())
.when(
@ -673,9 +673,6 @@ 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;
@ -690,12 +687,16 @@ impl TabPanel {
}
Some(
Tab::new(("tab", ix), panel.title(window, cx))
Tab::new("")
.child(panel.title(window, cx))
.py_2()
.selected(active)
.disabled(disabled)
.when(!disabled, |this| {
this.when(state.draggable, |this| {
this.on_click(cx.listener(move |view, _, window, cx| {
view.set_active_ix(ix, window, cx);
}))
.when(state.draggable, |this| {
this.on_drag(
DragPanel::new(panel.clone(), view.clone()),
|drag, _, _, cx| {

View file

@ -4,8 +4,8 @@ use crate::{ActiveTheme, Selectable, Sizable, Size, StyledExt};
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, AnyElement, App, ClickEvent, Div, Edges, ElementId, Hsla, InteractiveElement,
IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString, Stateful,
StatefulInteractiveElement, Styled, Window,
IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement,
Styled, Window,
};
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
@ -45,6 +45,23 @@ impl Default for TabStyle {
}
impl TabVariant {
fn height(&self, size: Size) -> Pixels {
match size {
Size::Small | Size::XSmall => match self {
TabVariant::Tab => px(24.),
TabVariant::Pill => px(24.),
TabVariant::Segmented => px(24.),
TabVariant::Underline => px(30.),
},
_ => match self {
TabVariant::Tab => px(30.),
TabVariant::Pill => px(31.),
TabVariant::Segmented => px(30.),
TabVariant::Underline => px(36.),
},
}
}
fn inner_height(&self, size: Size) -> Pixels {
match size {
Size::Small | Size::XSmall => match self {
@ -57,7 +74,7 @@ impl TabVariant {
TabVariant::Tab => px(30.),
TabVariant::Pill => px(31.),
TabVariant::Segmented => px(30.),
TabVariant::Underline => px(25.),
TabVariant::Underline => px(24.),
},
}
}
@ -122,7 +139,8 @@ impl TabVariant {
},
_ => match self {
TabVariant::Underline => Edges {
bottom: px(4.),
top: px(5.),
bottom: px(3.),
..Default::default()
},
_ => Edges::all(px(0.)),
@ -233,7 +251,7 @@ impl TabVariant {
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: cx.theme().accent_foreground,
fg: cx.theme().primary,
bg: cx.theme().transparent,
borders: Edges::all(px(1.)),
border_color: cx.theme().primary,
@ -322,10 +340,11 @@ impl TabVariant {
#[derive(IntoElement)]
pub struct Tab {
id: ElementId,
base: Stateful<Div>,
label: AnyElement,
base: Div,
label: SharedString,
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
children: Vec<AnyElement>,
variant: TabVariant,
size: Size,
disabled: bool,
@ -336,31 +355,30 @@ pub struct Tab {
impl From<&'static str> for Tab {
fn from(label: &'static str) -> Self {
let label = SharedString::from(label);
Self::new(label.clone(), label)
Self::new(label)
}
}
impl From<String> for Tab {
fn from(label: String) -> Self {
let label = SharedString::from(label);
Self::new(label.clone(), label)
Self::new(label)
}
}
impl From<SharedString> for Tab {
fn from(label: SharedString) -> Self {
let label = SharedString::from(label);
Self::new(label.clone(), label)
Self::new(label)
}
}
impl Tab {
pub fn new(id: impl Into<ElementId>, label: impl IntoElement) -> Self {
let id: ElementId = id.into();
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
id: id.clone(),
base: div().id(id).gap_1(),
label: label.into_any_element(),
id: ElementId::Integer(0),
base: div().gap_1(),
label: label.into(),
children: Vec::new(),
disabled: false,
selected: false,
prefix: None,
@ -371,6 +389,12 @@ impl Tab {
}
}
/// Set id to the tab.
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
self.id = id.into();
self
}
/// Set Tab Variant.
pub fn variant(mut self, variant: TabVariant) -> Self {
self.variant = variant;
@ -423,6 +447,12 @@ impl Tab {
}
}
impl ParentElement for Tab {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl Selectable for Tab {
fn element_id(&self) -> &ElementId {
&self.id
@ -470,13 +500,11 @@ impl RenderOnce for Tab {
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.),
};
let height = self.variant.height(self.size);
let has_label = !self.label.is_empty();
self.base
.id(self.id)
.flex()
.items_center()
.flex_shrink_0()
@ -516,7 +544,8 @@ impl RenderOnce for Tab {
.margins(inner_margins)
.text_ellipsis()
.flex_shrink_0()
.child(self.label)
.when(has_label, |this| this.child(self.label))
.when(!has_label, |this| this.children(self.children))
.bg(tab_style.inner_bg)
.rounded(tab_style.inner_radius)
.hover(|this| {

View file

@ -3,8 +3,8 @@ use std::sync::Arc;
use crate::{h_flex, ActiveTheme, Selectable, Sizable, Size, StyledExt};
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, rems, AbsoluteLength, AnyElement, App, Div, Edges, IntoElement, ParentElement, RenderOnce,
ScrollHandle, StatefulInteractiveElement as _, Styled, Window,
div, AnyElement, App, Div, Edges, ElementId, IntoElement, ParentElement, RenderOnce,
ScrollHandle, Stateful, StatefulInteractiveElement as _, Styled, Window,
};
use gpui::{px, InteractiveElement};
use smallvec::SmallVec;
@ -13,12 +13,12 @@ use super::{Tab, TabVariant};
#[derive(IntoElement)]
pub struct TabBar {
base: Div,
scroll_handle: ScrollHandle,
base: Stateful<Div>,
scroll_handle: Option<ScrollHandle>,
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
children: SmallVec<[Tab; 2]>,
last_empty_space: Option<AnyElement>,
last_empty_space: AnyElement,
selected_index: Option<usize>,
variant: TabVariant,
size: Size,
@ -27,16 +27,16 @@ pub struct TabBar {
impl TabBar {
/// Create a new TabBar.
pub fn new() -> Self {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
base: div().px(px(-1.)),
base: div().id(id).px(px(-1.)),
children: SmallVec::new(),
scroll_handle: ScrollHandle::new(),
scroll_handle: None,
prefix: None,
suffix: None,
variant: TabVariant::default(),
size: Size::default(),
last_empty_space: None,
last_empty_space: div().w_3().into_any_element(),
selected_index: None,
on_click: None,
}
@ -68,7 +68,7 @@ impl TabBar {
/// Track the scroll of the TabBar
pub fn track_scroll(mut self, scroll_handle: ScrollHandle) -> Self {
self.scroll_handle = scroll_handle;
self.scroll_handle = Some(scroll_handle);
self
}
@ -105,7 +105,7 @@ impl TabBar {
/// 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.last_empty_space = last_empty_space.into_any_element();
self
}
@ -139,31 +139,32 @@ impl RenderOnce for TabBar {
};
let (bg, paddings, gap) = match self.variant {
TabVariant::Tab => {
let padding = Edges::all(AbsoluteLength::Pixels(px(0.)));
let padding = Edges::all(px(0.));
(cx.theme().tab_bar, padding, px(0.))
}
TabVariant::Pill => {
let padding = Edges::all(AbsoluteLength::Rems(rems(0.25)));
let padding = Edges::all(px(0.));
(cx.theme().transparent, padding, default_gap)
}
TabVariant::Segmented => {
let padding = Edges::all(AbsoluteLength::Rems(rems(0.25)));
let padding = Edges::all(px(4.));
(cx.theme().accent, padding, default_gap / 2.)
}
TabVariant::Underline => {
let padding = Edges::all(AbsoluteLength::Pixels(px(0.)));
let padding = Edges::all(px(0.));
(cx.theme().transparent, padding, default_gap / 2.)
}
};
self.base
.group("tab-bar")
.w_full()
.relative()
.flex()
.flex_none()
.flex_nowrap()
.items_center()
.bg(bg)
.paddings(paddings)
.text_color(cx.theme().tab_foreground)
.when(
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
@ -172,6 +173,7 @@ impl RenderOnce for TabBar {
div()
.id("border-b")
.absolute()
.left_0()
.bottom_0()
.size_full()
.border_b_1()
@ -187,16 +189,20 @@ impl RenderOnce for TabBar {
.child(
h_flex()
.id("tabs")
.flex_grow()
.flex_1()
.overflow_x_scroll()
.track_scroll(&self.scroll_handle)
.when_some(self.scroll_handle, |this, scroll_handle| {
this.track_scroll(&scroll_handle)
})
.gap(gap)
.paddings(paddings)
.children(
self.children
.into_iter()
.enumerate()
.map(move |(ix, child)| {
child
.id(ix)
.variant(self.variant)
.with_size(self.size)
.when_some(self.selected_index, |this, selected_ix| {
@ -209,7 +215,7 @@ impl RenderOnce for TabBar {
})
}),
)
.children(self.last_empty_space),
.child(self.last_empty_space),
)
.when_some(self.suffix, |this, suffix| this.child(suffix))
}