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