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