Allow clicking a titlebar avatar to initiate following

This commit is contained in:
Max Brunsfeld 2022-03-18 13:37:07 -07:00
parent d860ed25c1
commit e338da0271
3 changed files with 134 additions and 114 deletions

View file

@ -184,6 +184,12 @@ macro_rules! action {
Box::new(self.clone()) Box::new(self.clone())
} }
} }
impl From<$arg> for $name {
fn from(arg: $arg) -> Self {
Self(arg)
}
}
}; };
($name:ident) => { ($name:ident) => {

View file

@ -4272,8 +4272,8 @@ mod tests {
workspace_b workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
let leader_id = project_b.read(cx).collaborators().keys().next().unwrap(); let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
workspace.follow(*leader_id, cx) workspace.follow(&leader_id.into(), cx).unwrap()
}) })
.await .await
.unwrap(); .unwrap();
@ -4291,8 +4291,7 @@ mod tests {
}); });
workspace_b workspace_b
.condition(cx_b, |workspace, cx| { .condition(cx_b, |workspace, cx| {
let active_item = workspace.active_item(cx).unwrap(); workspace.active_item(cx).unwrap().id() == editor_b1.id()
active_item.project_path(cx) == Some((worktree_id, "1.txt").into())
}) })
.await; .await;
} }

View file

