collab_titlebar_item.rs

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