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