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
463                            .share_button
464                            .style_for(state, self.contacts_popover.is_some());
465                        Label::new(label, style.text.clone())
466                            .contained()
467                            .with_style(style.container)
468                            .boxed()
469                    })
470                    .with_cursor_style(CursorStyle::PointingHand)
471                    .on_click(MouseButton::Left, move |_, cx| {
472                        if is_shared {
473                            cx.dispatch_action(UnshareProject);
474                        } else {
475                            cx.dispatch_action(ShareProject);
476                        }
477                    })
478                    .with_tooltip::<ShareUnshare, _>(
479                        0,
480                        tooltip.to_owned(),
481                        None,
482                        theme.tooltip.clone(),
483                        cx,
484                    )
485                    .boxed(),
486                )
487                .aligned()
488                .contained()
489                .with_margin_left(theme.workspace.titlebar.item_spacing)
490                .boxed(),
491        )
492    }
493
494    fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
495        let titlebar = &theme.workspace.titlebar;
496
497        Stack::new()
498            .with_child(
499                MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
500                    let style = titlebar.call_control.style_for(state, false);
501                    Svg::new("icons/ellipsis_14.svg")
502                        .with_color(style.color)
503                        .constrained()
504                        .with_width(style.icon_width)
505                        .aligned()
506                        .constrained()
507                        .with_width(style.button_width)
508                        .with_height(style.button_width)
509                        .contained()
510                        .with_style(style.container)
511                        .boxed()
512                })
513                .with_cursor_style(CursorStyle::PointingHand)
514                .on_click(MouseButton::Left, move |_, cx| {
515                    cx.dispatch_action(ToggleUserMenu);
516                })
517                .with_tooltip::<ToggleUserMenu, _>(
518                    0,
519                    "Toggle user menu".to_owned(),
520                    Some(Box::new(ToggleUserMenu)),
521                    theme.tooltip.clone(),
522                    cx,
523                )
524                .contained()
525                .with_margin_left(theme.workspace.titlebar.item_spacing)
526                .boxed(),
527            )
528            .with_child(
529                ChildView::new(&self.user_menu, cx)
530                    .aligned()
531                    .bottom()
532                    .right()
533                    .boxed(),
534            )
535            .boxed()
536    }
537
538    fn render_sign_in_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
539        let titlebar = &theme.workspace.titlebar;
540        MouseEventHandler::<SignIn>::new(0, cx, |state, _| {
541            let style = titlebar.sign_in_prompt.style_for(state, false);
542            Label::new("Sign In", style.text.clone())
543                .contained()
544                .with_style(style.container)
545                .boxed()
546        })
547        .with_cursor_style(CursorStyle::PointingHand)
548        .on_click(MouseButton::Left, move |_, cx| {
549            cx.dispatch_action(SignIn);
550        })
551        .boxed()
552    }
553
554    fn render_contacts_popover_host<'a>(
555        &'a self,
556        _theme: &'a theme::Titlebar,
557        cx: &'a RenderContext<Self>,
558    ) -> Option<ElementBox> {
559        self.contacts_popover.as_ref().map(|popover| {
560            Overlay::new(ChildView::new(popover, cx).boxed())
561                .with_fit_mode(OverlayFitMode::SwitchAnchor)
562                .with_anchor_corner(AnchorCorner::TopRight)
563                .with_z_index(999)
564                .aligned()
565                .bottom()
566                .right()
567                .boxed()
568        })
569    }
570
571    fn render_collaborators(
572        &self,
573        workspace: &ViewHandle<Workspace>,
574        theme: &Theme,
575        room: &ModelHandle<Room>,
576        cx: &mut RenderContext<Self>,
577    ) -> Vec<ElementBox> {
578        let mut participants = room
579            .read(cx)
580            .remote_participants()
581            .values()
582            .cloned()
583            .collect::<Vec<_>>();
584        participants.sort_by_cached_key(|p| p.user.github_login.clone());
585
586        participants
587            .into_iter()
588            .filter_map(|participant| {
589                let project = workspace.read(cx).project().read(cx);
590                let replica_id = project
591                    .collaborators()
592                    .get(&participant.peer_id)
593                    .map(|collaborator| collaborator.replica_id);
594                let user = participant.user.clone();
595                Some(
596                    Container::new(self.render_face_pile(
597                        &user,
598                        replica_id,
599                        participant.peer_id,
600                        Some(participant.location),
601                        workspace,
602                        theme,
603                        cx,
604                    ))
605                    .with_margin_right(theme.workspace.titlebar.face_pile_spacing)
606                    .boxed(),
607                )
608            })
609            .collect()
610    }
611
612    fn render_current_user(
613        &self,
614        workspace: &ViewHandle<Workspace>,
615        theme: &Theme,
616        user: &Arc<User>,
617        peer_id: PeerId,
618        cx: &mut RenderContext<Self>,
619    ) -> ElementBox {
620        let replica_id = workspace.read(cx).project().read(cx).replica_id();
621        Container::new(self.render_face_pile(
622            user,
623            Some(replica_id),
624            peer_id,
625            None,
626            workspace,
627            theme,
628            cx,
629        ))
630        .with_margin_right(theme.workspace.titlebar.item_spacing)
631        .boxed()
632    }
633
634    fn render_face_pile(
635        &self,
636        user: &User,
637        replica_id: Option<ReplicaId>,
638        peer_id: PeerId,
639        location: Option<ParticipantLocation>,
640        workspace: &ViewHandle<Workspace>,
641        theme: &Theme,
642        cx: &mut RenderContext<Self>,
643    ) -> ElementBox {
644        let project_id = workspace.read(cx).project().read(cx).remote_id();
645        let room = ActiveCall::global(cx).read(cx).room();
646        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
647        let followed_by_self = room
648            .and_then(|room| {
649                Some(
650                    is_being_followed
651                        && room
652                            .read(cx)
653                            .followers_for(peer_id, project_id?)
654                            .iter()
655                            .any(|&follower| {
656                                Some(follower) == workspace.read(cx).client().peer_id()
657                            }),
658                )
659            })
660            .unwrap_or(false);
661
662        let leader_style = theme.workspace.titlebar.leader_avatar;
663        let follower_style = theme.workspace.titlebar.follower_avatar;
664
665        let mut background_color = theme
666            .workspace
667            .titlebar
668            .container
669            .background_color
670            .unwrap_or_default();
671        if let Some(replica_id) = replica_id {
672            if followed_by_self {
673                let selection = theme.editor.replica_selection_style(replica_id).selection;
674                background_color = Color::blend(selection, background_color);
675                background_color.a = 255;
676            }
677        }
678
679        let mut content = Stack::new()
680            .with_children(user.avatar.as_ref().map(|avatar| {
681                let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
682                    .with_child(Self::render_face(
683                        avatar.clone(),
684                        Self::location_style(workspace, location, leader_style, cx),
685                        background_color,
686                    ))
687                    .with_children(
688                        (|| {
689                            let project_id = project_id?;
690                            let room = room?.read(cx);
691                            let followers = room.followers_for(peer_id, project_id);
692
693                            Some(followers.into_iter().flat_map(|&follower| {
694                                let remote_participant =
695                                    room.remote_participant_for_peer_id(follower);
696
697                                let avatar = remote_participant
698                                    .and_then(|p| p.user.avatar.clone())
699                                    .or_else(|| {
700                                        if follower == workspace.read(cx).client().peer_id()? {
701                                            workspace
702                                                .read(cx)
703                                                .user_store()
704                                                .read(cx)
705                                                .current_user()?
706                                                .avatar
707                                                .clone()
708                                        } else {
709                                            None
710                                        }
711                                    })?;
712
713                                let location = remote_participant.map(|p| p.location);
714
715                                Some(Self::render_face(
716                                    avatar.clone(),
717                                    Self::location_style(workspace, location, follower_style, cx),
718                                    background_color,
719                                ))
720                            }))
721                        })()
722                        .into_iter()
723                        .flatten(),
724                    );
725
726                let mut container = face_pile
727                    .contained()
728                    .with_style(theme.workspace.titlebar.leader_selection);
729
730                if let Some(replica_id) = replica_id {
731                    if followed_by_self {
732                        let color = theme.editor.replica_selection_style(replica_id).selection;
733                        container = container.with_background_color(color);
734                    }
735                }
736
737                container.boxed()
738            }))
739            .with_children((|| {
740                let replica_id = replica_id?;
741                let color = theme.editor.replica_selection_style(replica_id).cursor;
742                Some(
743                    AvatarRibbon::new(color)
744                        .constrained()
745                        .with_width(theme.workspace.titlebar.avatar_ribbon.width)
746                        .with_height(theme.workspace.titlebar.avatar_ribbon.height)
747                        .aligned()
748                        .bottom()
749                        .boxed(),
750                )
751            })())
752            .boxed();
753
754        if let Some(location) = location {
755            if let Some(replica_id) = replica_id {
756                content =
757                    MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
758                        content
759                    })
760                    .with_cursor_style(CursorStyle::PointingHand)
761                    .on_click(MouseButton::Left, move |_, cx| {
762                        cx.dispatch_action(ToggleFollow(peer_id))
763                    })
764                    .with_tooltip::<ToggleFollow, _>(
765                        peer_id.as_u64() as usize,
766                        if is_being_followed {
767                            format!("Unfollow {}", user.github_login)
768                        } else {
769                            format!("Follow {}", user.github_login)
770                        },
771                        Some(Box::new(FollowNextCollaborator)),
772                        theme.tooltip.clone(),
773                        cx,
774                    )
775                    .boxed();
776            } else if let ParticipantLocation::SharedProject { project_id } = location {
777                let user_id = user.id;
778                content = MouseEventHandler::<JoinProject>::new(
779                    peer_id.as_u64() as usize,
780                    cx,
781                    move |_, _| content,
782                )
783                .with_cursor_style(CursorStyle::PointingHand)
784                .on_click(MouseButton::Left, move |_, cx| {
785                    cx.dispatch_action(JoinProject {
786                        project_id,
787                        follow_user_id: user_id,
788                    })
789                })
790                .with_tooltip::<JoinProject, _>(
791                    peer_id.as_u64() as usize,
792                    format!("Follow {} into external project", user.github_login),
793                    Some(Box::new(FollowNextCollaborator)),
794                    theme.tooltip.clone(),
795                    cx,
796                )
797                .boxed();
798            }
799        }
800        content
801    }
802
803    fn location_style(
804        workspace: &ViewHandle<Workspace>,
805        location: Option<ParticipantLocation>,
806        mut style: AvatarStyle,
807        cx: &RenderContext<Self>,
808    ) -> AvatarStyle {
809        if let Some(location) = location {
810            if let ParticipantLocation::SharedProject { project_id } = location {
811                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
812                    style.image.grayscale = true;
813                }
814            } else {
815                style.image.grayscale = true;
816            }
817        }
818
819        style
820    }
821
822    fn render_face(
823        avatar: Arc<ImageData>,
824        avatar_style: AvatarStyle,
825        background_color: Color,
826    ) -> ElementBox {
827        Image::from_data(avatar)
828            .with_style(avatar_style.image)
829            .aligned()
830            .contained()
831            .with_background_color(background_color)
832            .with_corner_radius(avatar_style.outer_corner_radius)
833            .constrained()
834            .with_width(avatar_style.outer_width)
835            .with_height(avatar_style.outer_width)
836            .aligned()
837            .boxed()
838    }
839
840    fn render_connection_status(
841        &self,
842        status: &client::Status,
843        cx: &mut RenderContext<Self>,
844    ) -> Option<ElementBox> {
845        enum ConnectionStatusButton {}
846
847        let theme = &cx.global::<Settings>().theme.clone();
848        match status {
849            client::Status::ConnectionError
850            | client::Status::ConnectionLost
851            | client::Status::Reauthenticating { .. }
852            | client::Status::Reconnecting { .. }
853            | client::Status::ReconnectionError { .. } => Some(
854                Container::new(
855                    Align::new(
856                        ConstrainedBox::new(
857                            Svg::new("icons/cloud_slash_12.svg")
858                                .with_color(theme.workspace.titlebar.offline_icon.color)
859                                .boxed(),
860                        )
861                        .with_width(theme.workspace.titlebar.offline_icon.width)
862                        .boxed(),
863                    )
864                    .boxed(),
865                )
866                .with_style(theme.workspace.titlebar.offline_icon.container)
867                .boxed(),
868            ),
869            client::Status::UpgradeRequired => Some(
870                MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
871                    Label::new(
872                        "Please update Zed to collaborate",
873                        theme.workspace.titlebar.outdated_warning.text.clone(),
874                    )
875                    .contained()
876                    .with_style(theme.workspace.titlebar.outdated_warning.container)
877                    .aligned()
878                    .boxed()
879                })
880                .with_cursor_style(CursorStyle::PointingHand)
881                .on_click(MouseButton::Left, |_, cx| {
882                    cx.dispatch_action(auto_update::Check);
883                })
884                .boxed(),
885            ),
886            _ => None,
887        }
888    }
889}
890
891pub struct AvatarRibbon {
892    color: Color,
893}
894
895impl AvatarRibbon {
896    pub fn new(color: Color) -> AvatarRibbon {
897        AvatarRibbon { color }
898    }
899}
900
901impl Element for AvatarRibbon {
902    type LayoutState = ();
903
904    type PaintState = ();
905
906    fn layout(
907        &mut self,
908        constraint: gpui::SizeConstraint,
909        _: &mut gpui::LayoutContext,
910    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
911        (constraint.max, ())
912    }
913
914    fn paint(
915        &mut self,
916        bounds: gpui::geometry::rect::RectF,
917        _: gpui::geometry::rect::RectF,
918        _: &mut Self::LayoutState,
919        cx: &mut gpui::PaintContext,
920    ) -> Self::PaintState {
921        let mut path = PathBuilder::new();
922        path.reset(bounds.lower_left());
923        path.curve_to(
924            bounds.origin() + vec2f(bounds.height(), 0.),
925            bounds.origin(),
926        );
927        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
928        path.curve_to(bounds.lower_right(), bounds.upper_right());
929        path.line_to(bounds.lower_left());
930        cx.scene.push_path(path.build(self.color, None));
931    }
932
933    fn rect_for_text_range(
934        &self,
935        _: Range<usize>,
936        _: RectF,
937        _: RectF,
938        _: &Self::LayoutState,
939        _: &Self::PaintState,
940        _: &gpui::MeasurementContext,
941    ) -> Option<RectF> {
942        None
943    }
944
945    fn debug(
946        &self,
947        bounds: gpui::geometry::rect::RectF,
948        _: &Self::LayoutState,
949        _: &Self::PaintState,
950        _: &gpui::DebugContext,
951    ) -> gpui::json::Value {
952        json::json!({
953            "type": "AvatarRibbon",
954            "bounds": bounds.to_json(),
955            "color": self.color.to_json(),
956        })
957    }
958}