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