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