tab: Improve segmented tab style and add for icon mode tab support. (#688)

<img width="873" alt="image"
src="https://github.com/user-attachments/assets/f616a9ba-0a60-49f5-8291-04f79f5b582c"
/>
<img width="948" alt="image"
src="https://github.com/user-attachments/assets/5ba1b50e-5b5f-45e1-89ca-683bcac8331a"
/>
<img width="929" alt="image"
src="https://github.com/user-attachments/assets/6545b67f-6c53-482f-a3ac-b7929c9bec99"
/>
<img width="1153" alt="image"
src="https://github.com/user-attachments/assets/06abb411-43a7-4472-8e58-d7c8032bed7b"
/>
This commit is contained in:
Jason Lee 2025-03-05 15:41:03 +08:00 committed by GitHub
parent 6bf26f6f72
commit 4c96679cf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 122 additions and 86 deletions

View file

@ -156,7 +156,7 @@ impl Render for TabsStory {
})) }))
.child(Tab::new("Account")) .child(Tab::new("Account"))
.child(Tab::new("Profile").disabled(true)) .child(Tab::new("Profile").disabled(true))
.child(Tab::new("Documents")) .child(Tab::new("Documents & Files"))
.child(Tab::new("Mail")) .child(Tab::new("Mail"))
.child(Tab::new("Appearance")) .child(Tab::new("Appearance"))
.child(Tab::new("Settings")) .child(Tab::new("Settings"))
@ -174,16 +174,10 @@ impl Render for TabsStory {
.on_click(cx.listener(|this, ix: &usize, window, cx| { .on_click(cx.listener(|this, ix: &usize, window, cx| {
this.set_active_tab(*ix, window, cx); this.set_active_tab(*ix, window, cx);
})) }))
.children(vec![ .child(IconName::Bot)
"Account", .child(IconName::Calendar)
"Profile", .child(IconName::Map)
"Documents", .children(vec!["Appearance", "Settings", "About", "License"]),
"Mail",
"Appearance",
"Settings",
"About",
"License",
]),
), ),
) )
.child( .child(

View file

@ -528,7 +528,7 @@ impl Render for PopupMenu {
v_flex() v_flex()
.id("popup-menu") .id("popup-menu")
.key_context("PopupMenu") .key_context("PopupMenu")
.track_focus(&self.focus_handle) // .track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_prev)) .on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))

View file

@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{ActiveTheme, Selectable, Sizable, Size, StyledExt}; use crate::{h_flex, ActiveTheme, Icon, IconName, Selectable, Sizable, Size, StyledExt};
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, px, AnyElement, App, ClickEvent, Div, Edges, ElementId, Hsla, InteractiveElement, div, px, AnyElement, App, ClickEvent, Div, Edges, ElementId, Hsla, InteractiveElement,
@ -48,28 +48,17 @@ impl TabVariant {
fn height(&self, size: Size) -> Pixels { fn height(&self, size: Size) -> Pixels {
match size { match size {
Size::XSmall => match self { Size::XSmall => match self {
TabVariant::Tab => px(22.),
TabVariant::Pill => px(20.),
TabVariant::Segmented => px(20.),
TabVariant::Underline => px(26.), TabVariant::Underline => px(26.),
_ => px(20.),
}, },
Size::Small => match self { Size::Small => match self {
TabVariant::Tab => px(24.),
TabVariant::Pill => px(24.),
TabVariant::Segmented => px(24.),
TabVariant::Underline => px(30.), TabVariant::Underline => px(30.),
_ => px(24.),
}, },
Size::Large => match self { Size::Large => px(42.),
TabVariant::Tab => px(36.),
TabVariant::Pill => px(36.),
TabVariant::Segmented => px(36.),
TabVariant::Underline => px(42.),
},
_ => match self { _ => match self {
TabVariant::Tab => px(30.),
TabVariant::Pill => px(31.),
TabVariant::Segmented => px(30.),
TabVariant::Underline => px(36.), TabVariant::Underline => px(36.),
_ => px(32.),
}, },
} }
} }
@ -77,54 +66,45 @@ impl TabVariant {
fn inner_height(&self, size: Size) -> Pixels { fn inner_height(&self, size: Size) -> Pixels {
match size { match size {
Size::XSmall => match self { Size::XSmall => match self {
TabVariant::Tab => px(20.), TabVariant::Tab | TabVariant::Pill => px(20.),
TabVariant::Pill => px(20.), TabVariant::Segmented => px(16.),
TabVariant::Segmented => px(20.),
TabVariant::Underline => px(20.), TabVariant::Underline => px(20.),
}, },
Size::Small => match self { Size::Small => match self {
TabVariant::Tab => px(24.), TabVariant::Tab | TabVariant::Pill => px(24.),
TabVariant::Pill => px(24.), TabVariant::Segmented => px(20.),
TabVariant::Segmented => px(24.),
TabVariant::Underline => px(22.), TabVariant::Underline => px(22.),
}, },
Size::Large => match self { Size::Large => match self {
TabVariant::Tab => px(36.), TabVariant::Tab | TabVariant::Pill => px(36.),
TabVariant::Pill => px(36.), TabVariant::Segmented => px(34.),
TabVariant::Segmented => px(36.),
TabVariant::Underline => px(30.), TabVariant::Underline => px(30.),
}, },
_ => match self { _ => match self {
TabVariant::Tab => px(30.), TabVariant::Tab => px(30.),
TabVariant::Pill => px(31.), TabVariant::Pill => px(26.),
TabVariant::Segmented => px(30.), TabVariant::Segmented => px(24.),
TabVariant::Underline => px(24.), TabVariant::Underline => px(24.),
}, },
} }
} }
fn inner_paddings(&self, size: Size) -> Edges<Pixels> { fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
match size { let mut px = match size {
Size::XSmall => Edges { Size::XSmall => px(8.),
left: px(8.), Size::Small => px(12.),
right: px(8.), Size::Large => px(20.),
..Default::default() _ => px(16.),
}, };
Size::Small => Edges {
left: px(12.), if *self == TabVariant::Segmented {
right: px(12.), px = px / 2.;
..Default::default() }
},
Size::Large => Edges { Edges {
left: px(20.), left: px,
right: px(20.), right: px,
..Default::default() ..Default::default()
},
_ => Edges {
left: px(16.),
right: px(16.),
..Default::default()
},
} }
} }
@ -152,7 +132,7 @@ impl TabVariant {
fn normal(&self, cx: &App) -> TabStyle { fn normal(&self, cx: &App) -> TabStyle {
match self { match self {
TabVariant::Tab => TabStyle { TabVariant::Tab => TabStyle {
fg: cx.theme().foreground, fg: cx.theme().tab_foreground,
bg: cx.theme().transparent, bg: cx.theme().transparent,
borders: Edges { borders: Edges {
top: px(1.), top: px(1.),
@ -164,7 +144,7 @@ impl TabVariant {
..Default::default() ..Default::default()
}, },
TabVariant::Pill => TabStyle { TabVariant::Pill => TabStyle {
fg: cx.theme().foreground, fg: cx.theme().tab_foreground,
bg: cx.theme().transparent, bg: cx.theme().transparent,
borders: Edges::all(px(1.)), borders: Edges::all(px(1.)),
border_color: cx.theme().border, border_color: cx.theme().border,
@ -172,13 +152,13 @@ impl TabVariant {
..Default::default() ..Default::default()
}, },
TabVariant::Segmented => TabStyle { TabVariant::Segmented => TabStyle {
fg: cx.theme().foreground, fg: cx.theme().tab_foreground,
bg: cx.theme().transparent, bg: cx.theme().transparent,
radius: cx.theme().radius, inner_radius: cx.theme().radius,
..Default::default() ..Default::default()
}, },
TabVariant::Underline => TabStyle { TabVariant::Underline => TabStyle {
fg: cx.theme().foreground, fg: cx.theme().tab_foreground,
bg: cx.theme().transparent, bg: cx.theme().transparent,
radius: px(0.), radius: px(0.),
inner_bg: cx.theme().transparent, inner_bg: cx.theme().transparent,
@ -193,7 +173,7 @@ impl TabVariant {
} }
} }
fn hovered(&self, cx: &App) -> TabStyle { fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
match self { match self {
TabVariant::Tab => TabStyle { TabVariant::Tab => TabStyle {
fg: cx.theme().tab_foreground, fg: cx.theme().tab_foreground,
@ -217,8 +197,13 @@ impl TabVariant {
}, },
TabVariant::Segmented => TabStyle { TabVariant::Segmented => TabStyle {
fg: cx.theme().tab_foreground, fg: cx.theme().tab_foreground,
bg: cx.theme().transparent, bg: cx.theme().tab_bar,
radius: cx.theme().radius, inner_bg: if selected {
cx.theme().background
} else {
cx.theme().transparent
},
inner_radius: cx.theme().radius,
..Default::default() ..Default::default()
}, },
TabVariant::Underline => TabStyle { TabVariant::Underline => TabStyle {
@ -261,8 +246,9 @@ impl TabVariant {
}, },
TabVariant::Segmented => TabStyle { TabVariant::Segmented => TabStyle {
fg: cx.theme().tab_active_foreground, fg: cx.theme().tab_active_foreground,
bg: cx.theme().background, bg: cx.theme().tab_bar,
radius: cx.theme().radius, inner_radius: cx.theme().radius,
inner_bg: cx.theme().background,
shadow: true, shadow: true,
..Default::default() ..Default::default()
}, },
@ -311,12 +297,13 @@ impl TabVariant {
}, },
TabVariant::Segmented => TabStyle { TabVariant::Segmented => TabStyle {
fg: cx.theme().muted_foreground, fg: cx.theme().muted_foreground,
bg: if selected { bg: cx.theme().tab_bar,
inner_bg: if selected {
cx.theme().background cx.theme().background
} else { } else {
cx.theme().transparent cx.theme().transparent
}, },
radius: cx.theme().radius, inner_radius: cx.theme().radius,
..Default::default() ..Default::default()
}, },
TabVariant::Underline => TabStyle { TabVariant::Underline => TabStyle {
@ -343,6 +330,7 @@ pub struct Tab {
id: ElementId, id: ElementId,
base: Div, base: Div,
label: SharedString, label: SharedString,
icon: Option<Icon>,
prefix: Option<AnyElement>, prefix: Option<AnyElement>,
suffix: Option<AnyElement>, suffix: Option<AnyElement>,
children: Vec<AnyElement>, children: Vec<AnyElement>,
@ -373,12 +361,25 @@ impl From<SharedString> for Tab {
} }
} }
impl Tab { impl From<Icon> for Tab {
pub fn new(label: impl Into<SharedString>) -> Self { fn from(icon: Icon) -> Self {
Self::icon(icon)
}
}
impl From<IconName> for Tab {
fn from(icon_name: IconName) -> Self {
Self::icon(Icon::new(icon_name))
}
}
impl Default for Tab {
fn default() -> Self {
Self { Self {
id: ElementId::Integer(0), id: ElementId::Integer(0),
base: div().gap_1(), base: div().gap_1(),
label: label.into(), label: SharedString::new(""),
icon: None,
children: Vec::new(), children: Vec::new(),
disabled: false, disabled: false,
selected: false, selected: false,
@ -389,6 +390,22 @@ impl Tab {
on_click: None, on_click: None,
} }
} }
}
impl Tab {
/// Create a new tab with a label.
pub fn new(label: impl Into<SharedString>) -> Self {
let mut this = Self::default();
this.label = label.into();
this
}
/// Create a Icon tab.
pub fn icon(icon: impl Into<Icon>) -> Self {
let mut this = Self::default();
this.icon = Some(icon.into());
this
}
/// Set id to the tab. /// Set id to the tab.
pub fn id(mut self, id: impl Into<ElementId>) -> Self { pub fn id(mut self, id: impl Into<ElementId>) -> Self {
@ -493,7 +510,7 @@ impl RenderOnce for Tab {
} else { } else {
self.variant.normal(cx) self.variant.normal(cx)
}; };
let mut hover_style = self.variant.hovered(cx); let mut hover_style = self.variant.hovered(self.selected, cx);
if self.disabled { if self.disabled {
tab_style = self.variant.disabled(self.selected, cx); tab_style = self.variant.disabled(self.selected, cx);
hover_style = self.variant.disabled(self.selected, cx); hover_style = self.variant.disabled(self.selected, cx);
@ -512,7 +529,6 @@ impl RenderOnce for Tab {
.flex_shrink_0() .flex_shrink_0()
.cursor_pointer() .cursor_pointer()
.overflow_hidden() .overflow_hidden()
.line_height(height)
.h(height) .h(height)
.overflow_hidden() .overflow_hidden()
.text_color(tab_style.fg) .text_color(tab_style.fg)
@ -528,7 +544,6 @@ impl RenderOnce for Tab {
.border_b(tab_style.borders.bottom) .border_b(tab_style.borders.bottom)
.border_color(tab_style.border_color) .border_color(tab_style.border_color)
.rounded(tab_style.radius) .rounded(tab_style.radius)
.when(tab_style.shadow, |this| this.shadow_sm())
.when(!self.selected && !self.disabled, |this| { .when(!self.selected && !self.disabled, |this| {
this.hover(|this| { this.hover(|this| {
this.text_color(hover_style.fg) this.text_color(hover_style.fg)
@ -543,17 +558,33 @@ impl RenderOnce for Tab {
}) })
.when_some(self.prefix, |this, prefix| this.child(prefix)) .when_some(self.prefix, |this, prefix| this.child(prefix))
.child( .child(
div() h_flex()
.h(inner_height) .h(inner_height)
.line_height(inner_height) .line_height(inner_height)
.paddings(inner_paddings) .items_center()
.justify_center()
.overflow_hidden()
.margins(inner_margins) .margins(inner_margins)
.text_ellipsis() .text_ellipsis()
.flex_shrink_0() .flex_shrink_0()
.when(has_label, |this| this.child(self.label)) .map(|this| match self.icon {
.when(!has_label, |this| this.children(self.children)) Some(icon) => {
this.w(inner_height * 1.25)
.child(icon.map(|this| match self.size {
Size::XSmall => this.size_2p5(),
Size::Small => this.size_3p5(),
Size::Large => this.size_5(),
_ => this.size_4(),
}))
}
None => this
.paddings(inner_paddings)
.when(has_label, |this| this.child(self.label))
.when(!has_label, |this| this.children(self.children)),
})
.bg(tab_style.inner_bg) .bg(tab_style.inner_bg)
.rounded(tab_style.inner_radius) .rounded(tab_style.inner_radius)
.when(tab_style.shadow, |this| this.shadow_sm())
.hover(|this| { .hover(|this| {
this.bg(hover_style.inner_bg) this.bg(hover_style.inner_bg)
.rounded(hover_style.inner_radius) .rounded(hover_style.inner_radius)

View file

@ -148,8 +148,19 @@ impl RenderOnce for TabBar {
(cx.theme().transparent, padding, default_gap) (cx.theme().transparent, padding, default_gap)
} }
TabVariant::Segmented => { TabVariant::Segmented => {
let padding = Edges::all(px(4.)); let padding_x = match self.size {
(cx.theme().accent, padding, default_gap / 2.) Size::XSmall => px(3.),
Size::Small => px(3.),
Size::Large => px(6.),
_ => px(4.),
};
let padding = Edges {
left: padding_x,
right: padding_x,
..Default::default()
};
(cx.theme().tab_bar, padding, px(2.))
} }
TabVariant::Underline => { TabVariant::Underline => {
let padding = Edges::all(px(0.)); let padding = Edges::all(px(0.));

View file

@ -274,8 +274,8 @@ impl ThemeColor {
tab: gpui::transparent_black(), tab: gpui::transparent_black(),
tab_active: hsl(0.0, 0.0, 100.0), tab_active: hsl(0.0, 0.0, 100.0),
tab_active_foreground: hsl(240.0, 10., 3.9), tab_active_foreground: hsl(240.0, 10., 3.9),
tab_bar: hsl(240.0, 4.8, 95.9), tab_bar: hsl(240.0, 14.3, 95.9),
tab_foreground: hsl(240.0, 10., 3.9), tab_foreground: hsl(240.0, 10., 33.9),
table: hsl(0.0, 0.0, 100.), table: hsl(0.0, 0.0, 100.),
table_active: hsl(211.0, 97.0, 85.0).opacity(0.2), table_active: hsl(211.0, 97.0, 85.0).opacity(0.2),
table_active_border: hsl(211.0, 97.0, 85.0), table_active_border: hsl(211.0, 97.0, 85.0),