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