title_bar.rs

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