collab_titlebar_item.rs

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