From c8f36af82365c5976322d58110a004e87c9858ff Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 15:12:15 -0700 Subject: [PATCH] Show borders around avatars and panes to indicate following state --- crates/theme/src/theme.rs | 1 + crates/workspace/src/pane_group.rs | 61 ++++++++++++++++++++++++----- crates/workspace/src/workspace.rs | 38 +++++++++++++----- crates/zed/assets/themes/_base.toml | 1 + 4 files changed, 82 insertions(+), 19 deletions(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5e8e799b4c..61d0bf3f67 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -35,6 +35,7 @@ pub struct Workspace { pub tab: Tab, pub active_tab: Tab, pub pane_divider: Border, + pub leader_border_opacity: f32, pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, pub status_bar: StatusBar, diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 2b56a023fc..c3f4d2d3a6 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,9 +1,11 @@ +use crate::{FollowerStatesByLeader, Pane}; use anyhow::{anyhow, Result}; -use gpui::{elements::*, Axis, ViewHandle}; +use client::PeerId; +use collections::HashMap; +use gpui::{elements::*, Axis, Border, ViewHandle}; +use project::Collaborator; use theme::Theme; -use crate::Pane; - #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { root: Member, @@ -47,8 +49,13 @@ impl PaneGroup { } } - pub fn render<'a>(&self, theme: &Theme) -> ElementBox { - self.root.render(theme) + pub(crate) fn render<'a>( + &self, + theme: &Theme, + follower_states: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { + self.root.render(theme, follower_states, collaborators) } } @@ -80,10 +87,39 @@ impl Member { Member::Axis(PaneAxis { axis, members }) } - pub fn render(&self, theme: &Theme) -> ElementBox { + pub fn render( + &self, + theme: &Theme, + follower_states: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { match self { - Member::Pane(pane) => ChildView::new(pane).boxed(), - Member::Axis(axis) => axis.render(theme), + Member::Pane(pane) => { + let mut border = Border::default(); + let leader = follower_states + .iter() + .find_map(|(leader_id, follower_states)| { + if follower_states.contains_key(pane) { + Some(leader_id) + } else { + None + } + }) + .and_then(|leader_id| collaborators.get(leader_id)); + if let Some(leader) = leader { + let leader_color = theme + .editor + .replica_selection_style(leader.replica_id) + .cursor; + border = Border::all(1.0, leader_color); + border + .color + .fade_out(1. - theme.workspace.leader_border_opacity); + border.overlay = true; + } + ChildView::new(pane).contained().with_border(border).boxed() + } + Member::Axis(axis) => axis.render(theme, follower_states, collaborators), } } } @@ -172,11 +208,16 @@ impl PaneAxis { } } - fn render<'a>(&self, theme: &Theme) -> ElementBox { + fn render( + &self, + theme: &Theme, + follower_state: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .with_children(self.members.iter().enumerate().map(|(ix, member)| { - let mut member = member.render(theme); + let mut member = member.render(theme, follower_state, collaborators); if ix < last_member_ix { let mut border = theme.workspace.pane_divider; border.left = false; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1314b53e63..387bfa9b6e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -20,9 +20,9 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Entity, ImageData, - ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity, + ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -613,7 +613,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, FollowerState>>, + follower_states_by_leader: FollowerStatesByLeader, _observe_current_user: Task<()>, } @@ -622,6 +622,8 @@ struct LeaderState { followers: HashSet, } +type FollowerStatesByLeader = HashMap, FollowerState>>; + #[derive(Default)] struct FollowerState { active_view_id: Option, @@ -1262,6 +1264,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Option { for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { + let leader_id = *leader_id; if let Some(state) = states_by_pane.remove(&pane) { for (_, item) in state.items_by_leader_view_id { if let FollowerItem::Loaded(item) = item { @@ -1270,6 +1273,7 @@ impl Workspace { } if states_by_pane.is_empty() { + self.follower_states_by_leader.remove(&leader_id); if let Some(project_id) = self.project.read(cx).remote_id() { self.client .send(proto::Unfollow { @@ -1281,7 +1285,7 @@ impl Workspace { } cx.notify(); - return Some(*leader_id); + return Some(leader_id); } } None @@ -1420,17 +1424,25 @@ impl Workspace { theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { + let replica_color = theme.editor.replica_selection_style(replica_id).cursor; + let is_followed = peer_id.map_or(false, |peer_id| { + self.follower_states_by_leader.contains_key(&peer_id) + }); + let mut avatar_style = theme.workspace.titlebar.avatar; + if is_followed { + avatar_style.border = Border::all(1.0, replica_color); + } let content = Stack::new() .with_child( Image::new(avatar) - .with_style(theme.workspace.titlebar.avatar) + .with_style(avatar_style) .constrained() .with_width(theme.workspace.titlebar.avatar_width) .aligned() .boxed(), ) .with_child( - AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) + AvatarRibbon::new(replica_color) .constrained() .with_width(theme.workspace.titlebar.avatar_ribbon.width) .with_height(theme.workspace.titlebar.avatar_ribbon.height) @@ -1800,8 +1812,16 @@ impl View for Workspace { content.add_child( Flex::column() .with_child( - Flexible::new(1., true, self.center.render(&theme)) - .boxed(), + Flexible::new( + 1., + true, + self.center.render( + &theme, + &self.follower_states_by_leader, + self.project.read(cx).collaborators(), + ), + ) + .boxed(), ) .with_child(ChildView::new(&self.status_bar).boxed()) .flexible(1., true) diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 76547967bf..d0368d6933 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -4,6 +4,7 @@ base = { family = "Zed Sans", size = 14 } [workspace] background = "$surface.0" pane_divider = { width = 1, color = "$border.0" } +leader_border_opacity = 0.6 [workspace.titlebar] height = 32