title_bar.rs

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