title_bar.rs

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