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