collab_titlebar_item.rs

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