Vertically align root folder in project panel with tabs and sidebar icons

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/elements/uniform_list.rs | 32 ++++++++++++++++++++++++----
zed/assets/themes/_base.toml      | 24 +++++++++++---------
zed/src/project_panel.rs          | 12 ++++++++-
zed/src/theme.rs                  | 16 ++++++++------
zed/src/workspace/pane.rs         | 37 +++++++-------------------------
zed/src/workspace/sidebar.rs      | 15 ++++--------
6 files changed, 72 insertions(+), 64 deletions(-)

Detailed changes

gpui/src/elements/uniform_list.rs 🔗

@@ -43,6 +43,8 @@ where
     state: UniformListState,
     item_count: usize,
     append_items: F,
+    padding_top: f32,
+    padding_bottom: f32,
 }
 
 impl<F> UniformList<F>
@@ -54,9 +56,21 @@ where
             state,
             item_count,
             append_items,
+            padding_top: 0.,
+            padding_bottom: 0.,
         }
     }
 
+    pub fn with_padding_top(mut self, padding: f32) -> Self {
+        self.padding_top = padding;
+        self
+    }
+
+    pub fn with_padding_bottom(mut self, padding: f32) -> Self {
+        self.padding_bottom = padding;
+        self
+    }
+
     fn scroll(
         &self,
         _: Vector2F,
@@ -84,7 +98,7 @@ where
         }
 
         if let Some(item_ix) = state.scroll_to.take() {
-            let item_top = item_ix as f32 * item_height;
+            let item_top = self.padding_top + item_ix as f32 * item_height;
             let item_bottom = item_top + item_height;
 
             if item_top < state.scroll_top {
@@ -137,11 +151,16 @@ where
                 size.set_y(size.y().min(scroll_height).max(constraint.min.y()));
             }
 
-            scroll_max = (item_height * self.item_count as f32 - size.y()).max(0.);
+            let scroll_height =
+                item_height * self.item_count as f32 + self.padding_top + self.padding_bottom;
+            scroll_max = (scroll_height - size.y()).max(0.);
             self.autoscroll(scroll_max, size.y(), item_height);
 
             items.clear();
-            let start = cmp::min((self.scroll_top() / item_height) as usize, self.item_count);
+            let start = cmp::min(
+                ((self.scroll_top() - self.padding_top) / item_height) as usize,
+                self.item_count,
+            );
             let end = cmp::min(
                 self.item_count,
                 start + (size.y() / item_height).ceil() as usize + 1,
@@ -173,8 +192,11 @@ where
     ) -> Self::PaintState {
         cx.scene.push_layer(Some(bounds));
 
-        let mut item_origin =
-            bounds.origin() - vec2f(0.0, self.state.scroll_top() % layout.item_height);
+        let mut item_origin = bounds.origin()
+            - vec2f(
+                0.,
+                (self.state.scroll_top() - self.padding_top) % layout.item_height,
+            );
 
         for item in &mut layout.items {
             item.paint(item_origin, visible_bounds, cx);

zed/assets/themes/_base.toml 🔗

@@ -18,6 +18,7 @@ padding = { right = 4 }
 width = 16
 
 [workspace.tab]
+height = 34
 text = "$text.2"
 padding = { left = 12, right = 12 }
 icon_width = 8
@@ -26,10 +27,11 @@ 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" }
+border = { left = true, bottom = true, width = 1, color = "$border.0", overlay = true }
 
 [workspace.active_tab]
 extends = "$workspace.tab"
+border.bottom = false
 background = "$surface.1"
 text = "$text.0"
 
@@ -41,13 +43,14 @@ border = { right = true, width = 1, color = "$border.0" }
 padding = { left = 1 }
 background = "$border.0"
 
-[workspace.sidebar.icon]
-color = "$text.2.color"
-height = 18
+[workspace.sidebar.item]
+icon_color = "$text.2.color"
+icon_size = 18
+height = "$workspace.tab.height"
 
-[workspace.sidebar.active_icon]
-extends = "$workspace.sidebar.icon"
-color = "$text.0.color"
+[workspace.sidebar.active_item]
+extends = "$workspace.sidebar.item"
+icon_color = "$text.0.color"
 
 [workspace.left_sidebar]
 extends = "$workspace.sidebar"
@@ -58,7 +61,7 @@ extends = "$workspace.sidebar"
 border = { width = 1, color = "$border.0", left = true }
 
 [panel]
-padding = 12
+padding = { top = 12, left = 12, bottom = 12, right = 12 }
 
 [chat_panel]
 extends = "$panel"
@@ -161,12 +164,11 @@ corner_radius = 6
 
 [project_panel]
 extends = "$panel"
-padding = 0
-entry_base_padding = "$panel.padding"
+padding.top = 6    # ($workspace.tab.height - $project_panel.entry.height) / 2
 
 [project_panel.entry]
 text = "$text.1"
-padding = { top = 3, bottom = 3 }
+height = 22
 icon_color = "$text.3.color"
 icon_size = 8
 icon_spacing = 8

zed/src/project_panel.rs 🔗

@@ -507,11 +507,15 @@ impl ProjectPanel {
                         Label::new(details.filename, style.text.clone())
                             .contained()
                             .with_margin_left(style.icon_spacing)
+                            .aligned()
+                            .left()
                             .boxed(),
                     )
+                    .constrained()
+                    .with_height(theme.entry.height)
                     .contained()
                     .with_style(style.container)
-                    .with_padding_left(theme.entry_base_padding + details.depth as f32 * 20.)
+                    .with_padding_left(theme.container.padding.left + details.depth as f32 * 20.)
                     .boxed()
             },
         )
@@ -534,6 +538,8 @@ impl View for ProjectPanel {
 
     fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
         let settings = self.settings.clone();
+        let mut container_style = settings.borrow().theme.project_panel.container;
+        let padding = std::mem::take(&mut container_style.padding);
         let handle = self.handle.clone();
         UniformList::new(
             self.list.clone(),
@@ -551,8 +557,10 @@ impl View for ProjectPanel {
                 })
             },
         )
