
Follow-up to #29061 This PR ensures that the last tab does not flicker when either selecting. It also fixes an issue where the layout would shift in the new last tab when closing the last tab. https://github.com/user-attachments/assets/529a2a92-f25c-4ced-a992-fb6b2d3b5f61 This happened because in #29061, the left padding was removed due to issues with borders. However, the padding is relevant for the content to not shift (we are basically doing border-box sizing manually here). Instead, we need to remove the padding on the right side, as there is already a border present on the right side and this padding would make the last tab slightly larger than all other tabs. https://github.com/user-attachments/assets/c3a10b3c-4a1d-4160-9b68-7538207bb46e Release Notes: - Removed a small flicker when selecting or closing the last tab in a pane.
235 lines
7.4 KiB
Rust
235 lines
7.4 KiB
Rust
use std::cmp::Ordering;
|
|
|
|
use gpui::{AnyElement, IntoElement, Stateful};
|
|
use smallvec::SmallVec;
|
|
|
|
use crate::prelude::*;
|
|
|
|
/// The position of a [`Tab`] within a list of tabs.
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum TabPosition {
|
|
/// The tab is first in the list.
|
|
First,
|
|
|
|
/// The tab is in the middle of the list (i.e., it is not the first or last tab).
|
|
///
|
|
/// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
|
|
Middle(Ordering),
|
|
|
|
/// The tab is last in the list.
|
|
Last,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum TabCloseSide {
|
|
Start,
|
|
End,
|
|
}
|
|
|
|
#[derive(IntoElement, RegisterComponent)]
|
|
pub struct Tab {
|
|
div: Stateful<Div>,
|
|
selected: bool,
|
|
position: TabPosition,
|
|
close_side: TabCloseSide,
|
|
start_slot: Option<AnyElement>,
|
|
end_slot: Option<AnyElement>,
|
|
children: SmallVec<[AnyElement; 2]>,
|
|
}
|
|
|
|
impl Tab {
|
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
|
let id = id.into();
|
|
Self {
|
|
div: div()
|
|
.id(id.clone())
|
|
.debug_selector(|| format!("TAB-{}", id)),
|
|
selected: false,
|
|
position: TabPosition::First,
|
|
close_side: TabCloseSide::End,
|
|
start_slot: None,
|
|
end_slot: None,
|
|
children: SmallVec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn position(mut self, position: TabPosition) -> Self {
|
|
self.position = position;
|
|
self
|
|
}
|
|
|
|
pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
|
|
self.close_side = close_side;
|
|
self
|
|
}
|
|
|
|
pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
|
|
self.start_slot = element.into().map(IntoElement::into_any_element);
|
|
self
|
|
}
|
|
|
|
pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
|
|
self.end_slot = element.into().map(IntoElement::into_any_element);
|
|
self
|
|
}
|
|
|
|
pub fn content_height(cx: &App) -> Pixels {
|
|
DynamicSpacing::Base32.px(cx) - px(1.)
|
|
}
|
|
|
|
pub fn container_height(cx: &App) -> Pixels {
|
|
DynamicSpacing::Base32.px(cx)
|
|
}
|
|
}
|
|
|
|
impl InteractiveElement for Tab {
|
|
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
|
self.div.interactivity()
|
|
}
|
|
}
|
|
|
|
impl StatefulInteractiveElement for Tab {}
|
|
|
|
impl Toggleable for Tab {
|
|
fn toggle_state(mut self, selected: bool) -> Self {
|
|
self.selected = selected;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl ParentElement for Tab {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.children.extend(elements)
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Tab {
|
|
#[allow(refining_impl_trait)]
|
|
fn render(self, _: &mut Window, cx: &mut App) -> Stateful<Div> {
|
|
let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
|
|
false => (
|
|
cx.theme().colors().text_muted,
|
|
cx.theme().colors().tab_inactive_background,
|
|
cx.theme().colors().ghost_element_hover,
|
|
cx.theme().colors().ghost_element_active,
|
|
),
|
|
true => (
|
|
cx.theme().colors().text,
|
|
cx.theme().colors().tab_active_background,
|
|
cx.theme().colors().element_hover,
|
|
cx.theme().colors().element_active,
|
|
),
|
|
};
|
|
|
|
let (start_slot, end_slot) = {
|
|
let start_slot = h_flex()
|
|
.size(px(12.)) // use px over rem from size_3
|
|
.justify_center()
|
|
.children(self.start_slot);
|
|
|
|
let end_slot = h_flex()
|
|
.size(px(12.)) // use px over rem from size_3
|
|
.justify_center()
|
|
.children(self.end_slot);
|
|
|
|
match self.close_side {
|
|
TabCloseSide::End => (start_slot, end_slot),
|
|
TabCloseSide::Start => (end_slot, start_slot),
|
|
}
|
|
};
|
|
|
|
self.div
|
|
.h(Tab::container_height(cx))
|
|
.bg(tab_bg)
|
|
.border_color(cx.theme().colors().border)
|
|
.map(|this| match self.position {
|
|
TabPosition::First => {
|
|
if self.selected {
|
|
this.pl_px().border_r_1().pb_px()
|
|
} else {
|
|
this.pl_px().pr_px().border_b_1()
|
|
}
|
|
}
|
|
TabPosition::Last => {
|
|
if self.selected {
|
|
this.border_l_1().border_r_1().pb_px()
|
|
} else {
|
|
this.pl_px().border_b_1().border_r_1()
|
|
}
|
|
}
|
|
TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
|
|
TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
|
|
TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
|
|
})
|
|
.cursor_pointer()
|
|
.child(
|
|
h_flex()
|
|
.group("")
|
|
.relative()
|
|
.h(Tab::content_height(cx))
|
|
.px(DynamicSpacing::Base04.px(cx))
|
|
.gap(DynamicSpacing::Base04.rems(cx))
|
|
.text_color(text_color)
|
|
.child(start_slot)
|
|
.children(self.children)
|
|
.child(end_slot),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Component for Tab {
|
|
fn scope() -> ComponentScope {
|
|
ComponentScope::None
|
|
}
|
|
|
|
fn description() -> Option<&'static str> {
|
|
Some(
|
|
"A tab component that can be used in a tabbed interface, supporting different positions and states.",
|
|
)
|
|
}
|
|
|
|
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
|
Some(
|
|
v_flex()
|
|
.gap_6()
|
|
.children(vec![example_group_with_title(
|
|
"Variations",
|
|
vec![
|
|
single_example(
|
|
"Default",
|
|
Tab::new("default").child("Default Tab").into_any_element(),
|
|
),
|
|
single_example(
|
|
"Selected",
|
|
Tab::new("selected")
|
|
.toggle_state(true)
|
|
.child("Selected Tab")
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"First",
|
|
Tab::new("first")
|
|
.position(TabPosition::First)
|
|
.child("First Tab")
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"Middle",
|
|
Tab::new("middle")
|
|
.position(TabPosition::Middle(Ordering::Equal))
|
|
.child("Middle Tab")
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"Last",
|
|
Tab::new("last")
|
|
.position(TabPosition::Last)
|
|
.child("Last Tab")
|
|
.into_any_element(),
|
|
),
|
|
],
|
|
)])
|
|
.into_any_element(),
|
|
)
|
|
}
|
|
}
|