collab_titlebar_item.rs

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