Style people panel

Antonio Scandurra and Nathan Sobo created

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

Change summary

gpui/src/color.rs             |   8 ++
gpui/src/elements.rs          |   5 
gpui/src/elements/label.rs    |   3 
gpui/src/elements/line_box.rs |  87 -----------------------
gpui/src/font_cache.rs        |  12 +++
gpui/src/fonts.rs             |  12 +++
zed/assets/themes/_base.toml  |  13 +++
zed/src/people_panel.rs       | 135 ++++++++++++++++++++++++++++--------
zed/src/theme.rs              |   9 ++
9 files changed, 158 insertions(+), 126 deletions(-)

Detailed changes

gpui/src/color.rs 🔗

@@ -33,6 +33,14 @@ impl Color {
         Self(ColorU::from_u32(0xff0000ff))
     }
 
+    pub fn green() -> Self {
+        Self(ColorU::from_u32(0x00ff00ff))
+    }
+
+    pub fn blue() -> Self {
+        Self(ColorU::from_u32(0x0000ffff))
+    }
+
     pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
         Self(ColorU::new(r, g, b, a))
     }

gpui/src/elements.rs 🔗

@@ -8,7 +8,6 @@ mod flex;
 mod hook;
 mod image;
 mod label;
-mod line_box;
 mod list;
 mod mouse_event_handler;
 mod overlay;
