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