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 mut participants = room
576            .read(cx)
577            .remote_participants()
578            .values()
579            .cloned()
580            .collect::<Vec<_>>();
581        participants.sort_by_cached_key(|p| p.user.github_login.clone());
582
583        participants
584            .into_iter()
585            .filter_map(|participant| {
586                let project = workspace.read(cx).project().read(cx);
587                let replica_id = project
588                    .collaborators()
589                    .get(&participant.peer_id)
590                    .map(|collaborator| collaborator.replica_id);
591                let user = participant.user.clone();
592                Some(
593                    Container::new(self.render_face_pile(
594                        &user,
595                        replica_id,
596                        participant.peer_id,
597                        Some(participant.location),
598                        workspace,
599                        theme,
600                        cx,
601                    ))
602                    .with_margin_right(theme.workspace.titlebar.face_pile_spacing)
603                    .boxed(),
604                )
605            })
606            .collect()
607    }
608
609    fn render_current_user(
610        &self,
611        workspace: &ViewHandle<Workspace>,
612        theme: &Theme,
613        user: &Arc<User>,
614        peer_id: PeerId,
615        cx: &mut RenderContext<Self>,
616    ) -> ElementBox {
617        let replica_id = workspace.read(cx).project().read(cx).replica_id();
618        Container::new(self.render_face_pile(
619            user,
620            Some(replica_id),
621            peer_id,
622            None,
623            workspace,
624            theme,
625            cx,
626        ))
627        .with_margin_right(theme.workspace.titlebar.item_spacing)
628        .boxed()
629    }
630
631    fn render_face_pile(
632        &self,
633        user: &User,
634        replica_id: Option<ReplicaId>,
635        peer_id: PeerId,
636        location: Option<ParticipantLocation>,
637        workspace: &ViewHandle<Workspace>,
638        theme: &Theme,
639        cx: &mut RenderContext<Self>,
640    ) -> ElementBox {
641        let project_id = workspace.read(cx).project().read(cx).remote_id();
642        let room = ActiveCall::global(cx).read(cx).room();
643        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
644        let followed_by_self = room
645            .and_then(|room| {
646                Some(
647                    is_being_followed
648                        && room
649                            .read(cx)
650                            .followers_for(peer_id, project_id?)
651                            .iter()
652                            .any(|&follower| {
653                                Some(follower) == workspace.read(cx).client().peer_id()
654                            }),
655                )
656            })
657            .unwrap_or(false);
658
659        let leader_style = theme.workspace.titlebar.leader_avatar;
660        let follower_style = theme.workspace.titlebar.follower_avatar;
661
662        let mut background_color = theme
663            .workspace
664            .titlebar
665            .container
666            .background_color
667            .unwrap_or_default();
668        if let Some(replica_id) = replica_id {
669            if followed_by_self {
670                let selection = theme.editor.replica_selection_style(replica_id).selection;
671                background_color = Color::blend(selection, background_color);
672                background_color.a = 255;
673            }
674        }
675
676        let mut content = Stack::new()
677            .with_children(user.avatar.as_ref().map(|avatar| {
678                let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
679                    .with_child(Self::render_face(
680                        avatar.clone(),
681                        Self::location_style(workspace, location, leader_style, cx),
682                        background_color,
683                    ))
684                    .with_children(
685                        (|| {
686                            let project_id = project_id?;
687                            let room = room?.read(cx);
688                            let followers = room.followers_for(peer_id, project_id);
689
690                            Some(followers.into_iter().flat_map(|&follower| {
691                                let remote_participant =
692                                    room.remote_participant_for_peer_id(follower);
693
694                                let avatar = remote_participant
695                                    .and_then(|p| p.user.avatar.clone())
696                                    .or_else(|| {
697                                        if follower == workspace.read(cx).client().peer_id()? {
698                                            workspace
699                                                .read(cx)
700                                                .user_store()
701                                                .read(cx)
702                                                .current_user()?
703                                                .avatar
704                                                .clone()
705                                        } else {
706                                            None
707                                        }
708                                    })?;
709
710                                let location = remote_participant.map(|p| p.location);
711
712                                Some(Self::render_face(
713                                    avatar.clone(),
714                                    Self::location_style(workspace, location, follower_style, cx),
715                                    background_color,
716                                ))
717                            }))
718                        })()
719                        .into_iter()
720                        .flatten(),
721                    );
722
723                let mut container = face_pile
724                    .contained()
725                    .with_style(theme.workspace.titlebar.leader_selection);
726
727                if let Some(replica_id) = replica_id {
728                    if followed_by_self {
729                        let color = theme.editor.replica_selection_style(replica_id).selection;
730                        container = container.with_background_color(color);
731                    }
732                }
733
734                container.boxed()
735            }))
736            .with_children((|| {
737                let replica_id = replica_id?;
738                let color = theme.editor.replica_selection_style(replica_id).cursor;
739                Some(
740                    AvatarRibbon::new(color)
741                        .constrained()
742                        .with_width(theme.workspace.titlebar.avatar_ribbon.width)
743                        .with_height(theme.workspace.titlebar.avatar_ribbon.height)
744                        .aligned()
745                        .bottom()
746                        .boxed(),
747                )
748            })())
749            .boxed();
750
751        if let Some(location) = location {
752            if let Some(replica_id) = replica_id {
753                content =
754                    MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
755                        content
756                    })
757                    .with_cursor_style(CursorStyle::PointingHand)
758                    .on_click(MouseButton::Left, move |_, cx| {
759                        cx.dispatch_action(ToggleFollow(peer_id))
760                    })
761                    .with_tooltip::<ToggleFollow, _>(
762                        peer_id.as_u64() as usize,
763                        if is_being_followed {
764                            format!("Unfollow {}", user.github_login)
765                        } else {
766                            format!("Follow {}", user.github_login)
767                        },
768                        Some(Box::new(FollowNextCollaborator)),
769                        theme.tooltip.clone(),
770                        cx,
771                    )
772                    .boxed();
773            } else if let ParticipantLocation::SharedProject { project_id } = location {
774                let user_id = user.id;
775                content = MouseEventHandler::<JoinProject>::new(
776                    peer_id.as_u64() as usize,
777                    cx,
778                    move |_, _| content,
779                )
780                .with_cursor_style(CursorStyle::PointingHand)
781                .on_click(MouseButton::Left, move |_, cx| {
782                    cx.dispatch_action(JoinProject {
783                        project_id,
784                        follow_user_id: user_id,
785                    })
786                })
787                .with_tooltip::<JoinProject, _>(
788                    peer_id.as_u64() as usize,
789                    format!("Follow {} into external project", user.github_login),
790                    Some(Box::new(FollowNextCollaborator)),
791                    theme.tooltip.clone(),
792                    cx,
793                )
794                .boxed();
795            }
796        }
797        content
798    }
799
800    fn location_style(
801        workspace: &ViewHandle<Workspace>,
802        location: Option<ParticipantLocation>,
803        mut style: AvatarStyle,
804        cx: &RenderContext<Self>,
805    ) -> AvatarStyle {
806        if let Some(location) = location {
807            if let ParticipantLocation::SharedProject { project_id } = location {
808                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
809                    style.image.grayscale = true;
810                }
811            } else {
812                style.image.grayscale = true;
813            }
814        }
815
816        style
817    }
818
819    fn render_face(
820        avatar: Arc<ImageData>,
821        avatar_style: AvatarStyle,
822        background_color: Color,
823    ) -> ElementBox {
824        Image::new(avatar)
825            .with_style(avatar_style.image)
826            .aligned()
827            .contained()
828            .with_background_color(background_color)
829            .with_corner_radius(avatar_style.outer_corner_radius)
830            .constrained()
831            .with_width(avatar_style.outer_width)
832            .with_height(avatar_style.outer_width)
833            .aligned()
834            .boxed()
835    }
836
837    fn render_connection_status(
838        &self,
839        status: &client::Status,
840        cx: &mut RenderContext<Self>,
841    ) -> Option<ElementBox> {
842        enum ConnectionStatusButton {}
843
844        let theme = &cx.global::<Settings>().theme.clone();
845        match status {
846            client::Status::ConnectionError
847            | client::Status::ConnectionLost
848            | client::Status::Reauthenticating { .. }
849            | client::Status::Reconnecting { .. }
850            | client::Status::ReconnectionError { .. } => Some(
851                Container::new(
852                    Align::new(
853                        ConstrainedBox::new(
854                            Svg::new("icons/cloud_slash_12.svg")
855                                .with_color(theme.workspace.titlebar.offline_icon.color)
856                                .boxed(),
857                        )
858                        .with_width(theme.workspace.titlebar.offline_icon.width)
859                        .boxed(),
860                    )
861                    .boxed(),
862                )
863                .with_style(theme.workspace.titlebar.offline_icon.container)
864                .boxed(),
865            ),
866            client::Status::UpgradeRequired => Some(
867                MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
868                    Label::new(
869                        "Please update Zed to collaborate",
870                        theme.workspace.titlebar.outdated_warning.text.clone(),
871                    )
872                    .contained()
873                    .with_style(theme.workspace.titlebar.outdated_warning.container)
874                    .aligned()
875                    .boxed()
876                })
877                .with_cursor_style(CursorStyle::PointingHand)
878                .on_click(MouseButton::Left, |_, cx| {
879                    cx.dispatch_action(auto_update::Check);
880                })
881                .boxed(),
882            ),
883            _ => None,
884        }
885    }
886}
887
888pub struct AvatarRibbon {
889    color: Color,
890}
891
892impl AvatarRibbon {
893    pub fn new(color: Color) -> AvatarRibbon {
894        AvatarRibbon { color }
895    }
896}
897
898impl Element for AvatarRibbon {
899    type LayoutState = ();
900
901    type PaintState = ();
902
903    fn layout(
904        &mut self,
905        constraint: gpui::SizeConstraint,
906        _: &mut gpui::LayoutContext,
907    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
908        (constraint.max, ())
909    }
910
911    fn paint(
912        &mut self,
913        bounds: gpui::geometry::rect::RectF,
914        _: gpui::geometry::rect::RectF,
915        _: &mut Self::LayoutState,
916        cx: &mut gpui::PaintContext,
917    ) -> Self::PaintState {
918        let mut path = PathBuilder::new();
919        path.reset(bounds.lower_left());
920        path.curve_to(
921            bounds.origin() + vec2f(bounds.height(), 0.),
922            bounds.origin(),
923        );
924        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
925        path.curve_to(bounds.lower_right(), bounds.upper_right());
926        path.line_to(bounds.lower_left());
927        cx.scene.push_path(path.build(self.color, None));
928    }
929
930    fn rect_for_text_range(
931        &self,
932        _: Range<usize>,
933        _: RectF,
934        _: RectF,
935        _: &Self::LayoutState,
936        _: &Self::PaintState,
937        _: &gpui::MeasurementContext,
938    ) -> Option<RectF> {
939        None
940    }
941
942    fn debug(
943        &self,
944        bounds: gpui::geometry::rect::RectF,
945        _: &Self::LayoutState,
946        _: &Self::PaintState,
947        _: &gpui::DebugContext,
948    ) -> gpui::json::Value {
949        json::json!({
950            "type": "AvatarRibbon",
951            "bounds": bounds.to_json(),
952            "color": self.color.to_json(),
953        })
954    }
955}