Rework tabs
* Tabs are no longer flexible in width * Move status icons to the left side of the tab * The close button is always visbile for the active tab Co-Authored-By: Nate Butler <nate@zed.dev>
This commit is contained in:
co-authored by
Nate Butler
parent
0def328f14
commit
36fb559c03
@@ -244,10 +244,10 @@ impl ToJson for ContainerStyle {
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Margin {
|
||||
top: f32,
|
||||
left: f32,
|
||||
bottom: f32,
|
||||
right: f32,
|
||||
pub top: f32,
|
||||
pub left: f32,
|
||||
pub bottom: f32,
|
||||
pub right: f32,
|
||||
}
|
||||
|
||||
impl ToJson for Margin {
|
||||
|
||||
@@ -10,9 +10,13 @@ border = { width = 1, bottom = true, color = "$surface.1" }
|
||||
[workspace.tab]
|
||||
text = "$text.2"
|
||||
padding = { left = 10, right = 10 }
|
||||
icon_close = "$text.0.color"
|
||||
icon_width = 12
|
||||
spacing = 6
|
||||
icon_close = "$text.2.color"
|
||||
icon_close_active = "$text.0.color"
|
||||
icon_dirty = "$status.info"
|
||||
icon_conflict = "$status.warn"
|
||||
border = { left = true, bottom = true, width = 1, color = "$border.0" }
|
||||
|
||||
[workspace.active_tab]
|
||||
extends = "$workspace.tab"
|
||||
@@ -21,7 +25,7 @@ text = "$text.0"
|
||||
|
||||
[workspace.sidebar]
|
||||
padding = { left = 10, right = 10 }
|
||||
border = { right = true, width = 1, color = "$border.0"}
|
||||
border = { right = true, width = 1, color = "$border.0" }
|
||||
|
||||
[workspace.sidebar.resize_handle]
|
||||
padding = { left = 1 }
|
||||
@@ -46,7 +50,7 @@ border = { width = 1, color = "$surface.1", left = true }
|
||||
[chat_panel]
|
||||
channel_name = { extends = "$text.0", weight = "bold" }
|
||||
channel_name_hash = { text = "$text.2", padding.right = 5 }
|
||||
border = { left = true, width = 1, color = "$border.0"}
|
||||
border = { left = true, width = 1, color = "$border.0" }
|
||||
padding = 10
|
||||
|
||||
[chat_panel.message]
|
||||
@@ -111,7 +115,7 @@ margin.top = 12
|
||||
corner_radius = 6
|
||||
shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
|
||||
input_editor = "$chat_panel.input_editor"
|
||||
border = { width = 1, color = "$border.0"}
|
||||
border = { width = 1, color = "$border.0" }
|
||||
|
||||
[selector.item]
|
||||
text = "$text.1"
|
||||
|
||||
+4
-1
@@ -40,13 +40,16 @@ pub struct Workspace {
|
||||
pub right_sidebar: Sidebar,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Tab {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub label: LabelStyle,
|
||||
pub spacing: f32,
|
||||
pub icon_width: f32,
|
||||
pub icon_close: Color,
|
||||
pub icon_close_active: Color,
|
||||
pub icon_dirty: Color,
|
||||
pub icon_conflict: Color,
|
||||
}
|
||||
|
||||
+118
-129
@@ -1,5 +1,5 @@
|
||||
use super::{ItemViewHandle, SplitDirection};
|
||||
use crate::{settings::Settings, theme};
|
||||
use crate::settings::Settings;
|
||||
use gpui::{
|
||||
action,
|
||||
color::Color,
|
||||
@@ -55,6 +55,8 @@ pub enum Event {
|
||||
Split(SplitDirection),
|
||||
}
|
||||
|
||||
const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct State {
|
||||
pub tabs: Vec<TabState>,
|
||||
@@ -183,82 +185,137 @@ impl Pane {
|
||||
);
|
||||
|
||||
let mut row = Flex::row();
|
||||
let last_item_ix = self.items.len() - 1;
|
||||
for (ix, item) in self.items.iter().enumerate() {
|
||||
let is_active = ix == self.active_item;
|
||||
|
||||
enum Tab {}
|
||||
let border = &theme.workspace.tab.container.border;
|
||||
|
||||
row.add_child(
|
||||
Flexible::new(
|
||||
1.0,
|
||||
MouseEventHandler::new::<Tab, _, _, _>(item.id(), cx, |mouse_state, cx| {
|
||||
let title = item.title(cx);
|
||||
MouseEventHandler::new::<Tab, _, _, _>(item.id(), cx, |mouse_state, cx| {
|
||||
let mut title = item.title(cx);
|
||||
if title.len() > MAX_TAB_TITLE_LEN {
|
||||
let mut truncated_len = MAX_TAB_TITLE_LEN;
|
||||
while !title.is_char_boundary(truncated_len) {
|
||||
truncated_len -= 1;
|
||||
}
|
||||
title.truncate(truncated_len);
|
||||
title.push('…');
|
||||
}
|
||||
|
||||
let mut border = border.clone();
|
||||
border.left = ix > 0;
|
||||
border.right = ix == last_item_ix;
|
||||
border.bottom = !is_active;
|
||||
let mut style = theme.workspace.tab.clone();
|
||||
if is_active {
|
||||
style = theme.workspace.active_tab.clone();
|
||||
style.container.border.bottom = false;
|
||||
style.container.padding.bottom += style.container.border.width;
|
||||
}
|
||||
if ix == 0 {
|
||||
style.container.border.left = false;
|
||||
}
|
||||
|
||||
let mut container = Container::new(
|
||||
Stack::new()
|
||||
EventHandler::new(
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Align::new({
|
||||
let diameter = 6.0;
|
||||
let icon_color = if item.has_conflict(cx) {
|
||||
Some(style.icon_conflict)
|
||||
} else if item.is_dirty(cx) {
|
||||
Some(style.icon_dirty)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ConstrainedBox::new(
|
||||
Canvas::new(move |bounds, _, cx| {
|
||||
if let Some(color) = icon_color {
|
||||
let square = RectF::new(
|
||||
bounds.origin(),
|
||||
vec2f(diameter, diameter),
|
||||
);
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: square,
|
||||
background: Some(color),
|
||||
border: Default::default(),
|
||||
corner_radius: diameter / 2.,
|
||||
});
|
||||
}
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_width(diameter)
|
||||
.with_height(diameter)
|
||||
.boxed()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Align::new(
|
||||
Label::new(
|
||||
title,
|
||||
if is_active {
|
||||
theme.workspace.active_tab.label.clone()
|
||||
} else {
|
||||
theme.workspace.tab.label.clone()
|
||||
},
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(&ContainerStyle {
|
||||
margin: Margin {
|
||||
left: style.spacing,
|
||||
right: style.spacing,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
Label::new(
|
||||
title,
|
||||
if is_active {
|
||||
theme.workspace.active_tab.label.clone()
|
||||
} else {
|
||||
theme.workspace.tab.label.clone()
|
||||
},
|
||||
)
|
||||
ConstrainedBox::new(if is_active || mouse_state.hovered {
|
||||
let item_id = item.id();
|
||||
enum TabCloseButton {}
|
||||
let icon = Svg::new("icons/x.svg");
|
||||
MouseEventHandler::new::<TabCloseButton, _, _, _>(
|
||||
item_id,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
if mouse_state.hovered {
|
||||
icon.with_color(style.icon_close_active)
|
||||
.boxed()
|
||||
} else {
|
||||
icon.with_color(style.icon_close).boxed()
|
||||
}
|
||||
},
|
||||
)
|
||||
.on_click(move |cx| {
|
||||
cx.dispatch_action(CloseItem(item_id))
|
||||
})
|
||||
.named("close-tab-icon")
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
})
|
||||
.with_width(style.icon_width)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(Self::render_tab_icon(
|
||||
item.id(),
|
||||
line_height - 2.,
|
||||
mouse_state.hovered,
|
||||
item.is_dirty(cx),
|
||||
item.has_conflict(cx),
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
.right()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(if is_active {
|
||||
&theme.workspace.active_tab.container
|
||||
} else {
|
||||
&theme.workspace.tab.container
|
||||
})
|
||||
.with_border(border);
|
||||
|
||||
if is_active {
|
||||
container = container.with_padding_bottom(border.width);
|
||||
}
|
||||
|
||||
ConstrainedBox::new(
|
||||
EventHandler::new(container.boxed())
|
||||
.on_mouse_down(move |cx| {
|
||||
cx.dispatch_action(ActivateItem(ix));
|
||||
true
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_min_width(80.0)
|
||||
.with_max_width(264.0)
|
||||
.boxed()
|
||||
.with_style(&style.container)
|
||||
.boxed(),
|
||||
)
|
||||
.on_mouse_down(move |cx| {
|
||||
cx.dispatch_action(ActivateItem(ix));
|
||||
true
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.named("tab"),
|
||||
);
|
||||
.boxed()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
// Ensure there's always a minimum amount of space after the last tab,
|
||||
@@ -290,74 +347,6 @@ impl Pane {
|
||||
.with_height(line_height + 16.)
|
||||
.named("tabs")
|
||||
}
|
||||
|
||||
fn render_tab_icon(
|
||||
item_id: usize,
|
||||
close_icon_size: f32,
|
||||
tab_hovered: bool,
|
||||
is_dirty: bool,
|
||||
has_conflict: bool,
|
||||
theme: &theme::Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
enum TabCloseButton {}
|
||||
|
||||
let mut clicked_color = theme.workspace.tab.icon_dirty;
|
||||
clicked_color.a = 180;
|
||||
|
||||
let current_color = if has_conflict {
|
||||
Some(theme.workspace.tab.icon_conflict)
|
||||
} else if is_dirty {
|
||||
Some(theme.workspace.tab.icon_dirty)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let icon = if tab_hovered {
|
||||
let close_color = current_color.unwrap_or(theme.workspace.tab.icon_close);
|
||||
let icon = Svg::new("icons/x.svg").with_color(close_color);
|
||||
|
||||
MouseEventHandler::new::<TabCloseButton, _, _, _>(item_id, cx, |mouse_state, _| {
|
||||
if mouse_state.hovered {
|
||||
Container::new(icon.with_color(Color::white()).boxed())
|
||||
.with_background_color(if mouse_state.clicked {
|
||||
clicked_color
|
||||
} else {
|
||||
theme.workspace.tab.icon_dirty
|
||||
})
|
||||
.with_corner_radius(close_icon_size / 2.)
|
||||
.boxed()
|
||||
} else {
|
||||
icon.boxed()
|
||||
}
|
||||
})
|
||||
.on_click(move |cx| cx.dispatch_action(CloseItem(item_id)))
|
||||
.named("close-tab-icon")
|
||||
} else {
|
||||
let diameter = 8.;
|
||||
ConstrainedBox::new(
|
||||
Canvas::new(move |bounds, _, cx| {
|
||||
if let Some(current_color) = current_color {
|
||||
let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: square,
|
||||
background: Some(current_color),
|
||||
border: Default::default(),
|
||||
corner_radius: diameter / 2.,
|
||||
});
|
||||
}
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_width(diameter)
|
||||
.with_height(diameter)
|
||||
.named("unsaved-tab-icon")
|
||||
};
|
||||
|
||||
ConstrainedBox::new(Align::new(icon).boxed())
|
||||
.with_width(close_icon_size)
|
||||
.named("tab-icon")
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Pane {
|
||||
|
||||
Reference in New Issue
Block a user