title_bar.rs

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