Show borders around avatars and panes to indicate following state

This commit is contained in:
Max Brunsfeld 2022-03-21 15:12:15 -07:00
parent 06cd9ac664
commit c8f36af823
4 changed files with 82 additions and 19 deletions

View file

@ -35,6 +35,7 @@ pub struct Workspace {
pub tab: Tab, pub tab: Tab,
pub active_tab: Tab, pub active_tab: Tab,
pub pane_divider: Border, pub pane_divider: Border,
pub leader_border_opacity: f32,
pub left_sidebar: Sidebar, pub left_sidebar: Sidebar,
pub right_sidebar: Sidebar, pub right_sidebar: Sidebar,
pub status_bar: StatusBar, pub status_bar: StatusBar,

View file

@ -1,9 +1,11 @@
use crate::{FollowerStatesByLeader, Pane};
use anyhow::{anyhow, Result}; 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 theme::Theme;
use crate::Pane;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct PaneGroup { pub struct PaneGroup {
root: Member, root: Member,
@ -47,8 +49,13 @@ impl PaneGroup {
} }
} }
pub fn render<'a>(&self, theme: &Theme) -> ElementBox { pub(crate) fn render<'a>(
self.root.render(theme) &self,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
) -> ElementBox {
self.root.render(theme, follower_states, collaborators)
} }
} }
@ -80,10 +87,39 @@ impl Member {
Member::Axis(PaneAxis { axis, members }) Member::Axis(PaneAxis { axis, members })
} }
pub fn render(&self, theme: &Theme) -> ElementBox { pub fn render(
&self,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
) -> ElementBox {
match self { match self {
Member::Pane(pane) => ChildView::new(pane).boxed(), Member::Pane(pane) => {
Member::Axis(axis) => axis.render(theme), 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<PeerId, Collaborator>,
) -> ElementBox {
let last_member_ix = self.members.len() - 1; let last_member_ix = self.members.len() - 1;
Flex::new(self.axis) Flex::new(self.axis)
.with_children(self.members.iter().enumerate().map(|(ix, member)| { .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 { if ix < last_member_ix {
let mut border = theme.workspace.pane_divider; let mut border = theme.workspace.pane_divider;
border.left = false; border.left = false;

View file

@ -20,9 +20,9 @@ use gpui::{
json::{self, to_string_pretty, ToJson}, json::{self, to_string_pretty, ToJson},
keymap::Binding, keymap::Binding,
platform::{CursorStyle, WindowOptions}, platform::{CursorStyle, WindowOptions},
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Entity, ImageData, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity,
ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task,
ViewContext, ViewHandle, WeakViewHandle, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use log::error; use log::error;
@ -613,7 +613,7 @@ pub struct Workspace {
status_bar: ViewHandle<StatusBar>, status_bar: ViewHandle<StatusBar>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
leader_state: LeaderState, leader_state: LeaderState,
follower_states_by_leader: HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>, follower_states_by_leader: FollowerStatesByLeader,
_observe_current_user: Task<()>, _observe_current_user: Task<()>,
} }
@ -622,6 +622,8 @@ struct LeaderState {
followers: HashSet<PeerId>, followers: HashSet<PeerId>,
} }
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)] #[derive(Default)]
struct FollowerState { struct FollowerState {
active_view_id: Option<u64>, active_view_id: Option<u64>,
@ -1262,6 +1264,7 @@ impl Workspace {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<PeerId> { ) -> Option<PeerId> {
for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { 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) { if let Some(state) = states_by_pane.remove(&pane) {
for (_, item) in state.items_by_leader_view_id { for (_, item) in state.items_by_leader_view_id {
if let FollowerItem::Loaded(item) = item { if let FollowerItem::Loaded(item) = item {
@ -1270,6 +1273,7 @@ impl Workspace {
} }
if states_by_pane.is_empty() { 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() { if let Some(project_id) = self.project.read(cx).remote_id() {
self.client self.client
.send(proto::Unfollow { .send(proto::Unfollow {
@ -1281,7 +1285,7 @@ impl Workspace {
} }
cx.notify(); cx.notify();
return Some(*leader_id); return Some(leader_id);
} }
} }
None None
@ -1420,17 +1424,25 @@ impl Workspace {
theme: &Theme, theme: &Theme,
cx: &mut RenderContext<Self>, cx: &mut RenderContext<Self>,
) -> ElementBox { ) -> 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() let content = Stack::new()
.with_child( .with_child(
Image::new(avatar) Image::new(avatar)
.with_style(theme.workspace.titlebar.avatar) .with_style(avatar_style)
.constrained() .constrained()
.with_width(theme.workspace.titlebar.avatar_width) .with_width(theme.workspace.titlebar.avatar_width)
.aligned() .aligned()
.boxed(), .boxed(),
) )
.with_child( .with_child(
AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) AvatarRibbon::new(replica_color)
.constrained() .constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width) .with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height) .with_height(theme.workspace.titlebar.avatar_ribbon.height)
@ -1800,7 +1812,15 @@ impl View for Workspace {
content.add_child( content.add_child(
Flex::column() Flex::column()
.with_child( .with_child(
Flexible::new(1., true, self.center.render(&theme)) Flexible::new(
1.,
true,
self.center.render(
&theme,
&self.follower_states_by_leader,
self.project.read(cx).collaborators(),
),
)
.boxed(), .boxed(),
) )
.with_child(ChildView::new(&self.status_bar).boxed()) .with_child(ChildView::new(&self.status_bar).boxed())

View file

@ -4,6 +4,7 @@ base = { family = "Zed Sans", size = 14 }
[workspace] [workspace]
background = "$surface.0" background = "$surface.0"
pane_divider = { width = 1, color = "$border.0" } pane_divider = { width = 1, color = "$border.0" }
leader_border_opacity = 0.6
[workspace.titlebar] [workspace.titlebar]
height = 32 height = 32