title_bar.rs

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