collab_titlebar_item.rs

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