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