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                    ContextMenuItem::Item {
308                        label: "Give Feedback".into(),
309                        action: Box::new(feedback::feedback_editor::GiveFeedback),
310                    },
311                ]
312            } else {
313                vec![
314                    ContextMenuItem::Item {
315                        label: "Sign in".into(),
316                        action: Box::new(Authenticate),
317                    },
318                    ContextMenuItem::Item {
319                        label: "Give Feedback".into(),
320                        action: Box::new(feedback::feedback_editor::GiveFeedback),
321                    },
322                ]
323            };
324
325            user_menu.show(
326                vec2f(
327                    theme
328                        .workspace
329                        .titlebar
330                        .user_menu_button
331                        .default
332                        .button_width,
333                    theme.workspace.titlebar.height,
334                ),
335                AnchorCorner::TopRight,
336                items,
337                cx,
338            );
339        });
340    }
341
342    fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
343        ActiveCall::global(cx)
344            .update(cx, |call, cx| call.hang_up(cx))
345            .detach_and_log_err(cx);
346    }
347
348    fn render_toggle_contacts_button(
349        &self,
350        theme: &Theme,
351        cx: &mut RenderContext<Self>,
352    ) -> ElementBox {
353        let titlebar = &theme.workspace.titlebar;
354
355        let badge = if self
356            .user_store
357            .read(cx)
358            .incoming_contact_requests()
359            .is_empty()
360        {
361            None
362        } else {
363            Some(
364                Empty::new()
365                    .collapsed()
366                    .contained()
367                    .with_style(titlebar.toggle_contacts_badge)
368                    .contained()
369                    .with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
370                    .with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
371                    .aligned()
372                    .boxed(),
373            )
374        };
375
376        Stack::new()
377            .with_child(
378                MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
379                    let style = titlebar
380                        .toggle_contacts_button
381                        .style_for(state, self.contacts_popover.is_some());
382                    Svg::new("icons/plus_8.svg")
383                        .with_color(style.color)
384                        .constrained()
385                        .with_width(style.icon_width)
386                        .aligned()
387                        .constrained()
388                        .with_width(style.button_width)
389                        .with_height(style.button_width)
390                        .contained()
391                        .with_style(style.container)
392                        .boxed()
393                })
394                .with_cursor_style(CursorStyle::PointingHand)
395                .on_click(MouseButton::Left, move |_, cx| {
396                    cx.dispatch_action(ToggleContactsMenu);
397                })
398                .with_tooltip::<ToggleContactsMenu, _>(
399                    0,
400                    "Show contacts menu".into(),
401                    Some(Box::new(ToggleContactsMenu)),
402                    theme.tooltip.clone(),
403                    cx,
404                )
405                .aligned()
406                .boxed(),
407            )
408            .with_children(badge)
409            .with_children(self.render_contacts_popover_host(titlebar, cx))
410            .boxed()
411    }
412
413    fn render_toggle_screen_sharing_button(
414        &self,
415        theme: &Theme,
416        room: &ModelHandle<Room>,
417        cx: &mut RenderContext<Self>,
418    ) -> ElementBox {
419        let icon;
420        let tooltip;
421        if room.read(cx).is_screen_sharing() {
422            icon = "icons/disable_screen_sharing_12.svg";
423            tooltip = "Stop Sharing Screen"
424        } else {
425            icon = "icons/enable_screen_sharing_12.svg";
426            tooltip = "Share Screen";
427        }
428
429        let titlebar = &theme.workspace.titlebar;
430        MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
431            let style = titlebar.call_control.style_for(state, false);
432            Svg::new(icon)
433                .with_color(style.color)
434                .constrained()
435                .with_width(style.icon_width)
436                .aligned()
437                .constrained()
438                .with_width(style.button_width)
439                .with_height(style.button_width)
440                .contained()
441                .with_style(style.container)
442                .boxed()
443        })
444        .with_cursor_style(CursorStyle::PointingHand)
445        .on_click(MouseButton::Left, move |_, cx| {
446            cx.dispatch_action(ToggleScreenSharing);
447        })
448        .with_tooltip::<ToggleScreenSharing, _>(
449            0,
450            tooltip.into(),
451            Some(Box::new(ToggleScreenSharing)),
452            theme.tooltip.clone(),
453            cx,
454        )
455        .aligned()
456        .boxed()
457    }
458
459    fn render_in_call_share_unshare_button(
460        &self,
461        workspace: &ViewHandle<Workspace>,
462        theme: &Theme,
463        cx: &mut RenderContext<Self>,
464    ) -> Option<ElementBox> {
465        let project = workspace.read(cx).project();
466        if project.read(cx).is_remote() {
467            return None;
468        }
469
470        let is_shared = project.read(cx).is_shared();
471        let label = if is_shared { "Unshare" } else { "Share" };
472        let tooltip = if is_shared {
473            "Unshare project from call participants"
474        } else {
475            "Share project with call participants"
476        };
477
478        let titlebar = &theme.workspace.titlebar;
479
480        enum ShareUnshare {}
481        Some(
482            Stack::new()
483                .with_child(
484                    MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
485                        //TODO: Ensure this button has consistant width for both text variations
486                        let style = titlebar
487                            .share_button
488                            .style_for(state, self.contacts_popover.is_some());
489                        Label::new(label, style.text.clone())
490                            .contained()
491                            .with_style(style.container)
492                            .boxed()
493                    })
494                    .with_cursor_style(CursorStyle::PointingHand)
495                    .on_click(MouseButton::Left, move |_, cx| {
496                        if is_shared {
497                            cx.dispatch_action(UnshareProject);
498                        } else {
499                            cx.dispatch_action(ShareProject);
500                        }
501                    })
502                    .with_tooltip::<ShareUnshare, _>(
503                        0,
504                        tooltip.to_owned(),
505                        None,
506                        theme.tooltip.clone(),
507                        cx,
508                    )
509                    .boxed(),
510                )
511                .aligned()
512                .contained()
513                .with_margin_left(theme.workspace.titlebar.item_spacing)
514                .boxed(),
515        )
516    }
517
518    fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
519        let titlebar = &theme.workspace.titlebar;
520
521        Stack::new()
522            .with_child(
523                MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
524                    let style = titlebar.call_control.style_for(state, false);
525                    Svg::new("icons/ellipsis_14.svg")
526                        .with_color(style.color)
527                        .constrained()
528                        .with_width(style.icon_width)
529                        .aligned()
530                        .constrained()
531                        .with_width(style.button_width)
532                        .with_height(style.button_width)
533                        .contained()
534                        .with_style(style.container)
535                        .boxed()
536                })
537                .with_cursor_style(CursorStyle::PointingHand)
538                .on_click(MouseButton::Left, move |_, cx| {
539                    cx.dispatch_action(ToggleUserMenu);
540                })
541                .with_tooltip::<ToggleUserMenu, _>(
542                    0,
543                    "Toggle user menu".to_owned(),
544                    Some(Box::new(ToggleUserMenu)),
545                    theme.tooltip.clone(),
546                    cx,
547                )
548                .contained()
549                .with_margin_left(theme.workspace.titlebar.item_spacing)
550                .aligned()
551                .boxed(),
552            )
553            .with_child(ChildView::new(&self.user_menu, cx).boxed())
554            .boxed()
555    }
556
557    fn render_contacts_popover_host<'a>(
558        &'a self,
559        theme: &'a theme::Titlebar,
560        cx: &'a RenderContext<Self>,
561    ) -> Option<ElementBox> {
562        self.contacts_popover.as_ref().map(|popover| {
563            Overlay::new(
564                ChildView::new(popover, cx)
565                    .contained()
566                    .with_margin_top(theme.height)
567                    .with_margin_left(theme.toggle_contacts_button.default.button_width)
568                    .with_margin_right(-theme.toggle_contacts_button.default.button_width)
569                    .boxed(),
570            )
571            .with_fit_mode(OverlayFitMode::SwitchAnchor)
572            .with_anchor_corner(AnchorCorner::BottomLeft)
573            .with_z_index(999)
574            .boxed()
575        })
576    }
577
578    fn render_collaborators(
579        &self,
580        workspace: &ViewHandle<Workspace>,
581        theme: &Theme,
582        room: &ModelHandle<Room>,
583        cx: &mut RenderContext<Self>,
584    ) -> Vec<ElementBox> {
585        let mut participants = room
586            .read(cx)
587            .remote_participants()
588            .values()
589            .cloned()
590            .collect::<Vec<_>>();
591        participants.sort_by_cached_key(|p| p.user.github_login.clone());
592
593        participants
594            .into_iter()
595            .filter_map(|participant| {
596                let project = workspace.read(cx).project().read(cx);
597                let replica_id = project
598                    .collaborators()
599                    .get(&participant.peer_id)
600                    .map(|collaborator| collaborator.replica_id);
601                let user = participant.user.clone();
602                Some(
603                    Container::new(self.render_face_pile(
604                        &user,
605                        replica_id,
606                        participant.peer_id,
607                        Some(participant.location),
608                        workspace,
609                        theme,
610                        cx,
611                    ))
612                    .with_margin_right(theme.workspace.titlebar.face_pile_spacing)
613                    .boxed(),
614                )
615            })
616            .collect()
617    }
618
619    fn render_current_user(
620        &self,
621        workspace: &ViewHandle<Workspace>,
622        theme: &Theme,
623        user: &Arc<User>,
624        peer_id: PeerId,
625        cx: &mut RenderContext<Self>,
626    ) -> ElementBox {
627        let replica_id = workspace.read(cx).project().read(cx).replica_id();
628        Container::new(self.render_face_pile(
629            user,
630            Some(replica_id),
631            peer_id,
632            None,
633            workspace,
634            theme,
635            cx,
636        ))
637        .with_margin_right(theme.workspace.titlebar.item_spacing)
638        .boxed()
639    }
640
641    fn render_face_pile(
642        &self,
643        user: &User,
644        replica_id: Option<ReplicaId>,
645        peer_id: PeerId,
646        location: Option<ParticipantLocation>,
647        workspace: &ViewHandle<Workspace>,
648        theme: &Theme,
649        cx: &mut RenderContext<Self>,
650    ) -> ElementBox {
651        let project_id = workspace.read(cx).project().read(cx).remote_id();
652        let room = ActiveCall::global(cx).read(cx).room();
653        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
654        let followed_by_self = room
655            .and_then(|room| {
656                Some(
657                    is_being_followed
658                        && room
659                            .read(cx)
660                            .followers_for(peer_id, project_id?)
661                            .iter()
662                            .any(|&follower| {
663                                Some(follower) == workspace.read(cx).client().peer_id()
664                            }),
665                )
666            })
667            .unwrap_or(false);
668
669        let leader_style = theme.workspace.titlebar.leader_avatar;
670        let follower_style = theme.workspace.titlebar.follower_avatar;
671
672        let mut background_color = theme
673            .workspace
674            .titlebar
675            .container
676            .background_color
677            .unwrap_or_default();
678        if let Some(replica_id) = replica_id {
679            if followed_by_self {
680                let selection = theme.editor.replica_selection_style(replica_id).selection;
681                background_color = Color::blend(selection, background_color);
682                background_color.a = 255;
683            }
684        }
685
686        let mut content = Stack::new()
687            .with_children(user.avatar.as_ref().map(|avatar| {
688                let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
689                    .with_child(Self::render_face(
690                        avatar.clone(),
691                        Self::location_style(workspace, location, leader_style, cx),
692                        background_color,
693                    ))
694                    .with_children(
695                        (|| {
696                            let project_id = project_id?;
697                            let room = room?.read(cx);
698                            let followers = room.followers_for(peer_id, project_id);
699
700                            Some(followers.into_iter().flat_map(|&follower| {
701                                let remote_participant =
702                                    room.remote_participant_for_peer_id(follower);
703
704                                let avatar = remote_participant
705                                    .and_then(|p| p.user.avatar.clone())
706                                    .or_else(|| {
707                                        if follower == workspace.read(cx).client().peer_id()? {
708                                            workspace
709                                                .read(cx)
710                                                .user_store()
711                                                .read(cx)
712                                                .current_user()?
713                                                .avatar
714                                                .clone()
715                                        } else {
716                                            None
717                                        }
718                                    })?;
719
720                                let location = remote_participant.map(|p| p.location);
721
722                                Some(Self::render_face(
723                                    avatar.clone(),
724                                    Self::location_style(workspace, location, follower_style, cx),
725                                    background_color,
726                                ))
727                            }))
728                        })()
729                        .into_iter()
730                        .flatten(),
731                    );
732
733                let mut container = face_pile
734                    .contained()
735                    .with_style(theme.workspace.titlebar.leader_selection);
736
737                if let Some(replica_id) = replica_id {
738                    if followed_by_self {
739                        let color = theme.editor.replica_selection_style(replica_id).selection;
740                        container = container.with_background_color(color);
741                    }
742                }
743
744                container.boxed()
745            }))
746            .with_children((|| {
747                let replica_id = replica_id?;
748                let color = theme.editor.replica_selection_style(replica_id).cursor;
749                Some(
750                    AvatarRibbon::new(color)
751                        .constrained()
752                        .with_width(theme.workspace.titlebar.avatar_ribbon.width)
753                        .with_height(theme.workspace.titlebar.avatar_ribbon.height)
754                        .aligned()
755                        .bottom()
756                        .boxed(),
757                )
758            })())
759            .boxed();
760
761        if let Some(location) = location {
762            if let Some(replica_id) = replica_id {
763                content =
764                    MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
765                        content
766                    })
767                    .with_cursor_style(CursorStyle::PointingHand)
768                    .on_click(MouseButton::Left, move |_, cx| {
769                        cx.dispatch_action(ToggleFollow(peer_id))
770                    })
771                    .with_tooltip::<ToggleFollow, _>(
772                        peer_id.as_u64() as usize,
773                        if is_being_followed {
774                            format!("Unfollow {}", user.github_login)
775                        } else {
776                            format!("Follow {}", user.github_login)
777                        },
778                        Some(Box::new(FollowNextCollaborator)),
779                        theme.tooltip.clone(),
780                        cx,
781                    )
782                    .boxed();
783            } else if let ParticipantLocation::SharedProject { project_id } = location {
784                let user_id = user.id;
785                content = MouseEventHandler::<JoinProject>::new(
786                    peer_id.as_u64() as usize,
787                    cx,
788                    move |_, _| content,
789                )
790                .with_cursor_style(CursorStyle::PointingHand)
791                .on_click(MouseButton::Left, move |_, cx| {
792                    cx.dispatch_action(JoinProject {
793                        project_id,
794                        follow_user_id: user_id,
795                    })
796                })
797                .with_tooltip::<JoinProject, _>(
798                    peer_id.as_u64() as usize,
799                    format!("Follow {} into external project", user.github_login),
800                    Some(Box::new(FollowNextCollaborator)),
801                    theme.tooltip.clone(),
802                    cx,
803                )
804                .boxed();
805            }
806        }
807        content
808    }
809
810    fn location_style(
811        workspace: &ViewHandle<Workspace>,
812        location: Option<ParticipantLocation>,
813        mut style: AvatarStyle,
814        cx: &RenderContext<Self>,
815    ) -> AvatarStyle {
816        if let Some(location) = location {
817            if let ParticipantLocation::SharedProject { project_id } = location {
818                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
819                    style.image.grayscale = true;
820                }
821            } else {
822                style.image.grayscale = true;
823            }
824        }
825
826        style
827    }
828
829    fn render_face(
830        avatar: Arc<ImageData>,
831        avatar_style: AvatarStyle,
832        background_color: Color,
833    ) -> ElementBox {
834        Image::from_data(avatar)
835            .with_style(avatar_style.image)
836            .aligned()
837            .contained()
838            .with_background_color(background_color)
839            .with_corner_radius(avatar_style.outer_corner_radius)
840            .constrained()
841            .with_width(avatar_style.outer_width)
842            .with_height(avatar_style.outer_width)
843            .aligned()
844            .boxed()
845    }
846
847    fn render_connection_status(
848        &self,
849        status: &client::Status,
850        cx: &mut RenderContext<Self>,
851    ) -> Option<ElementBox> {
852        enum ConnectionStatusButton {}
853
854        let theme = &cx.global::<Settings>().theme.clone();
855        match status {
856            client::Status::ConnectionError
857            | client::Status::ConnectionLost
858            | client::Status::Reauthenticating { .. }
859            | client::Status::Reconnecting { .. }
860            | client::Status::ReconnectionError { .. } => Some(
861                Container::new(
862                    Align::new(
863                        ConstrainedBox::new(
864                            Svg::new("icons/cloud_slash_12.svg")
865                                .with_color(theme.workspace.titlebar.offline_icon.color)
866                                .boxed(),
867                        )
868                        .with_width(theme.workspace.titlebar.offline_icon.width)
869                        .boxed(),
870                    )
871                    .boxed(),
872                )
873                .with_style(theme.workspace.titlebar.offline_icon.container)
874                .boxed(),
875            ),
876            client::Status::UpgradeRequired => Some(
877                MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
878                    Label::new(
879                        "Please update Zed to collaborate",
880                        theme.workspace.titlebar.outdated_warning.text.clone(),
881                    )
882                    .contained()
883                    .with_style(theme.workspace.titlebar.outdated_warning.container)
884                    .aligned()
885                    .boxed()
886                })
887                .with_cursor_style(CursorStyle::PointingHand)
888                .on_click(MouseButton::Left, |_, cx| {
889                    cx.dispatch_action(auto_update::Check);
890                })
891                .boxed(),
892            ),
893            _ => None,
894        }
895    }
896}
897
898pub struct AvatarRibbon {
899    color: Color,
900}
901
902impl AvatarRibbon {
903    pub fn new(color: Color) -> AvatarRibbon {
904        AvatarRibbon { color }
905    }
906}
907
908impl Element for AvatarRibbon {
909    type LayoutState = ();
910
911    type PaintState = ();
912
913    fn layout(
914        &mut self,
915        constraint: gpui::SizeConstraint,
916        _: &mut gpui::LayoutContext,
917    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
918        (constraint.max, ())
919    }
920
921    fn paint(
922        &mut self,
923        bounds: gpui::geometry::rect::RectF,
924        _: gpui::geometry::rect::RectF,
925        _: &mut Self::LayoutState,
926        cx: &mut gpui::PaintContext,
927    ) -> Self::PaintState {
928        let mut path = PathBuilder::new();
929        path.reset(bounds.lower_left());
930        path.curve_to(
931            bounds.origin() + vec2f(bounds.height(), 0.),
932            bounds.origin(),
933        );
934        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
935        path.curve_to(bounds.lower_right(), bounds.upper_right());
936        path.line_to(bounds.lower_left());
937        cx.scene.push_path(path.build(self.color, None));
938    }
939
940    fn rect_for_text_range(
941        &self,
942        _: Range<usize>,
943        _: RectF,
944        _: RectF,
945        _: &Self::LayoutState,
946        _: &Self::PaintState,
947        _: &gpui::MeasurementContext,
948    ) -> Option<RectF> {
949        None
950    }
951
952    fn debug(
953        &self,
954        bounds: gpui::geometry::rect::RectF,
955        _: &Self::LayoutState,
956        _: &Self::PaintState,
957        _: &gpui::DebugContext,
958    ) -> gpui::json::Value {
959        json::json!({
960            "type": "AvatarRibbon",
961            "bounds": bounds.to_json(),
962            "color": self.color.to_json(),
963        })
964    }
965}