bevy_ui_debug_overlay/
lib.rs

1use bevy_app::Plugin;
2use bevy_asset::AssetId;
3use bevy_color::Hsla;
4use bevy_ecs::entity::Entity;
5#[cfg(feature = "bevy_reflect")]
6use bevy_ecs::reflect::ReflectResource;
7use bevy_ecs::system::Commands;
8use bevy_ecs::system::Query;
9use bevy_ecs::system::Res;
10use bevy_ecs::system::ResMut;
11use bevy_ecs::system::Resource;
12use bevy_math::Rect;
13use bevy_math::Vec2;
14use bevy_render::sync_world::RenderEntity;
15use bevy_render::sync_world::TemporaryRenderEntity;
16use bevy_render::view::ViewVisibility;
17use bevy_render::Extract;
18use bevy_render::ExtractSchedule;
19use bevy_render::RenderApp;
20use bevy_sprite::BorderRect;
21use bevy_transform::components::GlobalTransform;
22use bevy_ui::CalculatedClip;
23use bevy_ui::ComputedNode;
24use bevy_ui::DefaultUiCamera;
25
26use bevy_ui::ExtractedUiItem;
27use bevy_ui::ExtractedUiNode;
28use bevy_ui::ExtractedUiNodes;
29use bevy_ui::NodeType;
30use bevy_ui::TargetCamera;
31
32/// Configuration for the UI debug overlay
33#[derive(Resource)]
34#[cfg_attr(
35    feature = "bevy_reflect",
36    derive(bevy_reflect::Reflect),
37    reflect(Resource)
38)]
39pub struct UiDebugOverlay {
40    /// Set to true to enable the UI debug overlay
41    pub enabled: bool,
42    /// Width of the overlay's lines in logical pixels
43    pub line_width: f32,
44    /// Show outlines for non-visible UI nodes
45    pub show_hidden: bool,
46    /// Show outlines for clipped sections of UI nodes
47    pub show_clipped: bool,
48}
49
50impl UiDebugOverlay {
51    pub fn toggle(&mut self) {
52        self.enabled = !self.enabled;
53    }
54}
55
56impl Default for UiDebugOverlay {
57    fn default() -> Self {
58        Self {
59            enabled: false,
60            line_width: 1.,
61            show_hidden: false,
62            show_clipped: false,
63        }
64    }
65}
66
67#[allow(clippy::too_many_arguments, clippy::type_complexity)]
68pub fn extract_debug_overlay(
69    mut commands: Commands,
70    debug_overlay: Extract<Res<UiDebugOverlay>>,
71    mut extracted_uinodes: ResMut<ExtractedUiNodes>,
72    default_ui_camera: Extract<DefaultUiCamera>,
73    uinode_query: Extract<
74        Query<(
75            Entity,
76            &ComputedNode,
77            &ViewVisibility,
78            Option<&CalculatedClip>,
79            &GlobalTransform,
80            Option<&TargetCamera>,
81        )>,
82    >,
83    mapping: Extract<Query<RenderEntity>>,
84) {
85    if !debug_overlay.enabled {
86        return;
87    }
88
89    for (entity, uinode, visibility, maybe_clip, transform, camera) in &uinode_query {
90        if !debug_overlay.show_hidden && !visibility.get() {
91            continue;
92        }
93
94        let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
95        else {
96            continue;
97        };
98
99        let Ok(render_camera_entity) = mapping.get(camera_entity) else {
100            continue;
101        };
102
103        // Extract a border box to display an outline for every UI Node in the layout
104        extracted_uinodes.uinodes.insert(
105            commands.spawn(TemporaryRenderEntity).id(),
106            ExtractedUiNode {
107                // Add a large number to the UI node's stack index so that the overlay is always drawn on top
108                stack_index: uinode.stack_index() + u32::MAX / 2,
109                color: Hsla::sequential_dispersed(entity.index()).into(),
110                rect: Rect {
111                    min: Vec2::ZERO,
112                    max: uinode.size(),
113                },
114                clip: maybe_clip
115                    .filter(|_| !debug_overlay.show_clipped)
116                    .map(|clip| clip.clip),
117                image: AssetId::default(),
118                camera_entity: render_camera_entity,
119                item: ExtractedUiItem::Node {
120                    atlas_scaling: None,
121                    transform: transform.compute_matrix(),
122                    flip_x: false,
123                    flip_y: false,
124                    border: BorderRect::square(
125                        debug_overlay.line_width / uinode.inverse_scale_factor(),
126                    ),
127                    border_radius: uinode.border_radius(),
128                    node_type: NodeType::Border,
129                },
130                main_entity: entity.into(),
131            },
132        );
133    }
134}
135
136#[derive(Default)]
137pub struct UiDebugOverlayPlugin(pub UiDebugOverlay);
138
139impl UiDebugOverlayPlugin {
140    /// Start the app with the debug overlay disabled
141    pub fn start_disabled() -> Self {
142        Default::default()
143    }
144
145    /// Start the app with the debug overlay enabled
146    pub fn start_enabled() -> Self {
147        Self(UiDebugOverlay {
148            enabled: true,
149            ..Default::default()
150        })
151    }
152
153    /// Set the debug overlay's line width in logical pixels
154    pub fn with_line_width(mut self, line_width: f32) -> Self {
155        self.0.line_width = line_width;
156        self
157    }
158}
159
160impl Plugin for UiDebugOverlayPlugin {
161    fn build(&self, app: &mut bevy_app::App) {
162        #[cfg(feature = "bevy_reflect")]
163        app.register_type::<UiDebugOverlay>();
164        app.insert_resource(UiDebugOverlay { ..self.0 });
165
166        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
167            render_app.add_systems(ExtractSchedule, extract_debug_overlay);
168        }
169    }
170}