collab_titlebar_item.rs

  1use crate::{
  2    collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
  3    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
  4    ToggleScreenSharing,
  5};
  6use call::{ActiveCall, ParticipantLocation, Room};
  7use client::{proto::PeerId, Authenticate, ContactEventKind, SignOut, User, UserStore};
  8use clock::ReplicaId;
  9use contacts_popover::ContactsPopover;
 10use context_menu::{ContextMenu, ContextMenuItem};
 11use gpui::{
 12    actions,
 13    color::Color,
 14    elements::*,
 15    geometry::{rect::RectF, vector::vec2f, PathBuilder},
 16    impl_internal_actions,
 17    json::{self, ToJson},
 18    CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, RenderContext,
 19    Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
 20};
 21use settings::Settings;
 22use std::{ops::Range, sync::Arc};
 23use theme::{AvatarStyle, Theme};
 24use util::ResultExt;
 25use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
 26
 27actions!(
 28    collab,
 29    [
 30        ToggleCollaboratorList,
 31        ToggleContactsMenu,
 32        ToggleUserMenu,
 33        ShareProject,
 34        UnshareProject,
 35    ]
 36);
 37
 38impl_internal_actions!(collab, [LeaveCall]);
 39
 40#[derive(Copy, Clone, PartialEq)]
 41pub(crate) struct LeaveCall;
 42
 43pub fn init(cx: &mut MutableAppContext) {
 44    cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
 45    cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
 46    cx.add_action(CollabTitlebarItem::share_project);
 47    cx.add_action(CollabTitlebarItem::unshare_project);
 48    cx.add_action(CollabTitlebarItem::leave_call);
 49    cx.add_action(CollabTitlebarItem::toggle_user_menu);
 50}
 51
 52pub struct CollabTitlebarItem {
 53    workspace: WeakViewHandle<Workspace>,
 54    user_store: ModelHandle<UserStore>,
 55    contacts_popover: Option<ViewHandle<ContactsPopover>>,
 56    user_menu: ViewHandle<ContextMenu>,
 57    collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
 58    _subscriptions: Vec<Subscription>,
 59}
 60
 61impl Entity for CollabTitlebarItem {
 62    type Event = ();
 63}
 64
 65impl View for CollabTitlebarItem {
 66    fn ui_name() -> &'static str {
 67        "CollabTitlebarItem"
 68    }
 69
 70    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
 71        let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
 72            workspace
 73        } else {
 74            return Empty::new().boxed();
 75        };
 76
 77        let project = workspace.read(cx).project().read(cx);
 78        let mut project_title = String::new();
 79        for (i, name) in project.worktree_root_names(cx).enumerate() {
 80            if i > 0 {
 81                project_title.push_str(", ");
 82            }
 83            project_title.push_str(name);
 84        }
 85        if project_title.is_empty() {
 86            project_title = "empty project".to_owned();
 87        }
 88
 89        let theme = cx.global::<Settings>().theme.clone();
 90
 91        let mut left_container = Flex::row();
 92        let mut right_container = Flex::row();
 93
 94        left_container.add_child(
 95            Label::new(project_title, theme.workspace.titlebar.title.clone())
 96                .contained()
 97                .with_margin_right(theme.workspace.titlebar.item_spacing)
 98                .aligned()
 99                .left()
100                .boxed(),
101        );
102
103        let user = workspace.read(cx).user_store().read(cx).current_user();
104        let peer_id = workspace.read(cx).client().peer_id();
105        if let Some(((user, peer_id), room)) = user
106            .zip(peer_id)
107            .zip(ActiveCall::global(cx).read(cx).room().cloned())
108        {
109            left_container
110                .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
111
112            right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
113            right_container
114                .add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
115            right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
116        }
117
118        let status = workspace.read(cx).client().status();
119        let status = &*status.borrow();
120        if matches!(status, client::Status::Connected { .. }) {
121            right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
122        } else {
123            right_container.add_children(self.render_connection_status(status, cx));
124        }
125
126        right_container.add_child(self.render_user_menu_button(&theme, cx));
127
128        Stack::new()
129            .with_child(left_container.boxed())
130            .with_child(right_container.aligned().right().boxed())
131            .boxed()
132    }
133}
134
135impl CollabTitlebarItem {
136    pub fn new(
137        workspace: &ViewHandle<Workspace>,
138        user_store: &ModelHandle<UserStore>,
139        cx: &mut ViewContext<Self>,
140    ) -> Self {
141        let active_call = ActiveCall::global(cx);
142        let mut subscriptions = Vec::new();
143        subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
144        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
145        subscriptions.push(cx.observe_window_activation(|this, active, cx| {
146            this.window_activation_changed(active, cx)
147        }));
148        subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
149        subscriptions.push(
150            cx.subscribe(user_store, move |this, user_store, event, cx| {
151                if let Some(workspace) = this.workspace.upgrade(cx) {
152                    workspace.update(cx, |workspace, cx| {
153                        if let client::Event::Contact { user, kind } = event {
154                            if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
155                                workspace.show_notification(user.id as usize, cx, |cx| {
156                                    cx.add_view(|cx| {
157                                        ContactNotification::new(
158                                            user.clone(),
159                                            *kind,
160                                            user_store,
161                                            cx,
162                                        )
163                                    })
164                                })
165                            }
166                        }
167                    });
168                }
169            }),
170        );
171
172        Self {
173            workspace: workspace.downgrade(),
174            user_store: user_store.clone(),
175            contacts_popover: None,
176            user_menu: cx.add_view(|cx| {
177                let mut menu = ContextMenu::new(cx);
178                menu.set_position_mode(OverlayPositionMode::Local);
179                menu
180            }),
181            collaborator_list_popover: None,
182            _subscriptions: subscriptions,
183        }
184    }
185
186    fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
187        if let Some(workspace) = self.workspace.upgrade(cx) {
188            let project = if active {
189                Some(workspace.read(cx).project().clone())
190            } else {
191                None
192            };
193            ActiveCall::global(cx)
194                .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
195                .detach_and_log_err(cx);
196        }
197    }
198
199    fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
200        if ActiveCall::global(cx).read(cx).room().is_none() {
201            self.contacts_popover = None;
202        }
203        cx.notify();
204    }
205
206    fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
207        if let Some(workspace) = self.workspace.upgrade(cx) {
208            let active_call = ActiveCall::global(cx);
209            let project = workspace.read(cx).project().clone();
210            active_call
211                .update(cx, |call, cx| call.share_project(project, cx))
212                .detach_and_log_err(cx);
213        }
214    }
215
216    fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
217        if let Some(workspace) = self.workspace.upgrade(cx) {
218            let active_call = ActiveCall::global(cx);
219            let project = workspace.read(cx).project().clone();
220            active_call
221                .update(cx, |call, cx| call.unshare_project(project, cx))
222                .log_err();
223        }
224    }
225
226    pub fn toggle_collaborator_list_popover(
227        &mut self,
228        _: &ToggleCollaboratorList,
229        cx: &mut ViewContext<Self>,
230    ) {
231        match self.collaborator_list_popover.take() {
232            Some(_) => {}
233            None => {
234                if let Some(workspace) = self.workspace.upgrade(cx) {
235                    let user_store = workspace.read(cx).user_store().clone();
236                    let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
237
238                    cx.subscribe(&view, |this, _, event, cx| {
239                        match event {
240                            collaborator_list_popover::Event::Dismissed => {
241                                this.collaborator_list_popover = None;
242                            }
243                        }
244
245                        cx.notify();
246                    })
247                    .detach();
248
249                    self.collaborator_list_popover = Some(view);
250                }
251            }
252        }
253        cx.notify();
254    }
255
256    pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
257        if self.contacts_popover.take().is_none() {
258            if let Some(workspace) = self.workspace.upgrade(cx) {
259                let project = workspace.read(cx).project().clone();
260                let user_store = workspace.read(cx).user_store().clone();
261                let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
262                cx.subscribe(&view, |this, _, event, cx| {
263                    match event {
264                        contacts_popover::Event::Dismissed => {
265                            this.contacts_popover = None;
266                        }
267                    }
268
269                    cx.notify();
270                })
271                .detach();
272                self.contacts_popover = Some(view);
273            }
274        }
275
276        cx.notify();
277    }
278
279    pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
280        let theme = cx.global::<Settings>().theme.clone();
281        let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
282        let item_style = theme.context_menu.item.disabled_style().clone();
283        self.user_menu.update(cx, |user_menu, cx| {
284            let items = if let Some(user) = self.user_store.read(cx).current_user() {
285                vec![
286                    ContextMenuItem::Static(Box::new(move |_| {
287                        Flex::row()
288                            .with_children(user.avatar.clone().map(|avatar| {
289                                Self::render_face(
290                                    avatar,
291                                    avatar_style.clone(),
292                                    Color::transparent_black(),
293                                )
294                            }))
295                            .with_child(
296                                Label::new(user.github_login.clone(), item_style.label.clone())
297                                    .boxed(),
298                            )
299                            .contained()
300                            .with_style(item_style.container)
301                            .boxed()
302                    })),
303                    ContextMenuItem::Item {
304                        label: "Sign out".into(),
305                        action: Box::new(SignOut),
306                    },
307                ]
308            } else {
309                vec![ContextMenuItem::Item {
310                    label: "Sign in".into(),
311                    action: Box::new(Authenticate),
312                }]
313            };
314
315            user_menu.show(
316                vec2f(
317                    theme
318                        .workspace
319                        .titlebar
320                        .user_menu_button
321                        .default
322                        .button_width,
323                    theme.workspace.titlebar.height,
324                ),
325                AnchorCorner::TopRight,
326                items,
327                cx,
328            );
329        });
330    }
331
332    fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
333        ActiveCall::global(cx)
334            .update(cx, |call, cx| call.hang_up(cx))
335            .log_err();
336    }
337
338    fn render_toggle_contacts_button(
339        &self,
340        theme: &Theme,
341        cx: &mut RenderContext<Self>,
342    ) -> ElementBox {
343        let titlebar = &theme.workspace.titlebar;
344
345        let badge = if self
346            .user_store
347            .read(cx)
348            .incoming_contact_requests()
349            .is_empty()
350        {
351            None
352        } else {
353            Some(
354                Empty::new()
355                    .collapsed()
356                    .contained()
357                    .with_style(titlebar.toggle_contacts_badge)
358                    .contained()
359                    .with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
360                    .with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
361                    .aligned()
362                    .boxed(),
363            )
364        };
365
366        Stack::new()
367            .with_child(
368                MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
369                    let style = titlebar
370                        .toggle_contacts_button
371                        .style_for(state, self.contacts_popover.is_some());
372                    Svg::new("icons/plus_8.svg")
373                        .with_color(style.color)
374                        .constrained()
375                        .with_width(style.icon_width)
376                        .aligned()
377                        .constrained()
378                        .with_width(style.button_width)
379                        .with_height(style.button_width)
380                        .contained()
381                        .with_style(style.container)
382                        .boxed()
383                })
384                .with_cursor_style(CursorStyle::PointingHand)
385                .on_click(MouseButton::Left, move |_, cx| {
386                    cx.dispatch_action(ToggleContactsMenu);
387                })
388                .with_tooltip::<ToggleContactsMenu, _>(
389                    0,
390                    "Show contacts menu".into(),
391                    Some(Box::new(ToggleContactsMenu)),
392                    theme.tooltip.clone(),
393                    cx,
394                )
395                .aligned()
396                .boxed(),
397            )
398            .with_children(badge)
399            .with_children(self.render_contacts_popover_host(titlebar, cx))
400            .boxed()
401    }
402
403    fn render_toggle_screen_sharing_button(
404        &self,
405        theme: &Theme,
406        room: &ModelHandle<Room>,
407        cx: &mut RenderContext<Self>,
408    ) -> ElementBox {
409        let icon;
410        let tooltip;
411        if room.read(cx).is_screen_sharing() {
412            icon = "icons/disable_screen_sharing_12.svg";
413            tooltip = "Stop Sharing Screen"
414        } else {
415            icon = "icons/enable_screen_sharing_12.svg";
416            tooltip = "Share Screen";
417        }
418
419        let titlebar = &theme.workspace.titlebar;
420        MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
421            let style = titlebar.call_control.style_for(state, false);
422            Svg::new(icon)
423                .with_color(style.color)
424                .constrained()
425                .with_width(style.icon_width)
426                .aligned()
427                .constrained()
428                .with_width(style.button_width)
429                .with_height(style.button_width)
430                .contained()
431                .with_style(style.container)
432                .boxed()
433        })
434        .with_cursor_style(CursorStyle::PointingHand)
435        .on_click(MouseButton::Left, move |_, cx| {
436            cx.dispatch_action(ToggleScreenSharing);
437        })
438        .with_tooltip::<ToggleScreenSharing, _>(
439            0,
440            tooltip.into(),
441            Some(Box::new(ToggleScreenSharing)),
442            theme.tooltip.clone(),
443            cx,
444        )
445        .aligned()
446        .boxed()
447    }
448
449    fn render_in_call_share_unshare_button(
450        &self,
451        workspace: &ViewHandle<Workspace>,
452        theme: &Theme,
453        cx: &mut RenderContext<Self>,
454    ) -> Option<ElementBox> {
455        let project = workspace.read(cx).project();
456        if project.read(cx).is_remote() {
457            return None;
458        }
459
460        let is_shared = project.read(cx).is_shared();
461        let label = if is_shared { "Unshare" } else { "Share" };
462        let tooltip = if is_shared {
463            "Unshare project from call participants"
464        } else {
465            "Share project with call participants"
466        };
467
468        let titlebar = &theme.workspace.titlebar;
469
470        enum ShareUnshare {}
471        Some(
472            Stack::new()
473                .with_child(
474                    MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
475                        //TODO: Ensure this button has consistant width for both text variations
476                        let style = titlebar
477                            .share_button
478                            .style_for(state, self.contacts_popover.is_some());
479                        Label::new(label, style.text.clone())
480                            .contained()
481                            .with_style(style.container)
482                            .boxed()
483                    })
484                    .with_cursor_style(CursorStyle::PointingHand)
485                    .on_click(MouseButton::Left, move |_, cx| {
486                        if is_shared {
487                            cx.dispatch_action(UnshareProject);
488                        } else {
489                            cx.dispatch_action(ShareProject);
490                        }
491                    })
492                    .with_tooltip::<ShareUnshare, _>(
493                        0,
494                        tooltip.to_owned(),
495                        None,
496                        theme.tooltip.clone(),
497                        cx,
498                    )
499                    .boxed(),
500                )
501                .aligned()
502                .contained()
503                .with_margin_left(theme.workspace.titlebar.item_spacing)
504                .boxed(),
505        )
506    }
507
508    fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
509        let titlebar = &theme.workspace.titlebar;
510
511        Stack::new()
512            .with_child(
513                MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
514                    let style = titlebar.call_control.style_for(state, false);
515                    Svg::new("icons/ellipsis_14.svg")
516                        .with_color(style.color)
517                        .constrained()
518                        .with_width(style.icon_width)
519                        .aligned()
520                        .constrained()
521                        .with_width(style.button_width)
522                        .with_height(style.button_width)
523                        .contained()
524                        .with_style(style.container)
525                        .boxed()
526                })
527                .with_cursor_style(CursorStyle::PointingHand)
528                .on_click(MouseButton::Left, move |_, cx| {
529                    cx.dispatch_action(ToggleUserMenu);
530                })
531                .with_tooltip::<ToggleUserMenu, _>(
532                    0,
533                    "Toggle user menu".to_owned(),
534                    Some(Box::new(ToggleUserMenu)),
535                    theme.tooltip.clone(),
536                    cx,
537                )
538                .contained()
539                .with_margin_left(theme.workspace.titlebar.item_spacing)
540                .aligned()
541                .boxed(),
542            )
543            .with_child(ChildView::new(&self.user_menu, cx).boxed())
544            .boxed()
545    }
546
547    fn render_contacts_popover_host<'a>(
548        &'a self,
549        theme: &'a theme::Titlebar,
550        cx: &'a RenderContext<Self>,
551    ) -> Option<ElementBox> {
552        self.contacts_popover.as_ref().map(|popover| {
553            Overlay::new(
554                ChildView::new(popover, cx)
555                    .contained()
556                    .with_margin_top(theme.height)
557                    .with_margin_left(theme.toggle_contacts_button.default.button_width)
558                    .with_margin_right(-theme.toggle_contacts_button.default.button_width)
559                    .boxed(),
560            )
561            .with_fit_mode(OverlayFitMode::SwitchAnchor)
562            .with_anchor_corner(AnchorCorner::BottomLeft)
563            .with_z_index(999)
564            .boxed()
565        })
566    }
567
568    fn render_collaborators(
569        &self,
570        workspace: &ViewHandle<Workspace>,
571        theme: &Theme,
572        room: &ModelHandle<Room>,
573        cx: &mut RenderContext<Self>,
574    ) -> Vec<ElementBox> {
575        let project = workspace.read(cx).project().read(cx);
576
577        let mut participants = room
578            .read(cx)
579            .remote_participants()
580            .values()
581            .cloned()
582            .collect::<Vec<_>>();
583        participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
584
585        participants
586            .into_iter()
587            .filter_map(|participant| {
588                let project = workspace.read(cx).project().read(cx);
589                let replica_id = project
590                    .collaborators()
591                    .get(&participant.peer_id)
592                    .map(|collaborator| collaborator.replica_id);
593                let user = participant.user.clone();
594                Some(
595                    Container::new(self.render_face_pile(
596                        &user,
597                        replica_id,
598                        participant.peer_id,
599                        Some(participant.location),
600                        workspace,
601                        theme,
602                        cx,
603                    ))
604                    .with_margin_right(theme.workspace.titlebar.face_pile_spacing)
605                    .boxed(),
606                )
607            })
608            .collect()
609    }
610
611    fn render_current_user(
612        &self,
613        workspace: &ViewHandle<Workspace>,
614        theme: &Theme,
615        user: &Arc<User>,
616        peer_id: PeerId,
617        cx: &mut RenderContext<Self>,
618    ) -> ElementBox {
619        let replica_id = workspace.read(cx).project().read(cx).replica_id();
620        Container::new(self.render_face_pile(
621            user,
622            Some(replica_id),
623            peer_id,
624            None,
625            workspace,
626            theme,
627            cx,
628        ))
629        .with_margin_right(theme.workspace.titlebar.item_spacing)
630        .boxed()
631    }
632
633    fn render_face_pile(
634        &self,
635        user: &User,
636        replica_id: Option<ReplicaId>,
637        peer_id: PeerId,
638        location: Option<ParticipantLocation>,
639        workspace: &ViewHandle<Workspace>,
640        theme: &Theme,
641        cx: &mut RenderContext<Self>,
642    ) -> ElementBox {
643        let project_id = workspace.read(cx).project().read(cx).remote_id();
644        let room = ActiveCall::global(cx).read(cx).room();
645        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
646        let followed_by_self = room
647            .and_then(|room| {
648                Some(
649                    is_being_followed
650                        && room
651                            .read(cx)
652                            .followers_for(peer_id, project_id?)
653                            .iter()
654                            .any(|&follower| {
655                                Some(follower) == workspace.read(cx).client().peer_id()
656                            }),
657                )
658            })
659            .unwrap_or(false);
660
661        let leader_style = theme.workspace.titlebar.leader_avatar;
662        let follower_style = theme.workspace.titlebar.follower_avatar;
663
664        let mut background_color = theme
665            .workspace
666            .titlebar
667            .container
668            .background_color
669            .unwrap_or_default();
670        if let Some(replica_id) = replica_id {
671            if followed_by_self {
672                let selection = theme.editor.replica_selection_style(replica_id).selection;
673                background_color = Color::blend(selection, background_color);
674                background_color.a = 255;
675            }
676        }
677
678        let mut content = Stack::new()
679            .with_children(user.avatar.as_ref().map(|avatar| {
680                let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
681                    .with_child(Self::render_face(
682                        avatar.clone(),
683                        Self::location_style(workspace, location, leader_style, cx),
684                        background_color,
685                    ))
686                    .with_children(
687                        (|| {
688                            let project_id = project_id?;
689                            let room = room?.read(cx);
690                            let followers = room.followers_for(peer_id, project_id);
691
692                            Some(followers.into_iter().flat_map(|&follower| {
693                                let remote_participant =
694                                    room.remote_participant_for_peer_id(follower);
695
696                                let avatar = remote_participant
697                                    .and_then(|p| p.user.avatar.clone())
698                                    .or_else(|| {
699                                        if follower == workspace.read(cx).client().peer_id()? {
700                                            workspace
701                                                .read(cx)
702                                                .user_store()
703                                                .read(cx)
704                                                .current_user()?
705                                                .avatar
706                                                .clone()
707                                        } else {
708                                            None
709                                        }
710                                    })?;
711
712                                let location = remote_participant.map(|p| p.location);
713
714                                Some(Self::render_face(
715                                    avatar.clone(),
716                                    Self::location_style(workspace, location, follower_style, cx),
717                                    background_color,
718                                ))
719                            }))
720                        })()
721                        .into_iter()
722                        .flatten(),
723                    );
724
725                let mut container = face_pile
726                    .contained()
727                    .with_style(theme.workspace.titlebar.leader_selection);
728
729                if let Some(replica_id) = replica_id {
730                    if followed_by_self {
731                        let color = theme.editor.replica_selection_style(replica_id).selection;
732                        container = container.with_background_color(color);
733                    }
734                }
735
736                container.boxed()
737            }))
738            .with_children((|| {
739                let replica_id = replica_id?;
740                let color = theme.editor.replica_selection_style(replica_id).cursor;
741                Some(
742                    AvatarRibbon::new(color)
743                        .constrained()
744                        .with_width(theme.workspace.titlebar.avatar_ribbon.width)
745                        .with_height(theme.workspace.titlebar.avatar_ribbon.height)
746                        .aligned()
747                        .bottom()
748                        .boxed(),
749                )
750            })())
751            .boxed();
752
753        if let Some(location) = location {
754            if let Some(replica_id) = replica_id {
755                content =
756                    MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
757                        content
758                    })
759                    .with_cursor_style(CursorStyle::PointingHand)
760                    .on_click(MouseButton::Left, move |_, cx| {
761                        cx.dispatch_action(ToggleFollow(peer_id))
762                    })
763                    .with_tooltip::<ToggleFollow, _>(
764                        peer_id.as_u64() as usize,
765                        if is_being_followed {
766                            format!("Unfollow {}", user.github_login)
767                        } else {
768                            format!("Follow {}", user.github_login)
769                        },
770                        Some(Box::new(FollowNextCollaborator)),
771                        theme.tooltip.clone(),
772                        cx,
773                    )
774                    .boxed();
775            } else if let ParticipantLocation::SharedProject { project_id } = location {
776                let user_id = user.id;
777                content = MouseEventHandler::<JoinProject>::new(
778                    peer_id.as_u64() as usize,
779                    cx,
780                    move |_, _| content,
781                )
782                .with_cursor_style(CursorStyle::PointingHand)
783                .on_click(MouseButton::Left, move |_, cx| {
784                    cx.dispatch_action(JoinProject {
785                        project_id,
786                        follow_user_id: user_id,
787                    })
788                })
789                .with_tooltip::<JoinProject, _>(
790                    peer_id.as_u64() as usize,
791                    format!("Follow {} into external project", user.github_login),
792                    Some(Box::new(FollowNextCollaborator)),
793                    theme.tooltip.clone(),
794                    cx,
795                )
796                .boxed();
797            }
798        }
799        content
800    }
801
802    fn location_style(
803        workspace: &ViewHandle<Workspace>,
804        location: Option<ParticipantLocation>,
805        mut style: AvatarStyle,
806        cx: &RenderContext<Self>,
807    ) -> AvatarStyle {
808        if let Some(location) = location {
809            if let ParticipantLocation::SharedProject { project_id } = location {
810                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
811                    style.image.grayscale = true;
812                }
813            } else {
814                style.image.grayscale = true;
815            }
816        }
817
818        style
819    }
820
821    fn render_face(
822        avatar: Arc<ImageData>,
823        avatar_style: AvatarStyle,
824        background_color: Color,
825    ) -> ElementBox {
826        Image::new(avatar)
827            .with_style(avatar_style.image)
828            .aligned()
829            .contained()
830            .with_background_color(background_color)
831            .with_corner_radius(avatar_style.outer_corner_radius)
832            .constrained()
833            .with_width(avatar_style.outer_width)
834            .with_height(avatar_style.outer_width)
835            .aligned()
836            .boxed()
837    }
838
839    fn render_connection_status(
840        &self,
841        status: &client::Status,
842        cx: &mut RenderContext<Self>,
843    ) -> Option<ElementBox> {
844        enum ConnectionStatusButton {}
845
846        let theme = &cx.global::<Settings>().theme.clone();
847        match status {
848            client::Status::ConnectionError
849            | client::Status::ConnectionLost
850            | client::Status::Reauthenticating { .. }
851            | client::Status::Reconnecting { .. }
852            | client::Status::ReconnectionError { .. } => Some(
853                Container::new(
854                    Align::new(
855                        ConstrainedBox::new(
856                            Svg::new("icons/cloud_slash_12.svg")
857                                .with_color(theme.workspace.titlebar.offline_icon.color)
858                                .boxed(),
859                        )
860                        .with_width(theme.workspace.titlebar.offline_icon.width)
861                        .boxed(),
862                    )
863                    .boxed(),
864                )
865                .with_style(theme.workspace.titlebar.offline_icon.container)
866                .boxed(),
867            ),
868            client::Status::UpgradeRequired => Some(
869                MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
870                    Label::new(
871                        "Please update Zed to collaborate",
872                        theme.workspace.titlebar.outdated_warning.text.clone(),
873                    )
874                    .contained()
875                    .with_style(theme.workspace.titlebar.outdated_warning.container)
876                    .aligned()
877                    .boxed()
878                })
879                .with_cursor_style(CursorStyle::PointingHand)
880                .on_click(MouseButton::Left, |_, cx| {
881                    cx.dispatch_action(auto_update::Check);
882                })
883                .boxed(),
884            ),
885            _ => None,
886        }
887    }
888}
889
890pub struct AvatarRibbon {
891    color: Color,
892}
893
894impl AvatarRibbon {
895    pub fn new(color: Color) -> AvatarRibbon {
896        AvatarRibbon { color }
897    }
898}
899
900impl Element for AvatarRibbon {
901    type LayoutState = ();
902
903    type PaintState = ();
904
905    fn layout(
906        &mut self,
907        constraint: gpui::SizeConstraint,
908        _: &mut gpui::LayoutContext,
909    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
910        (constraint.max, ())
911    }
912
913    fn paint(
914        &mut self,
915        bounds: gpui::geometry::rect::RectF,
916        _: gpui::geometry::rect::RectF,
917        _: &mut Self::LayoutState,
918        cx: &mut gpui::PaintContext,
919    ) -> Self::PaintState {
920        let mut path = PathBuilder::new();
921        path.reset(bounds.lower_left());
922        path.curve_to(
923            bounds.origin() + vec2f(bounds.height(), 0.),
924            bounds.origin(),
925        );
926        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
927        path.curve_to(bounds.lower_right(), bounds.upper_right());
928        path.line_to(bounds.lower_left());
929        cx.scene.push_path(path.build(self.color, None));
930    }
931
932    fn rect_for_text_range(
933        &self,
934        _: Range<usize>,
935        _: RectF,
936        _: RectF,
937        _: &Self::LayoutState,
938        _: &Self::PaintState,
939        _: &gpui::MeasurementContext,
940    ) -> Option<RectF> {
941        None
942    }
943
944    fn debug(
945        &self,
946        bounds: gpui::geometry::rect::RectF,
947        _: &Self::LayoutState,
948        _: &Self::PaintState,
949        _: &gpui::DebugContext,
950    ) -> gpui::json::Value {
951        json::json!({
952            "type": "AvatarRibbon",
953            "bounds": bounds.to_json(),
954            "color": self.color.to_json(),
955        })
956    }
957}