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