collab_titlebar_item.rs

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