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