title_bar.rs

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