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