@@ -19,8 +18,8 @@ mod uniform_list;
 
 pub use self::{
     align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
-    hook::*, image::*, label::*, line_box::*, list::*, mouse_event_handler::*, overlay::*,
-    stack::*, svg::*, text::*, uniform_list::*,
+    hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*,
+    text::*, uniform_list::*,
 };
 pub use crate::presenter::ChildView;
 use crate::{

gpui/src/elements/label.rs 🔗

@@ -137,8 +137,7 @@ impl Element for Label {
         let size = vec2f(
             line.width().max(constraint.min.x()).min(constraint.max.x()),
             cx.font_cache
-                .line_height(self.style.text.font_id, self.style.text.font_size)
-                .ceil(),
+                .line_height(self.style.text.font_id, self.style.text.font_size),
         );
 
         (size, line)

gpui/src/elements/line_box.rs 🔗

@@ -1,87 +0,0 @@
-use crate::{
-    fonts::TextStyle,
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
-    json::{json, ToJson},
-    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
-    SizeConstraint,
-};
-
-pub struct LineBox {
-    child: ElementBox,
-    style: TextStyle,
-}
-
-impl LineBox {
-    pub fn new(child: ElementBox, style: TextStyle) -> Self {
-        Self { child, style }
-    }
-}
-
-impl Element for LineBox {
-    type LayoutState = f32;
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: SizeConstraint,
-        cx: &mut LayoutContext,
-    ) -> (Vector2F, Self::LayoutState) {
-        let line_height = cx
-            .font_cache
-            .line_height(self.style.font_id, self.style.font_size);
-        let character_height = cx
-            .font_cache
-            .ascent(self.style.font_id, self.style.font_size)
-            + cx.font_cache
-                .descent(self.style.font_id, self.style.font_size);
-        let child_max = vec2f(constraint.max.x(), character_height);
-        let child_size = self.child.layout(
-            SizeConstraint::new(constraint.min.min(child_max), child_max),
-            cx,
-        );
-        let size = vec2f(child_size.x(), line_height);
-        (size, (line_height - character_height) / 2.)
-    }
-
-    fn paint(
-        &mut self,
-        bounds: RectF,
-        visible_bounds: RectF,
-        padding_top: &mut f32,
-        cx: &mut PaintContext,
-    ) -> Self::PaintState {
-        self.child.paint(
-            bounds.origin() + vec2f(0., *padding_top),
-            visible_bounds,
-            cx,
-        );
-    }
-
-    fn dispatch_event(
-        &mut self,
-        event: &Event,
-        _: RectF,
-        _: &mut Self::LayoutState,
-        _: &mut Self::PaintState,
-        cx: &mut EventContext,
-    ) -> bool {
-        self.child.dispatch_event(event, cx)
-    }
-
-    fn debug(
-        &self,
-        bounds: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        cx: &DebugContext,
-    ) -> serde_json::Value {
-        json!({
-            "bounds": bounds.to_json(),
-            "style": self.style.to_json(),
-            "child": self.child.debug(cx),
-        })
-    }
-}

gpui/src/font_cache.rs 🔗

@@ -166,6 +166,10 @@ impl FontCache {
         self.metric(font_id, |m| m.cap_height) * self.em_scale(font_id, font_size)
     }
 
+    pub fn x_height(&self, font_id: FontId, font_size: f32) -> f32 {
+        self.metric(font_id, |m| m.x_height) * self.em_scale(font_id, font_size)
+    }
+
     pub fn ascent(&self, font_id: FontId, font_size: f32) -> f32 {
         self.metric(font_id, |m| m.ascent) * self.em_scale(font_id, font_size)
     }
@@ -178,6 +182,14 @@ impl FontCache {
         font_size / self.metric(font_id, |m| m.units_per_em as f32)
     }
 
+    pub fn baseline_offset(&self, font_id: FontId, font_size: f32) -> f32 {
+        let line_height = self.line_height(font_id, font_size);
+        let ascent = self.ascent(font_id, font_size);
+        let descent = self.descent(font_id, font_size);
+        let padding_top = (line_height - ascent - descent) / 2.;
+        padding_top + ascent
+    }
+
     pub fn line_wrapper(self: &Arc<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {
         let mut state = self.0.write();
         let wrappers = state

gpui/src/fonts.rs 🔗

@@ -132,6 +132,14 @@ impl TextStyle {
         font_cache.line_height(self.font_id, self.font_size)
     }
 
+    pub fn cap_height(&self, font_cache: &FontCache) -> f32 {
+        font_cache.cap_height(self.font_id, self.font_size)
+    }
+
+    pub fn x_height(&self, font_cache: &FontCache) -> f32 {
+        font_cache.x_height(self.font_id, self.font_size)
+    }
+
     pub fn em_width(&self, font_cache: &FontCache) -> f32 {
         font_cache.em_width(self.font_id, self.font_size)
     }
@@ -140,6 +148,10 @@ impl TextStyle {
         font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
     }
 
+    pub fn baseline_offset(&self, font_cache: &FontCache) -> f32 {
+        font_cache.baseline_offset(self.font_id, self.font_size)
+    }
+
     fn em_scale(&self, font_cache: &FontCache) -> f32 {
         font_cache.em_scale(self.font_id, self.font_size)
     }

zed/assets/themes/_base.toml 🔗

@@ -56,10 +56,13 @@ border = { width = 1, color = "$border.0", right = true }
 extends = "$workspace.sidebar"
 border = { width = 1, color = "$border.0", left = true }
 
+[panel]
+padding = 12
+
 [chat_panel]
+extends = "$panel"
 channel_name = { extends = "$text.0", weight = "bold" }
 channel_name_hash = { text = "$text.2", padding.right = 8 }
-padding = 12
 
 [chat_panel.message]
 body = "$text.1"
@@ -121,12 +124,18 @@ extends = "$chat_panel.sign_in_prompt"
 color = "$text.1.color"
 
 [people_panel]
-host_username = "$text.0"
+extends = "$panel"
+host_username = { extends = "$text.0", padding.left = 5 }
 worktree_host_avatar = { corner_radius = 10 }
 worktree_guest_avatar = { corner_radius = 8 }
 
 [people_panel.worktree_name]
 extends = "$text.0"
+padding = { left = 5 }
+
+[people_panel.tree_branch]
+width = 1
+color = "$surface.2"
 
 [selector]
 background = "$surface.0"

zed/src/people_panel.rs 🔗

@@ -4,7 +4,9 @@ use crate::{
     Settings,
 };
 use gpui::{
-    elements::*, Element, ElementBox, Entity, ModelHandle, RenderContext, Subscription, View,
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    Element, ElementBox, Entity, FontCache, ModelHandle, RenderContext, Subscription, View,
     ViewContext,
 };
 use postage::watch;
@@ -12,6 +14,7 @@ use postage::watch;
 pub struct PeoplePanel {
     collaborators: ListState,
     user_store: ModelHandle<UserStore>,
+    settings: watch::Receiver<Settings>,
     _maintain_collaborators: Subscription,
 }
 
@@ -28,15 +31,19 @@ impl PeoplePanel {
                 1000.,
                 {
                     let user_store = user_store.clone();
+                    let settings = settings.clone();
                     move |ix, cx| {
-                        let user_store = user_store.read(cx);
-                        let settings = settings.borrow();
-                        Self::render_collaborator(&user_store.collaborators()[ix], &settings.theme)
+                        Self::render_collaborator(
+                            &user_store.read(cx).collaborators()[ix],
+                            &settings.borrow().theme,
+                            cx.font_cache(),
+                        )
                     }
                 },
             ),
             _maintain_collaborators: cx.observe(&user_store, Self::update_collaborators),
             user_store,
+            settings,
         }
     }
 
@@ -46,54 +53,117 @@ impl PeoplePanel {
         cx.notify();
     }
 
-    fn render_collaborator(collaborator: &Collaborator, theme: &Theme) -> ElementBox {
+    fn render_collaborator(
+        collaborator: &Collaborator,
+        theme: &Theme,
+        font_cache: &FontCache,
+    ) -> ElementBox {
+        let theme = &theme.people_panel;
+        let worktree_count = collaborator.worktrees.len();
+        let line_height = theme.worktree_name.text.line_height(font_cache);
+        let cap_height = theme.worktree_name.text.cap_height(font_cache);
+        let baseline_offset = theme.worktree_name.text.baseline_offset(font_cache);
+        let tree_branch = theme.tree_branch;
+
         Flex::column()
             .with_child(
                 Flex::row()
                     .with_children(collaborator.user.avatar.clone().map(|avatar| {
                         ConstrainedBox::new(
                             Image::new(avatar)
-                                .with_style(theme.people_panel.worktree_host_avatar)
+                                .with_style(theme.worktree_host_avatar)
                                 .boxed(),
                         )
                         .with_width(20.)
                         .boxed()
                     }))
-                    .with_child(
-                        Label::new(
-                            collaborator.user.github_login.clone(),
-                            theme.people_panel.host_username.clone(),
-                        )
-                        .boxed(),
-                    )
-                    .boxed(),
-            )
-            .with_children(collaborator.worktrees.iter().map(|worktree| {
-                Flex::row()
                     .with_child(
                         Container::new(
                             Label::new(
-                                worktree.root_name.clone(),
-                                theme.people_panel.worktree_name.text.clone(),
+                                collaborator.user.github_login.clone(),
+                                theme.host_username.text.clone(),
                             )
                             .boxed(),
                         )
-                        .with_style(theme.people_panel.worktree_name.container)
+                        .with_style(theme.host_username.container)
                         .boxed(),
                     )
-                    .with_children(worktree.participants.iter().filter_map(|participant| {
-                        participant.avatar.clone().map(|avatar| {
-                            ConstrainedBox::new(
-                                Image::new(avatar)
-                                    .with_style(theme.people_panel.worktree_guest_avatar)
+                    .boxed(),
+            )
+            .with_children(
+                collaborator
+                    .worktrees
+                    .iter()
+                    .enumerate()
+                    .map(|(ix, worktree)| {
+                        Flex::row()
+                            .with_child(
+                                ConstrainedBox::new(
+                                    Canvas::new(move |bounds, _, cx| {
+                                        let start_x = bounds.min_x() + (bounds.width() / 2.)
+                                            - (tree_branch.width / 2.);
+                                        let end_x = bounds.max_x();
+                                        let start_y = bounds.min_y();
+                                        let end_y =
+                                            bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+                                        cx.scene.push_quad(gpui::Quad {
+                                            bounds: RectF::from_points(
+                                                vec2f(start_x, start_y),
+                                                vec2f(
+                                                    start_x + tree_branch.width,
+                                                    if ix + 1 == worktree_count {
+                                                        end_y
+                                                    } else {
+                                                        bounds.max_y()
+                                                    },
+                                                ),
+                                            ),
+                                            background: Some(tree_branch.color),
+                                            border: gpui::Border::default(),
+                                            corner_radius: 0.,
+                                        });
+                                        cx.scene.push_quad(gpui::Quad {
+                                            bounds: RectF::from_points(
+                                                vec2f(start_x, end_y),
+                                                vec2f(end_x, end_y + tree_branch.width),
+                                            ),
+                                            background: Some(tree_branch.color),
+                                            border: gpui::Border::default(),
+                                            corner_radius: 0.,
+                                        });
+                                    })
                                     .boxed(),
+                                )
+                                .with_width(20.)
+                                .with_height(line_height)
+                                .boxed(),
                             )
-                            .with_width(16.)
+                            .with_child(
+                                Container::new(
+                                    Label::new(
+                                        worktree.root_name.clone(),
+                                        theme.worktree_name.text.clone(),
+                                    )
+                                    .boxed(),
+                                )
+                                .with_style(theme.worktree_name.container)
+                                .boxed(),
+                            )
+                            .with_children(worktree.participants.iter().filter_map(|participant| {
+                                participant.avatar.clone().map(|avatar| {
+                                    ConstrainedBox::new(
+                                        Image::new(avatar)
+                                            .with_style(theme.worktree_guest_avatar)
+                                            .boxed(),
+                                    )
+                                    .with_width(16.)
+                                    .boxed()
+                                })
+                            }))
                             .boxed()
-                        })
-                    }))
-                    .boxed()
-            }))
+                    }),
+            )
             .boxed()
     }
 }
@@ -110,6 +180,9 @@ impl View for PeoplePanel {
     }
 
     fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-        List::new(self.collaborators.clone()).boxed()
+        let theme = &self.settings.borrow().theme.people_panel;
+        Container::new(List::new(self.collaborators.clone()).boxed())
+            .with_style(theme.container)
+            .boxed()
     }
 }

zed/src/theme.rs 🔗

@@ -109,10 +109,17 @@ pub struct ChatPanel {
 pub struct PeoplePanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub host_username: TextStyle,
+    pub host_username: ContainedText,
     pub worktree_name: ContainedText,
     pub worktree_host_avatar: ImageStyle,
     pub worktree_guest_avatar: ImageStyle,
+    pub tree_branch: TreeBranch,
+}
+
+#[derive(Copy, Clone, Deserialize)]
+pub struct TreeBranch {
+    pub width: f32,
+    pub color: Color,
 }
 
 #[derive(Deserialize)]