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