+        .with_padding_top(padding.top)
+        .with_padding_bottom(padding.bottom)
         .contained()
-        .with_style(self.settings.borrow().theme.project_panel.container)
+        .with_style(container_style)
         .boxed()
     }
 

zed/src/theme.rs 🔗

@@ -67,6 +67,7 @@ pub struct OfflineIcon {
 
 #[derive(Clone, Deserialize)]
 pub struct Tab {
+    pub height: f32,
     #[serde(flatten)]
     pub container: ContainerStyle,
     #[serde(flatten)]
@@ -84,14 +85,15 @@ pub struct Sidebar {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub width: f32,
-    pub icon: SidebarIcon,
-    pub active_icon: SidebarIcon,
+    pub item: SidebarItem,
+    pub active_item: SidebarItem,
     pub resize_handle: ContainerStyle,
 }
 
 #[derive(Deserialize)]
-pub struct SidebarIcon {
-    pub color: Color,
+pub struct SidebarItem {
+    pub icon_color: Color,
+    pub icon_size: f32,
     pub height: f32,
 }
 
@@ -107,18 +109,18 @@ pub struct ChatPanel {
     pub hovered_sign_in_prompt: TextStyle,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 pub struct ProjectPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub entry_base_padding: f32,
     pub entry: ProjectPanelEntry,
     pub hovered_entry: ProjectPanelEntry,
     pub selected_entry: ProjectPanelEntry,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 pub struct ProjectPanelEntry {
+    pub height: f32,
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub text: TextStyle,

zed/src/workspace/pane.rs 🔗

@@ -2,12 +2,11 @@ use super::{ItemViewHandle, SplitDirection};
 use crate::settings::Settings;
 use gpui::{
     action,
-    color::Color,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
     platform::CursorStyle,
-    Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
+    Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
 };
 use postage::watch;
 use std::{cmp, path::Path, sync::Arc};
@@ -180,10 +179,6 @@ impl Pane {
     fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
         let settings = self.settings.borrow();
         let theme = &settings.theme;
-        let line_height = cx.font_cache().line_height(
-            theme.workspace.tab.label.text.font_id,
-            theme.workspace.tab.label.text.font_size,
-        );
 
         enum Tabs {}
         let tabs = MouseEventHandler::new::<Tabs, _, _, _>(0, cx, |mouse_state, cx| {
@@ -202,12 +197,11 @@ impl Pane {
                         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;
-                    }
+                    let mut style = if is_active {
+                        theme.workspace.active_tab.clone()
+                    } else {
+                        theme.workspace.tab.clone()
+                    };
                     if ix == 0 {
                         style.container.border.left = false;
                     }
@@ -319,26 +313,11 @@ impl Pane {
                 })
             }
 
-            // Ensure there's always a minimum amount of space after the last tab,
-            // so that the tab's border doesn't abut the window's border.
-            let mut border = Border::bottom(1.0, Color::default());
-            border.color = theme.workspace.tab.container.border.color;
-
-            row.add_child(
-                ConstrainedBox::new(
-                    Container::new(Empty::new().boxed())
-                        .with_border(border)
-                        .boxed(),
-                )
-                .with_min_width(20.)
-                .named("fixed-filler"),
-            );
-
             row.add_child(
                 Expanded::new(
                     0.0,
                     Container::new(Empty::new().boxed())
-                        .with_border(border)
+                        .with_border(theme.workspace.tab.container.border)
                         .boxed(),
                 )
                 .named("filler"),
@@ -348,7 +327,7 @@ impl Pane {
         });
 
         ConstrainedBox::new(tabs.boxed())
-            .with_height(line_height + 16.)
+            .with_height(theme.workspace.tab.height)
             .named("tabs")
     }
 }

zed/src/workspace/sidebar.rs 🔗

@@ -68,11 +68,6 @@ impl Sidebar {
 
     pub fn render(&self, settings: &Settings, cx: &mut RenderContext<Workspace>) -> ElementBox {
         let side = self.side;
-        let theme = &settings.theme;
-        let line_height = cx.font_cache().line_height(
-            theme.workspace.tab.label.text.font_id,
-            theme.workspace.tab.label.text.font_size,
-        );
         let theme = self.theme(settings);
 
         ConstrainedBox::new(
@@ -80,9 +75,9 @@ impl Sidebar {
                 Flex::column()
                     .with_children(self.items.iter().enumerate().map(|(item_index, item)| {
                         let theme = if Some(item_index) == self.active_item_ix {
-                            &theme.active_icon
+                            &theme.active_item
                         } else {
-                            &theme.icon
+                            &theme.item
                         };
                         enum SidebarButton {}
                         MouseEventHandler::new::<SidebarButton, _, _, _>(
@@ -93,15 +88,15 @@ impl Sidebar {
                                     Align::new(
                                         ConstrainedBox::new(
                                             Svg::new(item.icon_path)
-                                                .with_color(theme.color)
+                                                .with_color(theme.icon_color)
                                                 .boxed(),
                                         )
-                                        .with_height(theme.height)
+                                        .with_height(theme.icon_size)
                                         .boxed(),
                                     )
                                     .boxed(),
                                 )
-                                .with_height(line_height + 16.0)
+                                .with_height(theme.height)
                                 .boxed()
                             },
                         )