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