collab_titlebar_item.rs

   1use crate::{
   2    contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
   3    toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
   4};
   5use call::{ActiveCall, ParticipantLocation, Room};
   6use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
   7use clock::ReplicaId;
   8use context_menu::{ContextMenu, ContextMenuItem};
   9use gpui::{
  10    actions,
  11    color::Color,
  12    elements::*,
  13    geometry::{rect::RectF, vector::vec2f, PathBuilder},
  14    json::{self, ToJson},
  15    platform::{CursorStyle, MouseButton},
  16    AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
  17    WeakViewHandle,
  18};
  19use picker::PickerEvent;
  20use project::{Project, RepositoryEntry};
  21use recent_projects::{build_recent_projects, RecentProjects};
  22use std::{ops::Range, sync::Arc};
  23use theme::{AvatarStyle, Theme};
  24use util::ResultExt;
  25use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
  26use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
  27
  28const MAX_PROJECT_NAME_LENGTH: usize = 40;
  29const MAX_BRANCH_NAME_LENGTH: usize = 40;
  30
  31actions!(
  32    collab,
  33    [
  34        ToggleUserMenu,
  35        ToggleProjectMenu,
  36        SwitchBranch,
  37        ShareProject,
  38        UnshareProject,
  39    ]
  40);
  41
  42pub fn init(cx: &mut AppContext) {
  43    cx.add_action(CollabTitlebarItem::share_project);
  44    cx.add_action(CollabTitlebarItem::unshare_project);
  45    cx.add_action(CollabTitlebarItem::toggle_user_menu);
  46    cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
  47    cx.add_action(CollabTitlebarItem::toggle_project_menu);
  48}
  49
  50pub struct CollabTitlebarItem {
  51    project: ModelHandle<Project>,
  52    user_store: ModelHandle<UserStore>,
  53    client: Arc<Client>,
  54    workspace: WeakViewHandle<Workspace>,
  55    branch_popover: Option<ViewHandle<BranchList>>,
  56    project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
  57    user_menu: ViewHandle<ContextMenu>,
  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 ViewContext<Self>) -> AnyElement<Self> {
  71        let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
  72            workspace
  73        } else {
  74            return Empty::new().into_any();
  75        };
  76
  77        let theme = theme::current(cx).clone();
  78        let mut left_container = Flex::row();
  79        let mut right_container = Flex::row().align_children_center();
  80
  81        left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
  82
  83        let user = self.user_store.read(cx).current_user();
  84        let peer_id = self.client.peer_id();
  85        if let Some(((user, peer_id), room)) = user
  86            .as_ref()
  87            .zip(peer_id)
  88            .zip(ActiveCall::global(cx).read(cx).room().cloned())
  89        {
  90            right_container
  91                .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
  92            right_container.add_child(self.render_leave_call(&theme, cx));
  93            let muted = room.read(cx).is_muted(cx);
  94            let speaking = room.read(cx).is_speaking();
  95            left_container.add_child(
  96                self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
  97            );
  98            left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
  99            right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
 100            right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
 101            right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
 102        }
 103
 104        let status = workspace.read(cx).client().status();
 105        let status = &*status.borrow();
 106        if matches!(status, client::Status::Connected { .. }) {
 107            let avatar = user.as_ref().and_then(|user| user.avatar.clone());
 108            right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
 109        } else {
 110            right_container.add_children(self.render_connection_status(status, cx));
 111            right_container.add_child(self.render_sign_in_button(&theme, cx));
 112            right_container.add_child(self.render_user_menu_button(&theme, None, cx));
 113        }
 114
 115        Stack::new()
 116            .with_child(left_container)
 117            .with_child(
 118                Flex::row()
 119                    .with_child(
 120                        right_container.contained().with_background_color(
 121                            theme
 122                                .titlebar
 123                                .container
 124                                .background_color
 125                                .unwrap_or_else(|| Color::transparent_black()),
 126                        ),
 127                    )
 128                    .aligned()
 129                    .right(),
 130            )
 131            .into_any()
 132    }
 133}
 134
 135impl CollabTitlebarItem {
 136    pub fn new(
 137        workspace: &Workspace,
 138        workspace_handle: &ViewHandle<Workspace>,
 139        cx: &mut ViewContext<Self>,
 140    ) -> Self {
 141        let project = workspace.project().clone();
 142        let user_store = workspace.app_state().user_store.clone();
 143        let client = workspace.app_state().client.clone();
 144        let active_call = ActiveCall::global(cx);
 145        let mut subscriptions = Vec::new();
 146        subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
 147        subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
 148        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
 149        subscriptions.push(cx.observe_window_activation(|this, active, cx| {
 150            this.window_activation_changed(active, cx)
 151        }));
 152        subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
 153        subscriptions.push(
 154            cx.subscribe(&user_store, move |this, user_store, event, cx| {
 155                if let Some(workspace) = this.workspace.upgrade(cx) {
 156                    workspace.update(cx, |workspace, cx| {
 157                        if let client::Event::Contact { user, kind } = event {
 158                            if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
 159                                workspace.show_notification(user.id as usize, cx, |cx| {
 160                                    cx.add_view(|cx| {
 161                                        ContactNotification::new(
 162                                            user.clone(),
 163                                            *kind,
 164                                            user_store,
 165                                            cx,
 166                                        )
 167                                    })
 168                                })
 169                            }
 170                        }
 171                    });
 172                }
 173            }),
 174        );
 175
 176        Self {
 177            workspace: workspace.weak_handle(),
 178            project,
 179            user_store,
 180            client,
 181            user_menu: cx.add_view(|cx| {
 182                let view_id = cx.view_id();
 183                let mut menu = ContextMenu::new(view_id, cx);
 184                menu.set_position_mode(OverlayPositionMode::Local);
 185                menu
 186            }),
 187            branch_popover: None,
 188            project_popover: None,
 189            _subscriptions: subscriptions,
 190        }
 191    }
 192
 193    fn collect_title_root_names(
 194        &self,
 195        theme: Arc<Theme>,
 196        cx: &mut ViewContext<Self>,
 197    ) -> AnyElement<Self> {
 198        let project = self.project.read(cx);
 199
 200        let (name, entry) = {
 201            let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
 202                let worktree = worktree.read(cx);
 203                (worktree.root_name(), worktree.root_git_entry())
 204            });
 205
 206            names_and_branches.next().unwrap_or(("", None))
 207        };
 208
 209        let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
 210        let branch_prepended = entry
 211            .as_ref()
 212            .and_then(RepositoryEntry::branch)
 213            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
 214        let project_style = theme.titlebar.project_menu_button.clone();
 215        let git_style = theme.titlebar.git_menu_button.clone();
 216        let item_spacing = theme.titlebar.item_spacing;
 217
 218        let mut ret = Flex::row().with_child(
 219            Stack::new()
 220                .with_child(
 221                    MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
 222                        let style = project_style
 223                            .in_state(self.project_popover.is_some())
 224                            .style_for(mouse_state);
 225                        enum RecentProjectsTooltip {}
 226                        Label::new(name, style.text.clone())
 227                            .contained()
 228                            .with_style(style.container)
 229                            .aligned()
 230                            .left()
 231                            .with_tooltip::<RecentProjectsTooltip>(
 232                                0,
 233                                "Recent projects",
 234                                Some(Box::new(recent_projects::OpenRecent)),
 235                                theme.tooltip.clone(),
 236                                cx,
 237                            )
 238                            .into_any_named("title-project-name")
 239                    })
 240                    .with_cursor_style(CursorStyle::PointingHand)
 241                    .on_down(MouseButton::Left, move |_, this, cx| {
 242                        this.toggle_project_menu(&Default::default(), cx)
 243                    })
 244                    .on_click(MouseButton::Left, move |_, _, _| {}),
 245                )
 246                .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
 247        );
 248        if let Some(git_branch) = branch_prepended {
 249            ret = ret.with_child(
 250                Flex::row().with_child(
 251                    Stack::new()
 252                        .with_child(
 253                            MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
 254                                enum BranchPopoverTooltip {}
 255                                let style = git_style
 256                                    .in_state(self.branch_popover.is_some())
 257                                    .style_for(mouse_state);
 258                                Label::new(git_branch, style.text.clone())
 259                                    .contained()
 260                                    .with_style(style.container.clone())
 261                                    .with_margin_right(item_spacing)
 262                                    .aligned()
 263                                    .left()
 264                                    .with_tooltip::<BranchPopoverTooltip>(
 265                                        0,
 266                                        "Recent branches",
 267                                        Some(Box::new(ToggleVcsMenu)),
 268                                        theme.tooltip.clone(),
 269                                        cx,
 270                                    )
 271                                    .into_any_named("title-project-branch")
 272                            })
 273                            .with_cursor_style(CursorStyle::PointingHand)
 274                            .on_down(MouseButton::Left, move |_, this, cx| {
 275                                this.toggle_vcs_menu(&Default::default(), cx)
 276                            })
 277                            .on_click(MouseButton::Left, move |_, _, _| {}),
 278                        )
 279                        .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
 280                ),
 281            )
 282        }
 283        ret.into_any()
 284    }
 285
 286    fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
 287        let project = if active {
 288            Some(self.project.clone())
 289        } else {
 290            None
 291        };
 292        ActiveCall::global(cx)
 293            .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
 294            .detach_and_log_err(cx);
 295    }
 296
 297    fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
 298        cx.notify();
 299    }
 300
 301    fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
 302        let active_call = ActiveCall::global(cx);
 303        let project = self.project.clone();
 304        active_call
 305            .update(cx, |call, cx| call.share_project(project, cx))
 306            .detach_and_log_err(cx);
 307    }
 308
 309    fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
 310        let active_call = ActiveCall::global(cx);
 311        let project = self.project.clone();
 312        active_call
 313            .update(cx, |call, cx| call.unshare_project(project, cx))
 314            .log_err();
 315    }
 316
 317    pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
 318        self.user_menu.update(cx, |user_menu, cx| {
 319            let items = if let Some(_) = self.user_store.read(cx).current_user() {
 320                vec![
 321                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
 322                    ContextMenuItem::action("Theme", theme_selector::Toggle),
 323                    ContextMenuItem::separator(),
 324                    ContextMenuItem::action(
 325                        "Share Feedback",
 326                        feedback::feedback_editor::GiveFeedback,
 327                    ),
 328                    ContextMenuItem::action("Sign Out", SignOut),
 329                ]
 330            } else {
 331                vec![
 332                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
 333                    ContextMenuItem::action("Theme", theme_selector::Toggle),
 334                    ContextMenuItem::separator(),
 335                    ContextMenuItem::action(
 336                        "Share Feedback",
 337                        feedback::feedback_editor::GiveFeedback,
 338                    ),
 339                ]
 340            };
 341            user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
 342        });
 343    }
 344
 345    fn render_branches_popover_host<'a>(
 346        &'a self,
 347        _theme: &'a theme::Titlebar,
 348        cx: &'a mut ViewContext<Self>,
 349    ) -> Option<AnyElement<Self>> {
 350        self.branch_popover.as_ref().map(|child| {
 351            let theme = theme::current(cx).clone();
 352            let child = ChildView::new(child, cx);
 353            let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
 354                child
 355                    .flex(1., true)
 356                    .contained()
 357                    .constrained()
 358                    .with_width(theme.titlebar.menu.width)
 359                    .with_height(theme.titlebar.menu.height)
 360            })
 361            .on_click(MouseButton::Left, |_, _, _| {})
 362            .on_down_out(MouseButton::Left, move |_, this, cx| {
 363                this.branch_popover.take();
 364                cx.emit(());
 365                cx.notify();
 366            })
 367            .contained()
 368            .into_any();
 369
 370            Overlay::new(child)
 371                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 372                .with_anchor_corner(AnchorCorner::TopLeft)
 373                .with_z_index(999)
 374                .aligned()
 375                .bottom()
 376                .left()
 377                .into_any()
 378        })
 379    }
 380
 381    fn render_project_popover_host<'a>(
 382        &'a self,
 383        _theme: &'a theme::Titlebar,
 384        cx: &'a mut ViewContext<Self>,
 385    ) -> Option<AnyElement<Self>> {
 386        self.project_popover.as_ref().map(|child| {
 387            let theme = theme::current(cx).clone();
 388            let child = ChildView::new(child, cx);
 389            let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
 390                child
 391                    .flex(1., true)
 392                    .contained()
 393                    .constrained()
 394                    .with_width(theme.titlebar.menu.width)
 395                    .with_height(theme.titlebar.menu.height)
 396            })
 397            .on_click(MouseButton::Left, |_, _, _| {})
 398            .on_down_out(MouseButton::Left, move |_, this, cx| {
 399                this.project_popover.take();
 400                cx.emit(());
 401                cx.notify();
 402            })
 403            .into_any();
 404
 405            Overlay::new(child)
 406                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 407                .with_anchor_corner(AnchorCorner::TopLeft)
 408                .with_z_index(999)
 409                .aligned()
 410                .bottom()
 411                .left()
 412                .into_any()
 413        })
 414    }
 415
 416    pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
 417        if self.branch_popover.take().is_none() {
 418            if let Some(workspace) = self.workspace.upgrade(cx) {
 419                let view = cx.add_view(|cx| build_branch_list(workspace, cx));
 420                cx.subscribe(&view, |this, _, event, cx| {
 421                    match event {
 422                        PickerEvent::Dismiss => {
 423                            this.branch_popover = None;
 424                        }
 425                    }
 426
 427                    cx.notify();
 428                })
 429                .detach();
 430                self.project_popover.take();
 431                cx.focus(&view);
 432                self.branch_popover = Some(view);
 433            }
 434        }
 435
 436        cx.notify();
 437    }
 438
 439    pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
 440        let workspace = self.workspace.clone();
 441        if self.project_popover.take().is_none() {
 442            cx.spawn(|this, mut cx| async move {
 443                let workspaces = WORKSPACE_DB
 444                    .recent_workspaces_on_disk()
 445                    .await
 446                    .unwrap_or_default()
 447                    .into_iter()
 448                    .map(|(_, location)| location)
 449                    .collect();
 450
 451                let workspace = workspace.clone();
 452                this.update(&mut cx, move |this, cx| {
 453                    let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
 454
 455                    cx.subscribe(&view, |this, _, event, cx| {
 456                        match event {
 457                            PickerEvent::Dismiss => {
 458                                this.project_popover = None;
 459                            }
 460                        }
 461
 462                        cx.notify();
 463                    })
 464                    .detach();
 465                    cx.focus(&view);
 466                    this.branch_popover.take();
 467                    this.project_popover = Some(view);
 468                    cx.notify();
 469                })
 470                .log_err();
 471            })
 472            .detach();
 473        }
 474        cx.notify();
 475    }
 476
 477    fn render_toggle_screen_sharing_button(
 478        &self,
 479        theme: &Theme,
 480        room: &ModelHandle<Room>,
 481        cx: &mut ViewContext<Self>,
 482    ) -> AnyElement<Self> {
 483        let icon;
 484        let tooltip;
 485        if room.read(cx).is_screen_sharing() {
 486            icon = "icons/desktop.svg";
 487            tooltip = "Stop Sharing Screen"
 488        } else {
 489            icon = "icons/desktop.svg";
 490            tooltip = "Share Screen";
 491        }
 492
 493        let active = room.read(cx).is_screen_sharing();
 494        let titlebar = &theme.titlebar;
 495        MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
 496            let style = titlebar
 497                .screen_share_button
 498                .in_state(active)
 499                .style_for(state);
 500
 501            Svg::new(icon)
 502                .with_color(style.color)
 503                .constrained()
 504                .with_width(style.icon_width)
 505                .aligned()
 506                .constrained()
 507                .with_width(style.button_width)
 508                .with_height(style.button_width)
 509                .contained()
 510                .with_style(style.container)
 511        })
 512        .with_cursor_style(CursorStyle::PointingHand)
 513        .on_click(MouseButton::Left, move |_, _, cx| {
 514            toggle_screen_sharing(&Default::default(), cx)
 515        })
 516        .with_tooltip::<ToggleScreenSharing>(
 517            0,
 518            tooltip,
 519            Some(Box::new(ToggleScreenSharing)),
 520            theme.tooltip.clone(),
 521            cx,
 522        )
 523        .aligned()
 524        .into_any()
 525    }
 526    fn render_toggle_mute(
 527        &self,
 528        theme: &Theme,
 529        room: &ModelHandle<Room>,
 530        cx: &mut ViewContext<Self>,
 531    ) -> AnyElement<Self> {
 532        let icon;
 533        let tooltip;
 534        let is_muted = room.read(cx).is_muted(cx);
 535        if is_muted {
 536            icon = "icons/mic-mute.svg";
 537            tooltip = "Unmute microphone";
 538        } else {
 539            icon = "icons/mic.svg";
 540            tooltip = "Mute microphone";
 541        }
 542
 543        let titlebar = &theme.titlebar;
 544        MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
 545            let style = titlebar
 546                .toggle_microphone_button
 547                .in_state(is_muted)
 548                .style_for(state);
 549            let image = Svg::new(icon)
 550                .with_color(style.color)
 551                .constrained()
 552                .with_width(style.icon_width)
 553                .aligned()
 554                .constrained()
 555                .with_width(style.button_width)
 556                .with_height(style.button_width)
 557                .contained()
 558                .with_style(style.container);
 559            if let Some(color) = style.container.background_color {
 560                image.with_background_color(color)
 561            } else {
 562                image
 563            }
 564        })
 565        .with_cursor_style(CursorStyle::PointingHand)
 566        .on_click(MouseButton::Left, move |_, _, cx| {
 567            toggle_mute(&Default::default(), cx)
 568        })
 569        .with_tooltip::<ToggleMute>(
 570            0,
 571            tooltip,
 572            Some(Box::new(ToggleMute)),
 573            theme.tooltip.clone(),
 574            cx,
 575        )
 576        .aligned()
 577        .into_any()
 578    }
 579    fn render_toggle_deafen(
 580        &self,
 581        theme: &Theme,
 582        room: &ModelHandle<Room>,
 583        cx: &mut ViewContext<Self>,
 584    ) -> AnyElement<Self> {
 585        let icon;
 586        let tooltip;
 587        let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
 588        if is_deafened {
 589            icon = "icons/speaker-off.svg";
 590            tooltip = "Unmute speakers";
 591        } else {
 592            icon = "icons/speaker-loud.svg";
 593            tooltip = "Mute speakers";
 594        }
 595
 596        let titlebar = &theme.titlebar;
 597        MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
 598            let style = titlebar
 599                .toggle_speakers_button
 600                .in_state(is_deafened)
 601                .style_for(state);
 602            Svg::new(icon)
 603                .with_color(style.color)
 604                .constrained()
 605                .with_width(style.icon_width)
 606                .aligned()
 607                .constrained()
 608                .with_width(style.button_width)
 609                .with_height(style.button_width)
 610                .contained()
 611                .with_style(style.container)
 612        })
 613        .with_cursor_style(CursorStyle::PointingHand)
 614        .on_click(MouseButton::Left, move |_, _, cx| {
 615            toggle_deafen(&Default::default(), cx)
 616        })
 617        .with_tooltip::<ToggleDeafen>(
 618            0,
 619            tooltip,
 620            Some(Box::new(ToggleDeafen)),
 621            theme.tooltip.clone(),
 622            cx,
 623        )
 624        .aligned()
 625        .into_any()
 626    }
 627    fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 628        let icon = "icons/exit.svg";
 629        let tooltip = "Leave call";
 630
 631        let titlebar = &theme.titlebar;
 632        MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
 633            let style = titlebar.leave_call_button.style_for(state);
 634            Svg::new(icon)
 635                .with_color(style.color)
 636                .constrained()
 637                .with_width(style.icon_width)
 638                .aligned()
 639                .constrained()
 640                .with_width(style.button_width)
 641                .with_height(style.button_width)
 642                .contained()
 643                .with_style(style.container)
 644        })
 645        .with_cursor_style(CursorStyle::PointingHand)
 646        .on_click(MouseButton::Left, move |_, _, cx| {
 647            ActiveCall::global(cx)
 648                .update(cx, |call, cx| call.hang_up(cx))
 649                .detach_and_log_err(cx);
 650        })
 651        .with_tooltip::<LeaveCall>(
 652            0,
 653            tooltip,
 654            Some(Box::new(LeaveCall)),
 655            theme.tooltip.clone(),
 656            cx,
 657        )
 658        .aligned()
 659        .into_any()
 660    }
 661    fn render_in_call_share_unshare_button(
 662        &self,
 663        workspace: &ViewHandle<Workspace>,
 664        theme: &Theme,
 665        cx: &mut ViewContext<Self>,
 666    ) -> Option<AnyElement<Self>> {
 667        let project = workspace.read(cx).project();
 668        if project.read(cx).is_remote() {
 669            return None;
 670        }
 671
 672        let is_shared = project.read(cx).is_shared();
 673        let label = if is_shared { "Stop Sharing" } else { "Share" };
 674        let tooltip = if is_shared {
 675            "Stop sharing project with call participants"
 676        } else {
 677            "Share project with call participants"
 678        };
 679
 680        let titlebar = &theme.titlebar;
 681
 682        enum ShareUnshare {}
 683        Some(
 684            Stack::new()
 685                .with_child(
 686                    MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
 687                        //TODO: Ensure this button has consistent width for both text variations
 688                        let style = titlebar.share_button.inactive_state().style_for(state);
 689                        Label::new(label, style.text.clone())
 690                            .contained()
 691                            .with_style(style.container)
 692                    })
 693                    .with_cursor_style(CursorStyle::PointingHand)
 694                    .on_click(MouseButton::Left, move |_, this, cx| {
 695                        if is_shared {
 696                            this.unshare_project(&Default::default(), cx);
 697                        } else {
 698                            this.share_project(&Default::default(), cx);
 699                        }
 700                    })
 701                    .with_tooltip::<ShareUnshare>(
 702                        0,
 703                        tooltip.to_owned(),
 704                        None,
 705                        theme.tooltip.clone(),
 706                        cx,
 707                    ),
 708                )
 709                .aligned()
 710                .contained()
 711                .with_margin_left(theme.titlebar.item_spacing)
 712                .into_any(),
 713        )
 714    }
 715
 716    fn render_user_menu_button(
 717        &self,
 718        theme: &Theme,
 719        avatar: Option<Arc<ImageData>>,
 720        cx: &mut ViewContext<Self>,
 721    ) -> AnyElement<Self> {
 722        let tooltip = theme.tooltip.clone();
 723        let user_menu_button_style = if avatar.is_some() {
 724            &theme.titlebar.user_menu.user_menu_button_online
 725        } else {
 726            &theme.titlebar.user_menu.user_menu_button_offline
 727        };
 728
 729        let avatar_style = &user_menu_button_style.avatar;
 730        Stack::new()
 731            .with_child(
 732                MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
 733                    let style = user_menu_button_style
 734                        .user_menu
 735                        .inactive_state()
 736                        .style_for(state);
 737
 738                    let mut dropdown = Flex::row().align_children_center();
 739
 740                    if let Some(avatar_img) = avatar {
 741                        dropdown = dropdown.with_child(Self::render_face(
 742                            avatar_img,
 743                            *avatar_style,
 744                            Color::transparent_black(),
 745                            None,
 746                        ));
 747                    };
 748
 749                    dropdown
 750                        .with_child(
 751                            Svg::new("icons/caret_down.svg")
 752                                .with_color(user_menu_button_style.icon.color)
 753                                .constrained()
 754                                .with_width(user_menu_button_style.icon.width)
 755                                .contained()
 756                                .into_any(),
 757                        )
 758                        .aligned()
 759                        .constrained()
 760                        .with_height(style.width)
 761                        .contained()
 762                        .with_style(style.container)
 763                        .into_any()
 764                })
 765                .with_cursor_style(CursorStyle::PointingHand)
 766                .on_down(MouseButton::Left, move |_, this, cx| {
 767                    this.user_menu.update(cx, |menu, _| menu.delay_cancel());
 768                })
 769                .on_click(MouseButton::Left, move |_, this, cx| {
 770                    this.toggle_user_menu(&Default::default(), cx)
 771                })
 772                .with_tooltip::<ToggleUserMenu>(
 773                    0,
 774                    "Toggle User Menu".to_owned(),
 775                    Some(Box::new(ToggleUserMenu)),
 776                    tooltip,
 777                    cx,
 778                )
 779                .contained(),
 780            )
 781            .with_child(
 782                ChildView::new(&self.user_menu, cx)
 783                    .aligned()
 784                    .bottom()
 785                    .right(),
 786            )
 787            .into_any()
 788    }
 789
 790    fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 791        let titlebar = &theme.titlebar;
 792        MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
 793            let style = titlebar.sign_in_button.inactive_state().style_for(state);
 794            Label::new("Sign In", style.text.clone())
 795                .contained()
 796                .with_style(style.container)
 797        })
 798        .with_cursor_style(CursorStyle::PointingHand)
 799        .on_click(MouseButton::Left, move |_, this, cx| {
 800            let client = this.client.clone();
 801            cx.app_context()
 802                .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
 803                .detach_and_log_err(cx);
 804        })
 805        .into_any()
 806    }
 807
 808    fn render_collaborators(
 809        &self,
 810        workspace: &ViewHandle<Workspace>,
 811        theme: &Theme,
 812        room: &ModelHandle<Room>,
 813        cx: &mut ViewContext<Self>,
 814    ) -> Vec<Container<Self>> {
 815        let mut participants = room
 816            .read(cx)
 817            .remote_participants()
 818            .values()
 819            .cloned()
 820            .collect::<Vec<_>>();
 821        participants.sort_by_cached_key(|p| p.user.github_login.clone());
 822
 823        participants
 824            .into_iter()
 825            .filter_map(|participant| {
 826                let project = workspace.read(cx).project().read(cx);
 827                let replica_id = project
 828                    .collaborators()
 829                    .get(&participant.peer_id)
 830                    .map(|collaborator| collaborator.replica_id);
 831                let user = participant.user.clone();
 832                Some(
 833                    Container::new(self.render_face_pile(
 834                        &user,
 835                        replica_id,
 836                        participant.peer_id,
 837                        Some(participant.location),
 838                        participant.muted,
 839                        participant.speaking,
 840                        workspace,
 841                        theme,
 842                        cx,
 843                    ))
 844                    .with_margin_right(theme.titlebar.face_pile_spacing),
 845                )
 846            })
 847            .collect()
 848    }
 849
 850    fn render_current_user(
 851        &self,
 852        workspace: &ViewHandle<Workspace>,
 853        theme: &Theme,
 854        user: &Arc<User>,
 855        peer_id: PeerId,
 856        muted: bool,
 857        speaking: bool,
 858        cx: &mut ViewContext<Self>,
 859    ) -> AnyElement<Self> {
 860        let replica_id = workspace.read(cx).project().read(cx).replica_id();
 861
 862        Container::new(self.render_face_pile(
 863            user,
 864            Some(replica_id),
 865            peer_id,
 866            None,
 867            muted,
 868            speaking,
 869            workspace,
 870            theme,
 871            cx,
 872        ))
 873        .with_margin_right(theme.titlebar.item_spacing)
 874        .into_any()
 875    }
 876
 877    fn render_face_pile(
 878        &self,
 879        user: &User,
 880        replica_id: Option<ReplicaId>,
 881        peer_id: PeerId,
 882        location: Option<ParticipantLocation>,
 883        muted: bool,
 884        speaking: bool,
 885        workspace: &ViewHandle<Workspace>,
 886        theme: &Theme,
 887        cx: &mut ViewContext<Self>,
 888    ) -> AnyElement<Self> {
 889        let project_id = workspace.read(cx).project().read(cx).remote_id();
 890        let room = ActiveCall::global(cx).read(cx).room();
 891        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
 892        let followed_by_self = room
 893            .and_then(|room| {
 894                Some(
 895                    is_being_followed
 896                        && room
 897                            .read(cx)
 898                            .followers_for(peer_id, project_id?)
 899                            .iter()
 900                            .any(|&follower| {
 901                                Some(follower) == workspace.read(cx).client().peer_id()
 902                            }),
 903                )
 904            })
 905            .unwrap_or(false);
 906
 907        let leader_style = theme.titlebar.leader_avatar;
 908        let follower_style = theme.titlebar.follower_avatar;
 909
 910        let microphone_state = if muted {
 911            Some(theme.titlebar.muted)
 912        } else if speaking {
 913            Some(theme.titlebar.speaking)
 914        } else {
 915            None
 916        };
 917
 918        let mut background_color = theme
 919            .titlebar
 920            .container
 921            .background_color
 922            .unwrap_or_default();
 923
 924        if let Some(replica_id) = replica_id {
 925            if followed_by_self {
 926                let selection = theme.editor.replica_selection_style(replica_id).selection;
 927                background_color = Color::blend(selection, background_color);
 928                background_color.a = 255;
 929            }
 930        }
 931
 932        let mut content = Stack::new()
 933            .with_children(user.avatar.as_ref().map(|avatar| {
 934                let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
 935                    .with_child(Self::render_face(
 936                        avatar.clone(),
 937                        Self::location_style(workspace, location, leader_style, cx),
 938                        background_color,
 939                        microphone_state,
 940                    ))
 941                    .with_children(
 942                        (|| {
 943                            let project_id = project_id?;
 944                            let room = room?.read(cx);
 945                            let followers = room.followers_for(peer_id, project_id);
 946
 947                            Some(followers.into_iter().flat_map(|&follower| {
 948                                let remote_participant =
 949                                    room.remote_participant_for_peer_id(follower);
 950
 951                                let avatar = remote_participant
 952                                    .and_then(|p| p.user.avatar.clone())
 953                                    .or_else(|| {
 954                                        if follower == workspace.read(cx).client().peer_id()? {
 955                                            workspace
 956                                                .read(cx)
 957                                                .user_store()
 958                                                .read(cx)
 959                                                .current_user()?
 960                                                .avatar
 961                                                .clone()
 962                                        } else {
 963                                            None
 964                                        }
 965                                    })?;
 966
 967                                Some(Self::render_face(
 968                                    avatar.clone(),
 969                                    follower_style,
 970                                    background_color,
 971                                    None,
 972                                ))
 973                            }))
 974                        })()
 975                        .into_iter()
 976                        .flatten(),
 977                    );
 978
 979                let mut container = face_pile
 980                    .contained()
 981                    .with_style(theme.titlebar.leader_selection);
 982
 983                if let Some(replica_id) = replica_id {
 984                    if followed_by_self {
 985                        let color = theme.editor.replica_selection_style(replica_id).selection;
 986                        container = container.with_background_color(color);
 987                    }
 988                }
 989
 990                container
 991            }))
 992            .with_children((|| {
 993                let replica_id = replica_id?;
 994                let color = theme.editor.replica_selection_style(replica_id).cursor;
 995                Some(
 996                    AvatarRibbon::new(color)
 997                        .constrained()
 998                        .with_width(theme.titlebar.avatar_ribbon.width)
 999                        .with_height(theme.titlebar.avatar_ribbon.height)
1000                        .aligned()
1001                        .bottom(),
1002                )
1003            })())
1004            .into_any();
1005
1006        if let Some(location) = location {
1007            if let Some(replica_id) = replica_id {
1008                enum ToggleFollow {}
1009
1010                content = MouseEventHandler::new::<ToggleFollow, _>(
1011                    replica_id.into(),
1012                    cx,
1013                    move |_, _| content,
1014                )
1015                .with_cursor_style(CursorStyle::PointingHand)
1016                .on_click(MouseButton::Left, move |_, item, cx| {
1017                    if let Some(workspace) = item.workspace.upgrade(cx) {
1018                        if let Some(task) = workspace
1019                            .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
1020                        {
1021                            task.detach_and_log_err(cx);
1022                        }
1023                    }
1024                })
1025                .with_tooltip::<ToggleFollow>(
1026                    peer_id.as_u64() as usize,
1027                    if is_being_followed {
1028                        format!("Unfollow {}", user.github_login)
1029                    } else {
1030                        format!("Follow {}", user.github_login)
1031                    },
1032                    Some(Box::new(FollowNextCollaborator)),
1033                    theme.tooltip.clone(),
1034                    cx,
1035                )
1036                .into_any();
1037            } else if let ParticipantLocation::SharedProject { project_id } = location {
1038                enum JoinProject {}
1039
1040                let user_id = user.id;
1041                content = MouseEventHandler::new::<JoinProject, _>(
1042                    peer_id.as_u64() as usize,
1043                    cx,
1044                    move |_, _| content,
1045                )
1046                .with_cursor_style(CursorStyle::PointingHand)
1047                .on_click(MouseButton::Left, move |_, this, cx| {
1048                    if let Some(workspace) = this.workspace.upgrade(cx) {
1049                        let app_state = workspace.read(cx).app_state().clone();
1050                        workspace::join_remote_project(project_id, user_id, app_state, cx)
1051                            .detach_and_log_err(cx);
1052                    }
1053                })
1054                .with_tooltip::<JoinProject>(
1055                    peer_id.as_u64() as usize,
1056                    format!("Follow {} into external project", user.github_login),
1057                    Some(Box::new(FollowNextCollaborator)),
1058                    theme.tooltip.clone(),
1059                    cx,
1060                )
1061                .into_any();
1062            }
1063        }
1064        content
1065    }
1066
1067    fn location_style(
1068        workspace: &ViewHandle<Workspace>,
1069        location: Option<ParticipantLocation>,
1070        mut style: AvatarStyle,
1071        cx: &ViewContext<Self>,
1072    ) -> AvatarStyle {
1073        if let Some(location) = location {
1074            if let ParticipantLocation::SharedProject { project_id } = location {
1075                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
1076                    style.image.grayscale = true;
1077                }
1078            } else {
1079                style.image.grayscale = true;
1080            }
1081        }
1082
1083        style
1084    }
1085
1086    fn render_face<V: 'static>(
1087        avatar: Arc<ImageData>,
1088        avatar_style: AvatarStyle,
1089        background_color: Color,
1090        microphone_state: Option<Color>,
1091    ) -> AnyElement<V> {
1092        Image::from_data(avatar)
1093            .with_style(avatar_style.image)
1094            .aligned()
1095            .contained()
1096            .with_background_color(microphone_state.unwrap_or(background_color))
1097            .with_corner_radius(avatar_style.outer_corner_radius)
1098            .constrained()
1099            .with_width(avatar_style.outer_width)
1100            .with_height(avatar_style.outer_width)
1101            .aligned()
1102            .into_any()
1103    }
1104
1105    fn render_connection_status(
1106        &self,
1107        status: &client::Status,
1108        cx: &mut ViewContext<Self>,
1109    ) -> Option<AnyElement<Self>> {
1110        enum ConnectionStatusButton {}
1111
1112        let theme = &theme::current(cx).clone();
1113        match status {
1114            client::Status::ConnectionError
1115            | client::Status::ConnectionLost
1116            | client::Status::Reauthenticating { .. }
1117            | client::Status::Reconnecting { .. }
1118            | client::Status::ReconnectionError { .. } => Some(
1119                Svg::new("icons/disconnected.svg")
1120                    .with_color(theme.titlebar.offline_icon.color)
1121                    .constrained()
1122                    .with_width(theme.titlebar.offline_icon.width)
1123                    .aligned()
1124                    .contained()
1125                    .with_style(theme.titlebar.offline_icon.container)
1126                    .into_any(),
1127            ),
1128            client::Status::UpgradeRequired => Some(
1129                MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
1130                    Label::new(
1131                        "Please update Zed to collaborate",
1132                        theme.titlebar.outdated_warning.text.clone(),
1133                    )
1134                    .contained()
1135                    .with_style(theme.titlebar.outdated_warning.container)
1136                    .aligned()
1137                })
1138                .with_cursor_style(CursorStyle::PointingHand)
1139                .on_click(MouseButton::Left, |_, _, cx| {
1140                    auto_update::check(&Default::default(), cx);
1141                })
1142                .into_any(),
1143            ),
1144            _ => None,
1145        }
1146    }
1147}
1148
1149pub struct AvatarRibbon {
1150    color: Color,
1151}
1152
1153impl AvatarRibbon {
1154    pub fn new(color: Color) -> AvatarRibbon {
1155        AvatarRibbon { color }
1156    }
1157}
1158
1159impl Element<CollabTitlebarItem> for AvatarRibbon {
1160    type LayoutState = ();
1161
1162    type PaintState = ();
1163
1164    fn layout(
1165        &mut self,
1166        constraint: gpui::SizeConstraint,
1167        _: &mut CollabTitlebarItem,
1168        _: &mut ViewContext<CollabTitlebarItem>,
1169    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
1170        (constraint.max, ())
1171    }
1172
1173    fn paint(
1174        &mut self,
1175        bounds: RectF,
1176        _: RectF,
1177        _: &mut Self::LayoutState,
1178        _: &mut CollabTitlebarItem,
1179        cx: &mut ViewContext<CollabTitlebarItem>,
1180    ) -> Self::PaintState {
1181        let mut path = PathBuilder::new();
1182        path.reset(bounds.lower_left());
1183        path.curve_to(
1184            bounds.origin() + vec2f(bounds.height(), 0.),
1185            bounds.origin(),
1186        );
1187        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
1188        path.curve_to(bounds.lower_right(), bounds.upper_right());
1189        path.line_to(bounds.lower_left());
1190        cx.scene().push_path(path.build(self.color, None));
1191    }
1192
1193    fn rect_for_text_range(
1194        &self,
1195        _: Range<usize>,
1196        _: RectF,
1197        _: RectF,
1198        _: &Self::LayoutState,
1199        _: &Self::PaintState,
1200        _: &CollabTitlebarItem,
1201        _: &ViewContext<CollabTitlebarItem>,
1202    ) -> Option<RectF> {
1203        None
1204    }
1205
1206    fn debug(
1207        &self,
1208        bounds: RectF,
1209        _: &Self::LayoutState,
1210        _: &Self::PaintState,
1211        _: &CollabTitlebarItem,
1212        _: &ViewContext<CollabTitlebarItem>,
1213    ) -> gpui::json::Value {
1214        json::json!({
1215            "type": "AvatarRibbon",
1216            "bounds": bounds.to_json(),
1217            "color": self.color.to_json(),
1218        })
1219    }
1220}