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