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