@ -43,7 +43,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use theme::{Theme, ThemeRegistry}; use theme::{Theme, ThemeRegistry};
use util::{ResultExt, TryFutureExt}; use util::ResultExt;
type ProjectItemBuilders = HashMap< type ProjectItemBuilders = HashMap<
TypeId, TypeId,
@ -68,6 +68,7 @@ action!(Open, Arc<AppState>);
action!(OpenNew, Arc<AppState>); action!(OpenNew, Arc<AppState>);
action!(OpenPaths, OpenParams); action!(OpenPaths, OpenParams);
action!(ToggleShare); action!(ToggleShare);
action!(FollowCollaborator, PeerId);
action!(JoinProject, JoinProjectParams); action!(JoinProject, JoinProjectParams);
action!(Save); action!(Save);
action!(DebugElements); action!(DebugElements);
@ -88,6 +89,7 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
}); });
cx.add_action(Workspace::toggle_share); cx.add_action(Workspace::toggle_share);
cx.add_async_action(Workspace::follow);
cx.add_action( cx.add_action(
|workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| { |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
workspace.save_active_item(cx).detach_and_log_err(cx); workspace.save_active_item(cx).detach_and_log_err(cx);
@ -1192,88 +1194,90 @@ impl Workspace {
} }
} }
pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Task<Result<()>> { pub fn follow(
if let Some(project_id) = self.project.read(cx).remote_id() { &mut self,
let request = self.client.request(proto::Follow { FollowCollaborator(leader_id): &FollowCollaborator,
project_id, cx: &mut ViewContext<Self>,
leader_id: leader_id.0, ) -> Option<Task<Result<()>>> {
}); let leader_id = *leader_id;
cx.spawn_weak(|this, mut cx| async move { let project_id = self.project.read(cx).remote_id()?;
let mut response = request.await?; let request = self.client.request(proto::Follow {
if let Some(this) = this.upgrade(&cx) { project_id,
let mut item_tasks = Vec::new(); leader_id: leader_id.0,
let (project, pane) = this.read_with(&cx, |this, _| { });
(this.project.clone(), this.active_pane().clone()) Some(cx.spawn_weak(|this, mut cx| async move {
}); let mut response = request.await?;
let item_builders = cx.update(|cx| { if let Some(this) = this.upgrade(&cx) {
cx.default_global::<FollowableItemBuilders>() let mut item_tasks = Vec::new();
.values() let (project, pane) = this.read_with(&cx, |this, _| {
.map(|b| b.0) (this.project.clone(), this.active_pane().clone())
.collect::<Vec<_>>() });
.clone() let item_builders = cx.update(|cx| {
}); cx.default_global::<FollowableItemBuilders>()
for view in &mut response.views { .values()
let variant = view .map(|b| b.0)
.variant .collect::<Vec<_>>()
.take() .clone()
.ok_or_else(|| anyhow!("missing variant"))?; });
cx.update(|cx| { for view in &mut response.views {
let mut variant = Some(variant); let variant = view
for build_item in &item_builders { .variant
if let Some(task) = .take()
build_item(pane.clone(), project.clone(), &mut variant, cx) .ok_or_else(|| anyhow!("missing variant"))?;
{ cx.update(|cx| {
item_tasks.push(task); let mut variant = Some(variant);
break; for build_item in &item_builders {
} else { if let Some(task) =
assert!(variant.is_some()); build_item(pane.clone(), project.clone(), &mut variant, cx)
} {
item_tasks.push(task);
break;
} else {
assert!(variant.is_some());
} }
});
}
this.update(&mut cx, |this, cx| {
this.follower_states_by_leader
.entry(leader_id)
.or_default()
.insert(
pane.downgrade(),
FollowerState {
active_view_id: response.active_view_id.map(|id| id as usize),
items_by_leader_view_id: Default::default(),
},
);
});
let items = futures::future::try_join_all(item_tasks).await?;
this.update(&mut cx, |this, cx| {
let follower_state = this
.follower_states_by_leader
.entry(leader_id)
.or_default()
.entry(pane.downgrade())
.or_default();
for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) {
let prev_state = follower_state.items_by_leader_view_id.remove(&id);
if let Some(FollowerItem::Loading(updates)) = prev_state {
for update in updates {
item.apply_update_message(update, cx)
.context("failed to apply view update")
.log_err();
}
}
follower_state
.items_by_leader_view_id
.insert(id, FollowerItem::Loaded(item));
} }
this.leader_updated(leader_id, cx);
}); });
} }
Ok(())
}) this.update(&mut cx, |this, cx| {
} else { this.follower_states_by_leader
Task::ready(Err(anyhow!("project is not remote"))) .entry(leader_id)
} .or_default()
.insert(
pane.downgrade(),
FollowerState {
active_view_id: response.active_view_id.map(|id| id as usize),
items_by_leader_view_id: Default::default(),
},
);
});
let items = futures::future::try_join_all(item_tasks).await?;
this.update(&mut cx, |this, cx| {
let follower_state = this
.follower_states_by_leader
.entry(leader_id)
.or_default()
.entry(pane.downgrade())
.or_default();
for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) {
let prev_state = follower_state.items_by_leader_view_id.remove(&id);
if let Some(FollowerItem::Loading(updates)) = prev_state {
for update in updates {
item.apply_update_message(update, cx)
.context("failed to apply view update")
.log_err();
}
}
follower_state
.items_by_leader_view_id
.insert(id, FollowerItem::Loaded(item));
}
this.leader_updated(leader_id, cx);
});
}
Ok(())
}))
} }
fn update_followers( fn update_followers(
@ -1383,7 +1387,9 @@ impl Workspace {
Some(self.render_avatar( Some(self.render_avatar(
collaborator.user.avatar.clone()?, collaborator.user.avatar.clone()?,
collaborator.replica_id, collaborator.replica_id,
Some(collaborator.peer_id),
theme, theme,
cx,
)) ))
}) })
.collect() .collect()
@ -1397,7 +1403,7 @@ impl Workspace {
cx: &mut RenderContext<Self>, cx: &mut RenderContext<Self>,
) -> ElementBox { ) -> ElementBox {
if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
self.render_avatar(avatar, replica_id, theme) self.render_avatar(avatar, replica_id, None, theme, cx)
} else { } else {
MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| { MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
let style = if state.hovered { let style = if state.hovered {
@ -1421,52 +1427,61 @@ impl Workspace {
&self, &self,
avatar: Arc<ImageData>, avatar: Arc<ImageData>,
replica_id: ReplicaId, replica_id: ReplicaId,
peer_id: Option<PeerId>,
theme: &Theme, theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox { ) -> ElementBox {
ConstrainedBox::new( let content = Stack::new()
Stack::new() .with_child(
.with_child( Image::new(avatar)
ConstrainedBox::new( .with_style(theme.workspace.titlebar.avatar)
Image::new(avatar) .constrained()
.with_style(theme.workspace.titlebar.avatar)
.boxed(),
)
.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(theme.editor.replica_selection_style(replica_id).cursor)
.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)
.aligned() .aligned()
.bottom() .bottom()
.boxed(), .boxed(),
) )
.boxed(), .constrained()
) .with_width(theme.workspace.right_sidebar.width)
.with_width(theme.workspace.right_sidebar.width) .boxed();
.boxed()
if let Some(peer_id) = peer_id {
MouseEventHandler::new::<FollowCollaborator, _, _>(
replica_id.into(),
cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(move |cx| cx.dispatch_action(FollowCollaborator(peer_id)))
.boxed()
} else {
content
}
} }
fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Option<ElementBox> { fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
if self.project().read(cx).is_local() && self.client.user_id().is_some() { if self.project().read(cx).is_local() && self.client.user_id().is_some() {
enum Share {}
let color = if self.project().read(cx).is_shared() { let color = if self.project().read(cx).is_shared() {
theme.workspace.titlebar.share_icon_active_color theme.workspace.titlebar.share_icon_active_color
} else { } else {
theme.workspace.titlebar.share_icon_color theme.workspace.titlebar.share_icon_color
}; };
Some( Some(
MouseEventHandler::new::<Share, _, _>(0, cx, |_, _| { MouseEventHandler::new::<ToggleShare, _, _>(0, cx, |_, _| {
Align::new( Align::new(
ConstrainedBox::new( Svg::new("icons/broadcast-24.svg")
Svg::new("icons/broadcast-24.svg").with_color(color).boxed(), .with_color(color)
) .constrained()
.with_width(24.) .with_width(24.)
.boxed(), .boxed(),
) )
.boxed() .boxed()
}) })