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