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