collab_titlebar_item.rs

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