title_bar.rs

   1mod application_menu;
   2pub mod collab;
   3mod onboarding_banner;
   4mod plan_chip;
   5mod title_bar_settings;
   6mod update_version;
   7
   8use crate::application_menu::{ApplicationMenu, show_menus};
   9use crate::plan_chip::PlanChip;
  10use arrayvec::ArrayVec;
  11use git_ui::worktree_picker::WorktreePicker;
  12pub use platform_title_bar::{
  13    self, DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, PlatformTitleBar,
  14    ShowNextWindowTab, ShowPreviousWindowTab,
  15};
  16use project::linked_worktree_short_name;
  17
  18#[cfg(not(target_os = "macos"))]
  19use crate::application_menu::{
  20    ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu,
  21};
  22
  23use auto_update::AutoUpdateStatus;
  24use call::ActiveCall;
  25use client::{Client, UserStore, zed_urls};
  26use cloud_api_types::Plan;
  27
  28use gpui::{
  29    Action, Anchor, Animation, AnimationExt, AnyElement, App, Context, Element, Entity, Focusable,
  30    InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
  31    StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div,
  32    pulsating_between,
  33};
  34use onboarding_banner::OnboardingBanner;
  35use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees};
  36use remote::RemoteConnectionOptions;
  37use settings::Settings;
  38
  39use std::sync::Arc;
  40use std::time::Duration;
  41use theme::ActiveTheme;
  42use title_bar_settings::TitleBarSettings;
  43use ui::{
  44    Avatar, ButtonLike, ContextMenu, IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle,
  45    TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
  46};
  47use update_version::UpdateVersion;
  48use util::ResultExt;
  49use workspace::{
  50    MultiWorkspace, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt,
  51};
  52
  53use zed_actions::OpenRemote;
  54
  55pub use onboarding_banner::restore_banner;
  56
  57const MAX_PROJECT_NAME_LENGTH: usize = 40;
  58const MAX_BRANCH_NAME_LENGTH: usize = 40;
  59const MAX_SHORT_SHA_LENGTH: usize = 8;
  60
  61actions!(
  62    collab,
  63    [
  64        /// Toggles the user menu dropdown.
  65        ToggleUserMenu,
  66        /// Toggles the project menu dropdown.
  67        ToggleProjectMenu,
  68        /// Switches to a different git branch.
  69        SwitchBranch,
  70        /// A debug action to simulate an update being available to test the update banner UI.
  71        SimulateUpdateAvailable
  72    ]
  73);
  74
  75pub fn init(cx: &mut App) {
  76    platform_title_bar::PlatformTitleBar::init(cx);
  77
  78    cx.observe_new(|workspace: &mut Workspace, window, cx| {
  79        let Some(window) = window else {
  80            return;
  81        };
  82        let multi_workspace = workspace.multi_workspace().cloned();
  83        let item = cx.new(|cx| TitleBar::new("title-bar", workspace, multi_workspace, window, cx));
  84        workspace.set_titlebar_item(item.into(), window, cx);
  85
  86        workspace.register_action(|workspace, _: &SimulateUpdateAvailable, _window, cx| {
  87            if let Some(titlebar) = workspace
  88                .titlebar_item()
  89                .and_then(|item| item.downcast::<TitleBar>().ok())
  90            {
  91                titlebar.update(cx, |titlebar, cx| {
  92                    titlebar.toggle_update_simulation(cx);
  93                });
  94            }
  95        });
  96
  97        #[cfg(not(target_os = "macos"))]
  98        workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
  99            if let Some(titlebar) = workspace
 100                .titlebar_item()
 101                .and_then(|item| item.downcast::<TitleBar>().ok())
 102            {
 103                titlebar.update(cx, |titlebar, cx| {
 104                    if let Some(ref menu) = titlebar.application_menu {
 105                        menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
 106                    }
 107                });
 108            }
 109        });
 110
 111        #[cfg(not(target_os = "macos"))]
 112        workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| {
 113            if let Some(titlebar) = workspace
 114                .titlebar_item()
 115                .and_then(|item| item.downcast::<TitleBar>().ok())
 116            {
 117                titlebar.update(cx, |titlebar, cx| {
 118                    if let Some(ref menu) = titlebar.application_menu {
 119                        menu.update(cx, |menu, cx| {
 120                            menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
 121                        });
 122                    }
 123                });
 124            }
 125        });
 126
 127        #[cfg(not(target_os = "macos"))]
 128        workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
 129            if let Some(titlebar) = workspace
 130                .titlebar_item()
 131                .and_then(|item| item.downcast::<TitleBar>().ok())
 132            {
 133                titlebar.update(cx, |titlebar, cx| {
 134                    if let Some(ref menu) = titlebar.application_menu {
 135                        menu.update(cx, |menu, cx| {
 136                            menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
 137                        });
 138                    }
 139                });
 140            }
 141        });
 142    })
 143    .detach();
 144}
 145
 146pub struct TitleBar {
 147    platform_titlebar: Entity<PlatformTitleBar>,
 148    project: Entity<Project>,
 149    user_store: Entity<UserStore>,
 150    client: Arc<Client>,
 151    workspace: WeakEntity<Workspace>,
 152    multi_workspace: Option<WeakEntity<MultiWorkspace>>,
 153    application_menu: Option<Entity<ApplicationMenu>>,
 154    _subscriptions: Vec<Subscription>,
 155    banner: Option<Entity<OnboardingBanner>>,
 156    update_version: Entity<UpdateVersion>,
 157    screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
 158    _diagnostics_subscription: Option<gpui::Subscription>,
 159}
 160
 161impl Render for TitleBar {
 162    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 163        if self.multi_workspace.is_none() {
 164            if let Some(mw) = self
 165                .workspace
 166                .upgrade()
 167                .and_then(|ws| ws.read(cx).multi_workspace().cloned())
 168            {
 169                self.multi_workspace = Some(mw.clone());
 170                self.platform_titlebar.update(cx, |titlebar, _cx| {
 171                    titlebar.set_multi_workspace(mw);
 172                });
 173            }
 174        }
 175
 176        let title_bar_settings = *TitleBarSettings::get_global(cx);
 177        let button_layout = title_bar_settings.button_layout;
 178
 179        let show_menus = show_menus(cx);
 180
 181        let mut children = <ArrayVec<_, 4>>::new();
 182
 183        let mut project_name = None;
 184        let mut repository = None;
 185        let mut linked_worktree_name = None;
 186        if let Some(worktree) = self.effective_active_worktree(cx) {
 187            repository = self.get_repository_for_worktree(&worktree, cx);
 188            let worktree = worktree.read(cx);
 189            project_name = worktree
 190                .root_name()
 191                .file_name()
 192                .map(|name| SharedString::from(name.to_string()));
 193            if let Some(repo) = &repository {
 194                let repo = repo.read(cx);
 195                linked_worktree_name = linked_worktree_short_name(
 196                    repo.original_repo_abs_path.as_ref(),
 197                    repo.work_directory_abs_path.as_ref(),
 198                );
 199                if let Some(name) = repo
 200                    .original_repo_abs_path
 201                    .file_name()
 202                    .and_then(|name| name.to_str())
 203                {
 204                    project_name = Some(SharedString::from(name.to_string()));
 205                }
 206            }
 207        }
 208
 209        children.push(
 210            h_flex()
 211                .h_full()
 212                .gap_0p5()
 213                .map(|title_bar| {
 214                    let mut render_project_items = title_bar_settings.show_branch_name
 215                        || title_bar_settings.show_project_items;
 216                    title_bar
 217                        .when_some(
 218                            self.application_menu.clone().filter(|_| !show_menus),
 219                            |title_bar, menu| {
 220                                render_project_items &=
 221                                    !menu.update(cx, |menu, cx| menu.all_menus_shown(cx));
 222                                title_bar.child(menu)
 223                            },
 224                        )
 225                        .children(self.render_restricted_mode(cx))
 226                        .when(render_project_items, |title_bar| {
 227                            title_bar
 228                                .when(title_bar_settings.show_project_items, |title_bar| {
 229                                    title_bar
 230                                        .children(self.render_project_host(cx))
 231                                        .child(self.render_project_name(project_name, window, cx))
 232                                })
 233                                .when_some(
 234                                    repository.filter(|_| title_bar_settings.show_branch_name),
 235                                    |title_bar, repository| {
 236                                        title_bar.children(self.render_project_branch(
 237                                            repository,
 238                                            linked_worktree_name,
 239                                            cx,
 240                                        ))
 241                                    },
 242                                )
 243                        })
 244                })
 245                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
 246                .into_any_element(),
 247        );
 248
 249        children.push(self.render_collaborator_list(window, cx).into_any_element());
 250
 251        if title_bar_settings.show_onboarding_banner {
 252            if let Some(banner) = &self.banner {
 253                children.push(banner.clone().into_any_element())
 254            }
 255        }
 256
 257        let status = self.client.status();
 258        let status = &*status.borrow();
 259        let user = self.user_store.read(cx).current_user();
 260
 261        let signed_in = user.is_some();
 262        let is_signing_in = user.is_none()
 263            && matches!(
 264                status,
 265                client::Status::Authenticating
 266                    | client::Status::Authenticated
 267                    | client::Status::Connecting
 268            );
 269        let is_signed_out_or_auth_error = user.is_none()
 270            && matches!(
 271                status,
 272                client::Status::SignedOut | client::Status::AuthenticationError
 273            );
 274
 275        children.push(
 276            h_flex()
 277                .map(|this| {
 278                    if signed_in {
 279                        this.pr_1p5()
 280                    } else {
 281                        this.pr_1()
 282                    }
 283                })
 284                .gap_1()
 285                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
 286                .children(self.render_call_controls(window, cx))
 287                .children(self.render_connection_status(status, cx))
 288                .child(self.update_version.clone())
 289                .when(
 290                    user.is_none()
 291                        && is_signed_out_or_auth_error
 292                        && TitleBarSettings::get_global(cx).show_sign_in,
 293                    |this| this.child(self.render_sign_in_button(cx)),
 294                )
 295                .when(is_signing_in, |this| {
 296                    this.child(
 297                        Label::new("Signing in…")
 298                            .size(LabelSize::Small)
 299                            .color(Color::Muted)
 300                            .with_animation(
 301                                "signing-in",
 302                                Animation::new(Duration::from_secs(2))
 303                                    .repeat()
 304                                    .with_easing(pulsating_between(0.4, 0.8)),
 305                                |label, delta| label.alpha(delta),
 306                            ),
 307                    )
 308                })
 309                .when(TitleBarSettings::get_global(cx).show_user_menu, |this| {
 310                    this.child(self.render_user_menu_button(cx))
 311                })
 312                .into_any_element(),
 313        );
 314
 315        if show_menus {
 316            self.platform_titlebar.update(cx, |this, _| {
 317                this.set_button_layout(button_layout);
 318                this.set_children(
 319                    self.application_menu
 320                        .clone()
 321                        .map(|menu| menu.into_any_element()),
 322                );
 323            });
 324
 325            let height = platform_title_bar_height(window);
 326            let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
 327                platform_titlebar.title_bar_color(window, cx)
 328            });
 329
 330            v_flex()
 331                .w_full()
 332                .child(self.platform_titlebar.clone().into_any_element())
 333                .child(
 334                    h_flex()
 335                        .bg(title_bar_color)
 336                        .h(height)
 337                        .pl_2()
 338                        .justify_between()
 339                        .w_full()
 340                        .children(children),
 341                )
 342                .into_any_element()
 343        } else {
 344            self.platform_titlebar.update(cx, |this, _| {
 345                this.set_button_layout(button_layout);
 346                this.set_children(children);
 347            });
 348            self.platform_titlebar.clone().into_any_element()
 349        }
 350    }
 351}
 352
 353impl TitleBar {
 354    pub fn new(
 355        id: impl Into<ElementId>,
 356        workspace: &Workspace,
 357        multi_workspace: Option<WeakEntity<MultiWorkspace>>,
 358        window: &mut Window,
 359        cx: &mut Context<Self>,
 360    ) -> Self {
 361        let project = workspace.project().clone();
 362        let git_store = project.read(cx).git_store().clone();
 363        let user_store = workspace.app_state().user_store.clone();
 364        let client = workspace.app_state().client.clone();
 365        let active_call = ActiveCall::global(cx);
 366
 367        let platform_style = PlatformStyle::platform();
 368        let application_menu = match platform_style {
 369            PlatformStyle::Mac => {
 370                if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
 371                    Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
 372                } else {
 373                    None
 374                }
 375            }
 376            PlatformStyle::Linux | PlatformStyle::Windows => {
 377                Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
 378            }
 379        };
 380
 381        let mut subscriptions = Vec::new();
 382        subscriptions.push(
 383            cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
 384                cx.notify()
 385            }),
 386        );
 387
 388        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
 389        subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
 390        subscriptions.push(
 391            cx.subscribe(&git_store, move |_, _, event, cx| match event {
 392                GitStoreEvent::ActiveRepositoryChanged(_)
 393                | GitStoreEvent::RepositoryUpdated(_, _, true) => {
 394                    cx.notify();
 395                }
 396                _ => {}
 397            }),
 398        );
 399        subscriptions.push(cx.observe(&user_store, |_a, _, cx| cx.notify()));
 400        if let Some(workspace_entity) = workspace.weak_handle().upgrade() {
 401            subscriptions.push(cx.subscribe(
 402                &workspace_entity,
 403                |_, _, event: &workspace::Event, cx| {
 404                    if matches!(event, workspace::Event::WorktreeCreationChanged) {
 405                        cx.notify();
 406                    }
 407                },
 408            ));
 409        }
 410        subscriptions.push(cx.observe_button_layout_changed(window, |_, _, cx| cx.notify()));
 411        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 412            subscriptions.push(cx.subscribe(&trusted_worktrees, |_, _, _, cx| {
 413                cx.notify();
 414            }));
 415        }
 416
 417        let update_version = cx.new(|cx| UpdateVersion::new(cx));
 418        let platform_titlebar = cx.new(|cx| {
 419            let mut titlebar = PlatformTitleBar::new(id, cx);
 420            if let Some(mw) = multi_workspace.clone() {
 421                titlebar = titlebar.with_multi_workspace(mw);
 422            }
 423            titlebar
 424        });
 425
 426        let mut this = Self {
 427            platform_titlebar,
 428            application_menu,
 429            workspace: workspace.weak_handle(),
 430            multi_workspace,
 431            project,
 432            user_store,
 433            client,
 434            _subscriptions: subscriptions,
 435            banner: None,
 436            update_version,
 437            screen_share_popover_handle: PopoverMenuHandle::default(),
 438            _diagnostics_subscription: None,
 439        };
 440
 441        this.observe_diagnostics(cx);
 442
 443        this
 444    }
 445
 446    fn worktree_count(&self, cx: &App) -> usize {
 447        self.project.read(cx).visible_worktrees(cx).count()
 448    }
 449
 450    fn toggle_update_simulation(&mut self, cx: &mut Context<Self>) {
 451        self.update_version
 452            .update(cx, |banner, cx| banner.update_simulation(cx));
 453        cx.notify();
 454    }
 455
 456    /// Returns the worktree to display in the title bar.
 457    /// - Prefer the worktree owning the project's active repository
 458    /// - Fall back to the first visible worktree
 459    pub fn effective_active_worktree(&self, cx: &App) -> Option<Entity<project::Worktree>> {
 460        let project = self.project.read(cx);
 461
 462        if let Some(repo) = project.active_repository(cx) {
 463            let repo = repo.read(cx);
 464            let repo_path = &repo.work_directory_abs_path;
 465
 466            for worktree in project.visible_worktrees(cx) {
 467                let worktree_path = worktree.read(cx).abs_path();
 468                if worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref()) {
 469                    return Some(worktree);
 470                }
 471            }
 472        }
 473
 474        project.visible_worktrees(cx).next()
 475    }
 476
 477    fn get_repository_for_worktree(
 478        &self,
 479        worktree: &Entity<project::Worktree>,
 480        cx: &App,
 481    ) -> Option<Entity<project::git_store::Repository>> {
 482        let project = self.project.read(cx);
 483        let git_store = project.git_store().read(cx);
 484        let worktree_path = worktree.read(cx).abs_path();
 485
 486        git_store
 487            .repositories()
 488            .values()
 489            .filter(|repo| {
 490                let repo_path = &repo.read(cx).work_directory_abs_path;
 491                worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref())
 492            })
 493            .max_by_key(|repo| repo.read(cx).work_directory_abs_path.as_os_str().len())
 494            .cloned()
 495    }
 496
 497    fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
 498        let workspace = self.workspace.clone();
 499
 500        let options = self.project.read(cx).remote_connection_options(cx)?;
 501        let host: SharedString = options.display_name().into();
 502
 503        let (nickname, tooltip_title, icon) = match options {
 504            RemoteConnectionOptions::Ssh(options) => (
 505                options.nickname.map(|nick| nick.into()),
 506                "Remote Project",
 507                IconName::Server,
 508            ),
 509            RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux),
 510            RemoteConnectionOptions::Docker(_dev_container_connection) => {
 511                (None, "Dev Container", IconName::Box)
 512            }
 513            #[cfg(any(test, feature = "test-support"))]
 514            RemoteConnectionOptions::Mock(_) => (None, "Mock Remote Project", IconName::Server),
 515        };
 516
 517        let nickname = nickname.unwrap_or_else(|| host.clone());
 518
 519        let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
 520            remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
 521            remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
 522            remote::ConnectionState::HeartbeatMissed => (
 523                Color::Warning,
 524                format!("Connection attempt to {host} missed. Retrying..."),
 525            ),
 526            remote::ConnectionState::Reconnecting => (
 527                Color::Warning,
 528                format!("Lost connection to {host}. Reconnecting..."),
 529            ),
 530            remote::ConnectionState::Disconnected => {
 531                (Color::Error, format!("Disconnected from {host}"))
 532            }
 533        };
 534
 535        let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
 536            remote::ConnectionState::Connecting => Color::Info,
 537            remote::ConnectionState::Connected => Color::Default,
 538            remote::ConnectionState::HeartbeatMissed => Color::Warning,
 539            remote::ConnectionState::Reconnecting => Color::Warning,
 540            remote::ConnectionState::Disconnected => Color::Error,
 541        };
 542
 543        let meta = SharedString::from(meta);
 544
 545        Some(
 546            PopoverMenu::new("remote-project-menu")
 547                .menu(move |window, cx| {
 548                    let workspace_entity = workspace.upgrade()?;
 549                    let fs = workspace_entity.read(cx).project().read(cx).fs().clone();
 550                    Some(recent_projects::RemoteServerProjects::popover(
 551                        fs,
 552                        workspace.clone(),
 553                        false,
 554                        window,
 555                        cx,
 556                    ))
 557                })
 558                .trigger_with_tooltip(
 559                    ButtonLike::new("remote_project")
 560                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 561                        .child(
 562                            h_flex()
 563                                .gap_2()
 564                                .max_w_32()
 565                                .child(
 566                                    IconWithIndicator::new(
 567                                        Icon::new(icon).size(IconSize::Small).color(icon_color),
 568                                        Some(Indicator::dot().color(indicator_color)),
 569                                    )
 570                                    .indicator_border_color(Some(
 571                                        cx.theme().colors().title_bar_background,
 572                                    ))
 573                                    .into_any_element(),
 574                                )
 575                                .child(Label::new(nickname).size(LabelSize::Small).truncate()),
 576                        ),
 577                    move |_window, cx| {
 578                        Tooltip::with_meta(
 579                            tooltip_title,
 580                            Some(&OpenRemote {
 581                                from_existing_connection: false,
 582                                create_new_window: false,
 583                            }),
 584                            meta.clone(),
 585                            cx,
 586                        )
 587                    },
 588                )
 589                .anchor(gpui::Anchor::TopLeft)
 590                .into_any_element(),
 591        )
 592    }
 593
 594    pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
 595        let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
 596            .map(|trusted_worktrees| {
 597                trusted_worktrees
 598                    .read(cx)
 599                    .has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx)
 600            })
 601            .unwrap_or(false);
 602        if !has_restricted_worktrees {
 603            return None;
 604        }
 605
 606        let button = Button::new("restricted_mode_trigger", "Restricted Mode")
 607            .style(ButtonStyle::Tinted(TintColor::Warning))
 608            .label_size(LabelSize::Small)
 609            .color(Color::Warning)
 610            .start_icon(
 611                Icon::new(IconName::Warning)
 612                    .size(IconSize::Small)
 613                    .color(Color::Warning),
 614            )
 615            .tooltip(|_, cx| {
 616                Tooltip::with_meta(
 617                    "You're in Restricted Mode",
 618                    Some(&ToggleWorktreeSecurity),
 619                    "Mark this project as trusted and unlock all features",
 620                    cx,
 621                )
 622            })
 623            .on_click({
 624                cx.listener(move |this, _, window, cx| {
 625                    this.workspace
 626                        .update(cx, |workspace, cx| {
 627                            workspace.show_worktree_trust_security_modal(true, window, cx)
 628                        })
 629                        .log_err();
 630                })
 631            });
 632
 633        if cfg!(macos_sdk_26) {
 634            // Make up for Tahoe's traffic light buttons having less spacing around them
 635            Some(div().child(button).ml_0p5().into_any_element())
 636        } else {
 637            Some(button.into_any_element())
 638        }
 639    }
 640
 641    pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
 642        if self.project.read(cx).is_via_remote_server() {
 643            return self.render_remote_project_connection(cx);
 644        }
 645
 646        if self.project.read(cx).is_disconnected(cx) {
 647            return Some(
 648                Button::new("disconnected", "Disconnected")
 649                    .disabled(true)
 650                    .color(Color::Disabled)
 651                    .label_size(LabelSize::Small)
 652                    .into_any_element(),
 653            );
 654        }
 655
 656        let host = self.project.read(cx).host()?;
 657        let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
 658        let participant_index = self
 659            .user_store
 660            .read(cx)
 661            .participant_indices()
 662            .get(&host_user.id)?;
 663
 664        Some(
 665            Button::new("project_owner_trigger", host_user.github_login.clone())
 666                .color(Color::Player(participant_index.0))
 667                .label_size(LabelSize::Small)
 668                .tooltip(move |_, cx| {
 669                    let tooltip_title = format!(
 670                        "{} is sharing this project. Click to follow.",
 671                        host_user.github_login
 672                    );
 673
 674                    Tooltip::with_meta(tooltip_title, None, "Click to Follow", cx)
 675                })
 676                .on_click({
 677                    let host_peer_id = host.peer_id;
 678                    cx.listener(move |this, _, window, cx| {
 679                        this.workspace
 680                            .update(cx, |workspace, cx| {
 681                                workspace.follow(host_peer_id, window, cx);
 682                            })
 683                            .log_err();
 684                    })
 685                })
 686                .into_any_element(),
 687        )
 688    }
 689
 690    fn render_project_name(
 691        &self,
 692        name: Option<SharedString>,
 693        _: &mut Window,
 694        cx: &mut Context<Self>,
 695    ) -> impl IntoElement {
 696        let workspace = self.workspace.clone();
 697
 698        let is_project_selected = name.is_some();
 699
 700        let display_name = if let Some(ref name) = name {
 701            util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
 702        } else {
 703            "Open Recent Project".to_string()
 704        };
 705
 706        let is_sidebar_open = self
 707            .multi_workspace
 708            .as_ref()
 709            .and_then(|mw| mw.upgrade())
 710            .map(|mw| mw.read(cx).sidebar_open())
 711            .unwrap_or(false)
 712            && PlatformTitleBar::is_multi_workspace_enabled(cx);
 713
 714        let is_threads_list_view_active = self
 715            .multi_workspace
 716            .as_ref()
 717            .and_then(|mw| mw.upgrade())
 718            .map(|mw| mw.read(cx).is_threads_list_view_active(cx))
 719            .unwrap_or(false);
 720
 721        if is_sidebar_open && is_threads_list_view_active {
 722            return self
 723                .render_recent_projects_popover(display_name, is_project_selected, cx)
 724                .into_any_element();
 725        }
 726
 727        let focus_handle = workspace
 728            .upgrade()
 729            .map(|w| w.read(cx).focus_handle(cx))
 730            .unwrap_or_else(|| cx.focus_handle());
 731
 732        let window_project_groups: Vec<_> = self
 733            .multi_workspace
 734            .as_ref()
 735            .and_then(|mw| mw.upgrade())
 736            .map(|mw| mw.read(cx).project_group_keys())
 737            .unwrap_or_default();
 738
 739        PopoverMenu::new("recent-projects-menu")
 740            .menu(move |window, cx| {
 741                Some(recent_projects::RecentProjects::popover(
 742                    workspace.clone(),
 743                    window_project_groups.clone(),
 744                    false,
 745                    focus_handle.clone(),
 746                    window,
 747                    cx,
 748                ))
 749            })
 750            .trigger_with_tooltip(
 751                Button::new("project_name_trigger", display_name)
 752                    .label_size(LabelSize::Small)
 753                    .when(self.worktree_count(cx) > 1, |this| {
 754                        this.end_icon(
 755                            Icon::new(IconName::ChevronDown)
 756                                .size(IconSize::XSmall)
 757                                .color(Color::Muted),
 758                        )
 759                    })
 760                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 761                    .when(!is_project_selected, |s| s.color(Color::Muted)),
 762                move |_window, cx| {
 763                    Tooltip::for_action(
 764                        "Recent Projects",
 765                        &zed_actions::OpenRecent {
 766                            create_new_window: false,
 767                        },
 768                        cx,
 769                    )
 770                },
 771            )
 772            .anchor(gpui::Anchor::TopLeft)
 773            .into_any_element()
 774    }
 775
 776    fn render_recent_projects_popover(
 777        &self,
 778        display_name: String,
 779        is_project_selected: bool,
 780        cx: &mut Context<Self>,
 781    ) -> impl IntoElement {
 782        let workspace = self.workspace.clone();
 783
 784        let focus_handle = workspace
 785            .upgrade()
 786            .map(|w| w.read(cx).focus_handle(cx))
 787            .unwrap_or_else(|| cx.focus_handle());
 788
 789        let window_project_groups: Vec<_> = self
 790            .multi_workspace
 791            .as_ref()
 792            .and_then(|mw| mw.upgrade())
 793            .map(|mw| mw.read(cx).project_group_keys())
 794            .unwrap_or_default();
 795
 796        PopoverMenu::new("sidebar-title-recent-projects-menu")
 797            .menu(move |window, cx| {
 798                Some(recent_projects::RecentProjects::popover(
 799                    workspace.clone(),
 800                    window_project_groups.clone(),
 801                    false,
 802                    focus_handle.clone(),
 803                    window,
 804                    cx,
 805                ))
 806            })
 807            .trigger_with_tooltip(
 808                Button::new("project_name_trigger", display_name)
 809                    .label_size(LabelSize::Small)
 810                    .when(self.worktree_count(cx) > 1, |this| {
 811                        this.end_icon(
 812                            Icon::new(IconName::ChevronDown)
 813                                .size(IconSize::XSmall)
 814                                .color(Color::Muted),
 815                        )
 816                    })
 817                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 818                    .when(!is_project_selected, |s| s.color(Color::Muted)),
 819                move |_window, cx| {
 820                    Tooltip::for_action(
 821                        "Recent Projects",
 822                        &zed_actions::OpenRecent {
 823                            create_new_window: false,
 824                        },
 825                        cx,
 826                    )
 827                },
 828            )
 829            .anchor(gpui::Anchor::TopLeft)
 830    }
 831
 832    fn render_project_branch(
 833        &self,
 834        repository: Entity<project::git_store::Repository>,
 835        linked_worktree_name: Option<SharedString>,
 836        cx: &mut Context<Self>,
 837    ) -> Option<AnyElement> {
 838        let workspace = self.workspace.upgrade()?;
 839
 840        let (branch_name, icon_info, is_detached_head) = {
 841            let repo = repository.read(cx);
 842
 843            let is_detached_head = repo.branch.is_none();
 844
 845            let branch_name = repo
 846                .branch
 847                .as_ref()
 848                .map(|branch| branch.name())
 849                .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
 850                .or_else(|| {
 851                    repo.head_commit.as_ref().map(|commit| {
 852                        commit
 853                            .sha
 854                            .chars()
 855                            .take(MAX_SHORT_SHA_LENGTH)
 856                            .collect::<String>()
 857                    })
 858                });
 859
 860            let status = repo.status_summary();
 861            let tracked = status.index + status.worktree;
 862            let icon_info = if status.conflict > 0 {
 863                (IconName::Warning, Color::VersionControlConflict)
 864            } else if tracked.modified > 0 {
 865                (IconName::SquareDot, Color::VersionControlModified)
 866            } else if tracked.added > 0 || status.untracked > 0 {
 867                (IconName::SquarePlus, Color::VersionControlAdded)
 868            } else if tracked.deleted > 0 {
 869                (IconName::SquareMinus, Color::VersionControlDeleted)
 870            } else {
 871                (IconName::GitBranch, Color::Muted)
 872            };
 873
 874            (branch_name, icon_info, is_detached_head)
 875        };
 876
 877        let branch_name = branch_name?;
 878        let settings = TitleBarSettings::get_global(cx);
 879        let effective_repository = Some(repository);
 880
 881        let worktree_label: SharedString = linked_worktree_name.unwrap_or_else(|| "main".into());
 882
 883        let (creation_in_progress, is_switch) = self
 884            .workspace
 885            .upgrade()
 886            .map(|ws| {
 887                let creation = ws.read(cx).active_worktree_creation();
 888                (creation.label.clone(), creation.is_switch)
 889            })
 890            .unwrap_or((None, false));
 891        let is_creating = creation_in_progress.is_some();
 892
 893        let display_label: SharedString = if let Some(ref name) = creation_in_progress {
 894            if is_switch {
 895                format!("Loading {}", name).into()
 896            } else {
 897                format!("Creating {}", name).into()
 898            }
 899        } else {
 900            worktree_label.clone()
 901        };
 902
 903        let worktree_button = {
 904            let project = self.project.clone();
 905            let workspace_handle = workspace.downgrade();
 906            PopoverMenu::new("worktree-picker-menu")
 907                .menu(move |window, cx| {
 908                    // When opened from the title bar, focus is on the trigger
 909                    // button (not a dock), so `focused_dock` is `None`. That's
 910                    // fine — there's no prior dock focus to restore.
 911                    Some(cx.new(|cx| {
 912                        WorktreePicker::new(project.clone(), workspace_handle.clone(), window, cx)
 913                    }))
 914                })
 915                .trigger_with_tooltip(
 916                    Button::new("worktree_picker_trigger", display_label)
 917                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 918                        .label_size(LabelSize::Small)
 919                        .color(Color::Muted)
 920                        .loading(is_creating)
 921                        .start_icon(
 922                            Icon::new(IconName::GitWorktree)
 923                                .size(IconSize::XSmall)
 924                                .color(Color::Muted),
 925                        ),
 926                    move |_window, cx| {
 927                        Tooltip::with_meta(
 928                            "Worktree",
 929                            Some(&zed_actions::git::Worktree),
 930                            format!("Currently In Use: {}", worktree_label),
 931                            cx,
 932                        )
 933                    },
 934                )
 935                .anchor(gpui::Anchor::TopLeft)
 936        };
 937
 938        let branch_tooltip_label = branch_name.clone();
 939        let (branch_icon, branch_icon_color) = if settings.show_branch_status_icon {
 940            icon_info
 941        } else {
 942            (IconName::GitBranch, Color::Muted)
 943        };
 944
 945        let trigger = if is_detached_head {
 946            Button::new("project_branch_trigger", "Create Branch")
 947                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 948                .label_size(LabelSize::Small)
 949                .start_icon(
 950                    Icon::new(IconName::GitBranchPlus)
 951                        .size(IconSize::XSmall)
 952                        .color(Color::Muted),
 953                )
 954        } else {
 955            Button::new("project_branch_trigger", branch_name)
 956                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 957                .label_size(LabelSize::Small)
 958                .color(Color::Muted)
 959                .start_icon(
 960                    Icon::new(branch_icon)
 961                        .size(IconSize::XSmall)
 962                        .color(branch_icon_color),
 963                )
 964        };
 965
 966        let git_picker_button = PopoverMenu::new("branch-menu")
 967            .menu(move |window, cx| {
 968                Some(git_ui::git_picker::popover(
 969                    workspace.downgrade(),
 970                    effective_repository.clone(),
 971                    git_ui::git_picker::GitPickerTab::Branches,
 972                    gpui::rems(34.),
 973                    window,
 974                    cx,
 975                ))
 976            })
 977            .trigger_with_tooltip(trigger, move |_window, cx| {
 978                let meta = if is_detached_head {
 979                    format!("Detached HEAD: {}", branch_tooltip_label)
 980                } else {
 981                    format!("Currently Checked Out: {}", branch_tooltip_label)
 982                };
 983                Tooltip::with_meta("Branch & Stash", Some(&zed_actions::git::Branch), meta, cx)
 984            })
 985            .anchor(gpui::Anchor::TopLeft);
 986
 987        Some(
 988            h_flex()
 989                .gap_px()
 990                .child(worktree_button)
 991                .child(
 992                    Label::new("/")
 993                        .size(LabelSize::Small)
 994                        .color(Color::Muted)
 995                        .alpha(0.25),
 996                )
 997                .child(git_picker_button)
 998                .into_any_element(),
 999        )
1000    }
1001
1002    fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1003        if window.is_window_active() {
1004            ActiveCall::global(cx)
1005                .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
1006                .detach_and_log_err(cx);
1007        } else if cx.active_window().is_none() {
1008            ActiveCall::global(cx)
1009                .update(cx, |call, cx| call.set_location(None, cx))
1010                .detach_and_log_err(cx);
1011        }
1012        self.workspace
1013            .update(cx, |workspace, cx| {
1014                workspace.update_active_view_for_followers(window, cx);
1015            })
1016            .ok();
1017    }
1018
1019    fn active_call_changed(&mut self, cx: &mut Context<Self>) {
1020        self.observe_diagnostics(cx);
1021        cx.notify();
1022    }
1023
1024    fn observe_diagnostics(&mut self, cx: &mut Context<Self>) {
1025        let diagnostics = ActiveCall::global(cx)
1026            .read(cx)
1027            .room()
1028            .and_then(|room| room.read(cx).diagnostics().cloned());
1029
1030        if let Some(diagnostics) = diagnostics {
1031            self._diagnostics_subscription = Some(cx.observe(&diagnostics, |_, _, cx| cx.notify()));
1032        } else {
1033            self._diagnostics_subscription = None;
1034        }
1035    }
1036
1037    fn share_project(&mut self, cx: &mut Context<Self>) {
1038        let active_call = ActiveCall::global(cx);
1039        let project = self.project.clone();
1040        active_call
1041            .update(cx, |call, cx| call.share_project(project, cx))
1042            .detach_and_log_err(cx);
1043    }
1044
1045    fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1046        let active_call = ActiveCall::global(cx);
1047        let project = self.project.clone();
1048        active_call
1049            .update(cx, |call, cx| call.unshare_project(project, cx))
1050            .log_err();
1051    }
1052
1053    fn render_connection_status(
1054        &self,
1055        status: &client::Status,
1056        cx: &mut Context<Self>,
1057    ) -> Option<AnyElement> {
1058        match status {
1059            client::Status::ConnectionError
1060            | client::Status::ConnectionLost
1061            | client::Status::Reauthenticating
1062            | client::Status::Reconnecting
1063            | client::Status::ReconnectionError { .. } => Some(
1064                div()
1065                    .id("disconnected")
1066                    .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
1067                    .tooltip(Tooltip::text("Disconnected"))
1068                    .into_any_element(),
1069            ),
1070            client::Status::UpgradeRequired => {
1071                let auto_updater = auto_update::AutoUpdater::get(cx);
1072                let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
1073                    Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
1074                    Some(AutoUpdateStatus::Installing { .. })
1075                    | Some(AutoUpdateStatus::Downloading { .. })
1076                    | Some(AutoUpdateStatus::Checking) => "Updating...",
1077                    Some(AutoUpdateStatus::Idle)
1078                    | Some(AutoUpdateStatus::Errored { .. })
1079                    | None => "Please update Zed to Collaborate",
1080                };
1081
1082                Some(
1083                    Button::new("connection-status", label)
1084                        .label_size(LabelSize::Small)
1085                        .on_click(|_, window, cx| {
1086                            if let Some(auto_updater) = auto_update::AutoUpdater::get(cx)
1087                                && auto_updater.read(cx).status().is_updated()
1088                            {
1089                                workspace::reload(cx);
1090                                return;
1091                            }
1092                            auto_update::check(&Default::default(), window, cx);
1093                        })
1094                        .into_any_element(),
1095                )
1096            }
1097            _ => None,
1098        }
1099    }
1100
1101    pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
1102        let client = self.client.clone();
1103        let workspace = self.workspace.clone();
1104        Button::new("sign_in", "Sign In")
1105            .label_size(LabelSize::Small)
1106            .on_click(move |_, window, cx| {
1107                let client = client.clone();
1108                let workspace = workspace.clone();
1109                window
1110                    .spawn(cx, async move |mut cx| {
1111                        client
1112                            .sign_in_with_optional_connect(true, cx)
1113                            .await
1114                            .notify_workspace_async_err(workspace, &mut cx);
1115                    })
1116                    .detach();
1117            })
1118    }
1119
1120    pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
1121        let show_update_button = self.update_version.read(cx).show_update_in_menu_bar();
1122
1123        let user_store = self.user_store.clone();
1124        let user_store_read = user_store.read(cx);
1125        let user = user_store_read.current_user();
1126
1127        let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
1128        let user_login = user.as_ref().map(|u| u.github_login.clone());
1129
1130        let is_signed_in = user.is_some();
1131
1132        let has_subscription_period = user_store_read.subscription_period().is_some();
1133        let plan = user_store_read.plan().filter(|_| {
1134            // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
1135            has_subscription_period
1136        });
1137
1138        let has_organization = user_store_read.current_organization().is_some();
1139
1140        let current_organization = user_store_read.current_organization();
1141        let business_organization = current_organization
1142            .as_ref()
1143            .filter(|organization| !organization.is_personal);
1144        let organizations: Vec<_> = user_store_read
1145            .organizations()
1146            .iter()
1147            .map(|org| {
1148                let plan = user_store_read.plan_for_organization(&org.id);
1149                (org.clone(), plan)
1150            })
1151            .collect();
1152
1153        let show_user_picture = TitleBarSettings::get_global(cx).show_user_picture;
1154
1155        let trigger = if is_signed_in && show_user_picture {
1156            let avatar = user_avatar.map(|avatar| Avatar::new(avatar)).map(|avatar| {
1157                if show_update_button {
1158                    avatar.indicator(
1159                        div()
1160                            .absolute()
1161                            .bottom_0()
1162                            .right_0()
1163                            .child(Indicator::dot().color(Color::Accent)),
1164                    )
1165                } else {
1166                    avatar
1167                }
1168            });
1169
1170            ButtonLike::new("user-menu").child(
1171                h_flex()
1172                    .when_some(business_organization, |this, organization| {
1173                        this.gap_2()
1174                            .child(Label::new(&organization.name).size(LabelSize::Small))
1175                    })
1176                    .children(avatar),
1177            )
1178        } else {
1179            ButtonLike::new("user-menu")
1180                .child(Icon::new(IconName::ChevronDown).size(IconSize::Small))
1181        };
1182
1183        PopoverMenu::new("user-menu")
1184            .trigger(trigger)
1185            .menu(move |window, cx| {
1186                let user_login = user_login.clone();
1187                let current_organization = current_organization.clone();
1188                let organizations = organizations.clone();
1189                let user_store = user_store.clone();
1190
1191                ContextMenu::build(window, cx, |menu, _, _cx| {
1192                    menu.when(is_signed_in, |this| {
1193                        let user_login = user_login.clone();
1194                        this.custom_entry(
1195                            move |_window, _cx| {
1196                                let user_login = user_login.clone().unwrap_or_default();
1197
1198                                h_flex()
1199                                    .w_full()
1200                                    .justify_between()
1201                                    .child(Label::new(user_login))
1202                                    .when(!has_organization, |parent| {
1203                                        parent.child(PlanChip::new(plan.unwrap_or(Plan::ZedFree)))
1204                                    })
1205                                    .into_any_element()
1206                            },
1207                            move |_, cx| {
1208                                cx.open_url(&zed_urls::account_url(cx));
1209                            },
1210                        )
1211                        .separator()
1212                    })
1213                    .when(show_update_button, |this| {
1214                        this.custom_entry(
1215                            move |_window, _cx| {
1216                                h_flex()
1217                                    .w_full()
1218                                    .gap_1()
1219                                    .justify_between()
1220                                    .child(Label::new("Restart to update Zed").color(Color::Accent))
1221                                    .child(
1222                                        Icon::new(IconName::Download)
1223                                            .size(IconSize::Small)
1224                                            .color(Color::Accent),
1225                                    )
1226                                    .into_any_element()
1227                            },
1228                            move |_, cx| {
1229                                workspace::reload(cx);
1230                            },
1231                        )
1232                        .separator()
1233                    })
1234                    .when(has_organization, |this| {
1235                        let mut this = this.header("Organization");
1236
1237                        for (organization, plan) in &organizations {
1238                            let organization = organization.clone();
1239                            let plan = *plan;
1240
1241                            let is_current =
1242                                current_organization
1243                                    .as_ref()
1244                                    .is_some_and(|current_organization| {
1245                                        current_organization.id == organization.id
1246                                    });
1247
1248                            this = this.custom_entry(
1249                                {
1250                                    let organization = organization.clone();
1251                                    move |_window, _cx| {
1252                                        h_flex()
1253                                            .w_full()
1254                                            .gap_4()
1255                                            .justify_between()
1256                                            .child(
1257                                                h_flex()
1258                                                    .gap_1()
1259                                                    .child(Label::new(&organization.name))
1260                                                    .when(is_current, |this| {
1261                                                        this.child(
1262                                                            Icon::new(IconName::Check)
1263                                                                .color(Color::Accent),
1264                                                        )
1265                                                    }),
1266                                            )
1267                                            .children(plan.map(|plan| PlanChip::new(plan)))
1268                                            .into_any_element()
1269                                    }
1270                                },
1271                                {
1272                                    let user_store = user_store.clone();
1273                                    let organization = organization.clone();
1274                                    move |_window, cx| {
1275                                        user_store.update(cx, |user_store, cx| {
1276                                            user_store
1277                                                .set_current_organization(organization.clone(), cx);
1278                                        });
1279                                    }
1280                                },
1281                            );
1282                        }
1283
1284                        this.separator()
1285                    })
1286                    .action("Settings", zed_actions::OpenSettings.boxed_clone())
1287                    .action("Keymap", Box::new(zed_actions::OpenKeymap))
1288                    .action(
1289                        "Themes…",
1290                        zed_actions::theme_selector::Toggle::default().boxed_clone(),
1291                    )
1292                    .action(
1293                        "Icon Themes…",
1294                        zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
1295                    )
1296                    .action(
1297                        "Extensions",
1298                        zed_actions::Extensions::default().boxed_clone(),
1299                    )
1300                    .when(is_signed_in, |this| {
1301                        this.separator()
1302                            .action("Sign Out", client::SignOut.boxed_clone())
1303                    })
1304                })
1305                .into()
1306            })
1307            .anchor(Anchor::TopRight)
1308    }
1309}