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