collab_titlebar_item.rs

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