Path: blob/main/crates/bevy_dev_tools/src/diagnostics_overlay.rs
9328 views
//! Overlay showing diagnostics1//!2//! The window can be created using the [`DiagnosticsOverlay`] component34use alloc::borrow::Cow;5use core::time::Duration;67use bevy_app::prelude::*;8use bevy_color::{palettes, prelude::*};9use bevy_diagnostic::{Diagnostic, DiagnosticPath, DiagnosticsStore, FrameTimeDiagnosticsPlugin};10use bevy_ecs::{prelude::*, relationship::Relationship};11use bevy_pbr::{diagnostic::MaterialAllocatorDiagnosticPlugin, StandardMaterial};12use bevy_picking::prelude::*;13use bevy_render::diagnostic::MeshAllocatorDiagnosticPlugin;14use bevy_text::prelude::*;15use bevy_time::common_conditions::on_timer;16use bevy_ui::prelude::*;1718/// Initial offset from the top left corner of the window19/// for the diagnostics overlay20const INITIAL_OFFSET: Val = Val::Px(32.);21/// Alpha value for [`BackgroundColor`] of the overlay22const BACKGROUND_COLOR_ALPHA: f32 = 0.75;23/// Row and column gap for the diagnostics overlay24const ROW_COLUMN_GAP: Val = Val::Px(4.);25/// Padding for cels of the diagnostics overlay26const DEFAULT_PADDING: UiRect = UiRect::all(Val::Px(4.));27/// Initial Z-index for the [`DiagnosticsOverlayPlane`]28pub const INITIAL_DIAGNOSTICS_OVERLAY_PLANE_Z_INDEX: GlobalZIndex = GlobalZIndex(1_000_000);29/// Alias to shorten the name30type StandardMaterialAllocator = MaterialAllocatorDiagnosticPlugin<StandardMaterial>;3132/// Diagnostics overlay displays on a draggable and collapsible window33/// statistics stored on the [`DiagnosticsStore`]. Spawning an entity34/// with this component will create the window for you. Some presets35/// are also provided.36///37/// ```38/// # use bevy_dev_tools::diagnostics_overlay::{DiagnosticsOverlay, DiagnosticsOverlayItem, DiagnosticsOverlayStatistic};39/// # use bevy_ecs::prelude::{Commands, World};40/// # use bevy_diagnostic::DiagnosticPath;41/// # let mut world = World::new();42/// # let mut commands = world.commands();43/// // Spawning an overlay window from the struct44/// commands.spawn(DiagnosticsOverlay {45/// title: "Fps".into(),46/// diagnostic_overlay_items: vec![DiagnosticPath::new("fps").into()]47/// });48/// // Spawning an overlay window from the `new` method49/// commands.spawn(DiagnosticsOverlay::new(50/// "Fps",51/// vec![DiagnosticPath::new("fps").into()]52/// ));53/// // Spawning an overlay window from the `new` method using a different statistic54/// // and float precision55/// commands.spawn(DiagnosticsOverlay::new(56/// "Fps",57/// vec![DiagnosticsOverlayItem {58/// path: DiagnosticPath::new("fps"),59/// statistic: DiagnosticsOverlayStatistic::Value,60/// precision: 461/// }]62/// ));63/// // Spawning an overlay window from the `fps` preset64/// commands.spawn(DiagnosticsOverlay::fps());65/// ```66///67/// A [`DiagnosticsOverlay`] entity will be managed by [`DiagnosticsOverlayPlugin`],68/// and be added as a child of the [`DiagnosticsOverlayPlane`].69///70/// If any value is showing as `Missing`, means that the [`DiagnosticPath`] is not registered,71/// so make sure that the plugin that writes to it is properly set up.72#[derive(Component)]73pub struct DiagnosticsOverlay {74/// Title that will appear on the overlay window75pub title: Cow<'static, str>,76/// Items that will appear on this overlay window77pub diagnostic_overlay_items: Vec<DiagnosticsOverlayItem>,78}7980impl DiagnosticsOverlay {81/// Creates a new instance of a [`DiagnosticsOverlay`]82pub fn new(83title: impl Into<Cow<'static, str>>,84diagnostic_paths: Vec<DiagnosticsOverlayItem>,85) -> Self {86Self {87title: title.into(),88diagnostic_overlay_items: diagnostic_paths,89}90}9192/// Create a [`DiagnosticsOverlay`] with the diagnostcs from [`FrameTimeDiagnosticsPlugin`]93pub fn fps() -> Self {94Self {95title: Cow::Owned("Fps".to_owned()),96diagnostic_overlay_items: vec![97FrameTimeDiagnosticsPlugin::FPS.into(),98FrameTimeDiagnosticsPlugin::FRAME_TIME.into(),99DiagnosticsOverlayItem {100path: FrameTimeDiagnosticsPlugin::FRAME_COUNT,101statistic: DiagnosticsOverlayStatistic::Smoothed,102precision: 0,103},104],105}106}107108/// Create a [`DiagnosticsOverlay`] with the diagnostics from109/// [`MaterialAllocatorDiagnosticPlugin`] of [`StandardMaterial`] and110/// [`MeshAllocatorDiagnosticPlugin`]111pub fn mesh_and_standard_material() -> Self {112Self {113title: Cow::Owned("Mesh and standard materials".to_owned()),114diagnostic_overlay_items: vec![115DiagnosticsOverlayItem {116path: StandardMaterialAllocator::slabs_diagnostic_path(),117statistic: DiagnosticsOverlayStatistic::Smoothed,118precision: 0,119},120DiagnosticsOverlayItem {121path: StandardMaterialAllocator::slabs_size_diagnostic_path(),122statistic: DiagnosticsOverlayStatistic::Smoothed,123precision: 0,124},125DiagnosticsOverlayItem {126path: StandardMaterialAllocator::allocations_diagnostic_path(),127statistic: DiagnosticsOverlayStatistic::Smoothed,128precision: 0,129},130DiagnosticsOverlayItem {131path: MeshAllocatorDiagnosticPlugin::slabs_diagnostic_path().clone(),132statistic: DiagnosticsOverlayStatistic::Smoothed,133precision: 0,134},135DiagnosticsOverlayItem {136path: MeshAllocatorDiagnosticPlugin::slabs_size_diagnostic_path().clone(),137statistic: DiagnosticsOverlayStatistic::Smoothed,138precision: 0,139},140DiagnosticsOverlayItem {141path: MeshAllocatorDiagnosticPlugin::allocations_diagnostic_path().clone(),142statistic: DiagnosticsOverlayStatistic::Smoothed,143precision: 0,144},145],146}147}148}149150/// Marker for the UI root that will hold all of the [`DiagnosticsOverlay`]151/// entities.152///153/// Initially the [`DiagnosticsOverlayPlane`] will be positioned at the154/// [`GlobalZIndex`] of [`INITIAL_DIAGNOSTICS_OVERLAY_PLANE_Z_INDEX`].155/// You are free to edit the z index of the plane or have your ui hierarchies156/// be relative to it.157#[derive(Component)]158pub struct DiagnosticsOverlayPlane;159160/// An item to be displayed on the overlay.161///162/// Items built using `From<DiagnosticPath>` will use163/// [`DiagnosticsOverlayStatistic::Smoothed`].164pub struct DiagnosticsOverlayItem {165/// The statistic of the diagnostic to display166pub statistic: DiagnosticsOverlayStatistic,167/// The diagnostic to display168pub path: DiagnosticPath,169/// How many decimal places to show, default is 4170pub precision: usize,171}172173impl From<DiagnosticPath> for DiagnosticsOverlayItem {174/// Creates an instance of [`DiagnosticsOverlayItem`]175/// from a [`DiagnosticPath`] using [`DiagnosticsOverlayStatistic::Smoothed`].176fn from(value: DiagnosticPath) -> Self {177Self {178path: value,179statistic: Default::default(),180precision: 4,181}182}183}184185/// The statistic to use when displaying a diagnostic186#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]187pub enum DiagnosticsOverlayStatistic {188/// The most recent value of on the diagnostic store189Value,190/// The average of a window of values in the diagnostic store.191Average,192/// The smoothed average of a window of values in the diagnostic store193/// using the [EMA](https://en.wikipedia.org/wiki/Exponential_smoothing).194#[default]195Smoothed,196}197198impl DiagnosticsOverlayStatistic {199/// Fetch the appropriate statistic from a [`Diagnostic`]200pub fn fetch(&self, diagnostic: &Diagnostic) -> Option<f64> {201match self {202Self::Value => diagnostic.value(),203Self::Average => diagnostic.average(),204Self::Smoothed => diagnostic.smoothed(),205}206}207}208209/// System set for the systems of the [`DiagnosticsOverlayPlugin`]210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemSet)]211pub enum DiagnosticsOverlaySystems {212/// Rebuild the contents of the [`DiagnosticsOverlay`] entities213Rebuild,214}215216/// Plugin that builds a visual overlay to present diagnostics.217///218/// The contents of each [`DiagnosticsOverlay`] are rebuilt ever second.219pub struct DiagnosticsOverlayPlugin;220221impl Plugin for DiagnosticsOverlayPlugin {222fn build(&self, app: &mut App) {223app.configure_sets(Update, DiagnosticsOverlaySystems::Rebuild);224app.add_systems(Startup, build_plane);225app.add_systems(226Update,227rebuild_diagnostics_list228.run_if(on_timer(Duration::from_secs(1)))229.in_set(DiagnosticsOverlaySystems::Rebuild),230);231232app.add_observer(build_overlay);233app.add_observer(drag_by_header);234app.add_observer(collapse_on_click_to_header);235app.add_observer(bring_to_front);236}237}238239/// Builds the Ui plane where the [`DiagnosticsOverlay`] entities240/// will reside.241fn build_plane(mut commands: Commands) {242commands.spawn((243DiagnosticsOverlayPlane,244Node {245width: Val::Percent(100.),246height: Val::Percent(100.),247..Default::default()248},249INITIAL_DIAGNOSTICS_OVERLAY_PLANE_Z_INDEX,250));251}252253/// Header of the overlay254#[derive(Component)]255struct DiagnosticsOverlayHeader;256257/// Section of the overlay that will have the diagnostics258#[derive(Component)]259struct DiagnosticsOverlayContents;260261fn rebuild_diagnostics_list(262mut commands: Commands,263diagnostics_overlays: Query<&DiagnosticsOverlay>,264diagnostics_overlay_contents: Query<(Entity, &ChildOf), With<DiagnosticsOverlayContents>>,265diagnostics_store: Res<DiagnosticsStore>,266) {267for (entity, child_of) in diagnostics_overlay_contents {268commands.entity(entity).despawn_children();269270let Ok(diagnostics_overlay) = diagnostics_overlays.get(child_of.get()) else {271panic!("DiagnosticsOverlayContents has been tempered with. Parent was not a DiagnosticsOverlay.");272};273274for (i, diagnostic_overlay_item) in diagnostics_overlay275.diagnostic_overlay_items276.iter()277.enumerate()278{279let maybe_diagnostic = diagnostics_store.get(&diagnostic_overlay_item.path);280let diagnostic = maybe_diagnostic281.map(|diagnostic| {282format!(283"{}{}",284diagnostic_overlay_item285.statistic286.fetch(diagnostic)287.map(|sample| format!(288"{:.prec$}",289sample,290prec = diagnostic_overlay_item.precision291))292.unwrap_or("No sample".to_owned()),293diagnostic.suffix294)295})296.unwrap_or("Missing".to_owned());297298commands.spawn((299ChildOf(entity),300Node {301grid_row: GridPlacement::start(i as i16 + 1),302grid_column: GridPlacement::start(1),303..Default::default()304},305Pickable::IGNORE,306children![(307Text::new(diagnostic_overlay_item.path.to_string()),308TextFont {309font_size: FontSize::Px(10.),310..Default::default()311},312Pickable::IGNORE,313)],314));315commands.spawn((316ChildOf(entity),317Node {318grid_row: GridPlacement::start(i as i16 + 1),319grid_column: GridPlacement::start(2),320..Default::default()321},322Pickable::IGNORE,323children![(324Text::new(diagnostic),325TextFont {326font_size: FontSize::Px(10.),327..Default::default()328},329Pickable::IGNORE,330)],331));332}333}334}335336fn build_overlay(337event: On<Add, DiagnosticsOverlay>,338mut commands: Commands,339diagnostics_overlays: Query<&DiagnosticsOverlay>,340diagnostics_overlay_plane: Single<Entity, With<DiagnosticsOverlayPlane>>,341) {342let entity = event.entity;343let Ok(diagnostics_overlay) = diagnostics_overlays.get(entity) else {344unreachable!("DiagnosticsOverlay must be available.");345};346347commands.entity(entity).insert((348Node {349position_type: PositionType::Absolute,350top: INITIAL_OFFSET,351left: INITIAL_OFFSET,352flex_direction: FlexDirection::Column,353..Default::default()354},355ChildOf(*diagnostics_overlay_plane),356children![357(358Node {359padding: DEFAULT_PADDING,360border_radius: BorderRadius::bottom(Val::Px(4.)),361..Default::default()362},363DiagnosticsOverlayHeader,364BackgroundColor(365palettes::tailwind::GRAY_900366.with_alpha(BACKGROUND_COLOR_ALPHA)367.into()368),369children![(370Text::new(diagnostics_overlay.title.as_ref()),371TextFont {372font_size: FontSize::Px(12.),373..Default::default()374},375Pickable::IGNORE376)],377),378(379Node {380display: Display::Grid,381row_gap: ROW_COLUMN_GAP,382column_gap: ROW_COLUMN_GAP,383padding: DEFAULT_PADDING,384border_radius: BorderRadius::bottom(Val::Px(4.)),385..Default::default()386},387DiagnosticsOverlayContents,388BackgroundColor(389palettes::tailwind::GRAY_600390.with_alpha(BACKGROUND_COLOR_ALPHA)391.into()392),393)394],395));396}397398fn drag_by_header(399mut event: On<Pointer<Drag>>,400mut diagnostics_overlays: Query<&mut Node, With<DiagnosticsOverlay>>,401diagnostics_overlay_headers: Query<&ChildOf, With<DiagnosticsOverlayHeader>>,402) {403let entity = event.entity;404if let Ok(child_of) = diagnostics_overlay_headers.get(entity) {405event.propagate(false);406let Ok(mut node) = diagnostics_overlays.get_mut(child_of.get()) else {407panic!("DiagnosticsOverlayHeader has been tempered with. Parent was not a DiagnosticsOverlay.");408};409let delta = event.delta;410let Val::Px(top) = &mut node.top else {411panic!(412"DiagnosticsOverlay has been tempered with. Node must have `top` using `Val::Px`."413);414};415*top += delta.y;416let Val::Px(left) = &mut node.left else {417panic!(418"DiagnosticsOverlay has been tempered with. Node must have `left` using `Val::Px`."419);420};421*left += delta.x;422}423}424425fn collapse_on_click_to_header(426mut event: On<Pointer<Click>>,427mut diagnostics_overlays: Query<&Children, With<DiagnosticsOverlay>>,428mut diagnostics_overlay_contents: Query<&mut Node, With<DiagnosticsOverlayContents>>,429diagnostics_overlay_header: Query<&ChildOf, With<DiagnosticsOverlayHeader>>,430) {431if event.duration > Duration::from_millis(250) {432return;433}434435let entity = event.entity;436if let Ok(child_of) = diagnostics_overlay_header.get(entity) {437event.propagate(false);438439let Ok(children) = diagnostics_overlays.get_mut(child_of.get()) else {440unreachable!("DiagnosticsOverlay has been tempered with. Do not despawn its children.");441};442let mut lists_iter = diagnostics_overlay_contents.iter_many_mut(children.collection());443444let Some(mut node) = lists_iter.fetch_next() else {445panic!(446"DiagnosticsOverlay has been tempered with. DiagnosticsOverlay must\447have a child with DiagnosticsList."448);449};450451let next_display_mode = match node.display {452Display::Grid => Display::None,453Display::None => Display::Grid,454_ => panic!(455"The DiagnosticsList has be tempered with. Valid Displays for a\456DiagnosticsList are Grid or None."457),458};459node.display = next_display_mode;460461if lists_iter.fetch_next().is_some() {462panic!(463"DiagnosticsOverlay has been tempered with. DiagnosticsOverlay must\464only ever have one single child with DiagnosticsList."465);466}467}468}469470fn bring_to_front(471mut event: On<Pointer<Press>>,472mut commands: Commands,473diagnostics_overlays: Query<(), With<DiagnosticsOverlay>>,474diagnostics_overlay_plane: Single<Entity, With<DiagnosticsOverlayPlane>>,475) {476let entity = event.entity;477if diagnostics_overlays.contains(entity) {478event.propagate(false);479commands480.entity(entity)481.remove::<ChildOf>()482.insert(ChildOf(*diagnostics_overlay_plane));483}484}485486487