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