diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index ae45544952f6b535d4ce938bd08efec733380883..48dcfa1b137986df78690a1adaf8e06249dae314 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -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 { diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 107ffe619dbc24097d713aebd36570f04016f642..c69fa22a68fd5a80628bdde1a73bc6ed4fdd4b0f 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -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" diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 3743120ef19e80e41ca72446c23ff866ef3a1233..f0bfa35c9c9e46d8a6e239a213faa81e65b25ce2 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -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, } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 3894ad4cbdac6aba7b336ed2437d80bbf76d052c..ee8db46ca70da88fd153203b39ce39c1d6f69212 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -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, @@ -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::(item.id(), cx, |mouse_state, cx| { - let title = item.title(cx); - - let mut border = border.clone(); - border.left = ix > 0; - border.right = ix == last_item_ix; - border.bottom = !is_active; - - let mut container = Container::new( - Stack::new() + MouseEventHandler::new::(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 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; + } + + EventHandler::new( + Container::new( + Flex::row() .with_child( - Align::new( - Label::new( - title, - if is_active { - theme.workspace.active_tab.label.clone() - } else { - theme.workspace.tab.label.clone() - }, + 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(Self::render_tab_icon( - item.id(), - line_height - 2., - mouse_state.hovered, - item.is_dirty(cx), - item.has_conflict(cx), - theme, - cx, - )) - .right() + Align::new( + ConstrainedBox::new(if is_active || mouse_state.hovered { + let item_id = item.id(); + enum TabCloseButton {} + let icon = Svg::new("icons/x.svg"); + MouseEventHandler::new::( + 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(), ) .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, - ) -> 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::(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 {