title_bar.rs

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