Draw the UI debug overlay using the UI renderer (#16693)
# Objective Draw the UI debug overlay using the UI renderer. Significantly simpler and easier to use than `bevy_dev_tools::ui_debug_overlay` which uses `bevy_gizmos`. * Supports multiple windows and UI rendered to texture. * Draws rounded debug rects for rounded UI nodes. Fixes #16666 ## Solution Removed the `ui_debug_overlay` module from `bevy_dev_tools`. Added a `bevy_ui_debug` feature gate. Draw the UI debug overlay using the UI renderer. Adds a new module `bevy_ui::render::debug_overlay`. The debug overlay extraction function queries for the existing UI layout and then adds a border around each UI node with `u32::MAX / 2` added to each stack index so it's drawn on top. There is a `UiDebugOptions` resource that can be used to enable or disable the debug overlay and set the line width. ## Testing The `testbed_ui` example has been changed to use the new debug overlay: ``` cargo run --example testbed_ui --features bevy_ui_debug ``` Press Space to toggle the debug overlay on and off. --- ## Showcase <img width="961" alt="testbed-ui-new-debug" src="https://github.com/user-attachments/assets/e9523d18-39ae-46a8-adbe-7d3f3ab8e951"> ## Migration Guide The `ui_debug_overlay` module has been removed from `bevy_dev_tools`. There is a new debug overlay implemented using the `bevy_ui` renderer. To use it, enable the `bevy_ui_debug` feature and set the `enable` field of the `UiDebugOptions` resource to `true`.
This commit is contained in:
parent
c4a24d5b51
commit
9098973fb9
@ -156,6 +156,9 @@ bevy_ui_picking_backend = [
|
|||||||
"bevy_internal/bevy_ui_picking_backend",
|
"bevy_internal/bevy_ui_picking_backend",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Provides a debug overlay for bevy UI
|
||||||
|
bevy_ui_debug = ["bevy_internal/bevy_ui_debug"]
|
||||||
|
|
||||||
# Force dynamic linking, which improves iterative compile times
|
# Force dynamic linking, which improves iterative compile times
|
||||||
dynamic_linking = ["dep:bevy_dylib", "bevy_internal/dynamic_linking"]
|
dynamic_linking = ["dep:bevy_dylib", "bevy_internal/dynamic_linking"]
|
||||||
|
|
||||||
|
@ -9,29 +9,19 @@ license = "MIT OR Apache-2.0"
|
|||||||
keywords = ["bevy"]
|
keywords = ["bevy"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["bevy_ui_debug"]
|
|
||||||
bevy_ci_testing = ["serde", "ron"]
|
bevy_ci_testing = ["serde", "ron"]
|
||||||
bevy_ui_debug = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# bevy
|
# bevy
|
||||||
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
|
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
|
||||||
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
|
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
|
||||||
bevy_color = { path = "../bevy_color", version = "0.15.0-dev" }
|
bevy_color = { path = "../bevy_color", version = "0.15.0-dev" }
|
||||||
bevy_core = { path = "../bevy_core", version = "0.15.0-dev" }
|
|
||||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" }
|
|
||||||
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.15.0-dev" }
|
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.15.0-dev" }
|
||||||
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
|
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
|
||||||
bevy_gizmos = { path = "../bevy_gizmos", version = "0.15.0-dev", features = [
|
|
||||||
"bevy_render",
|
|
||||||
] }
|
|
||||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
|
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
|
||||||
bevy_input = { path = "../bevy_input", version = "0.15.0-dev" }
|
bevy_input = { path = "../bevy_input", version = "0.15.0-dev" }
|
||||||
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
|
|
||||||
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" }
|
|
||||||
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
|
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
|
||||||
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
|
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
|
||||||
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
|
|
||||||
bevy_text = { path = "../bevy_text", version = "0.15.0-dev" }
|
bevy_text = { path = "../bevy_text", version = "0.15.0-dev" }
|
||||||
bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev" }
|
bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev" }
|
||||||
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
||||||
|
@ -15,9 +15,6 @@ pub mod ci_testing;
|
|||||||
|
|
||||||
pub mod fps_overlay;
|
pub mod fps_overlay;
|
||||||
|
|
||||||
#[cfg(feature = "bevy_ui_debug")]
|
|
||||||
pub mod ui_debug_overlay;
|
|
||||||
|
|
||||||
pub mod states;
|
pub mod states;
|
||||||
|
|
||||||
/// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools`
|
/// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools`
|
||||||
|
@ -1,192 +0,0 @@
|
|||||||
use bevy_color::Color;
|
|
||||||
use bevy_gizmos::{config::GizmoConfigGroup, prelude::Gizmos};
|
|
||||||
use bevy_math::{Vec2, Vec2Swizzles};
|
|
||||||
use bevy_reflect::Reflect;
|
|
||||||
use bevy_transform::prelude::GlobalTransform;
|
|
||||||
use bevy_utils::HashMap;
|
|
||||||
|
|
||||||
use super::{CameraQuery, LayoutRect};
|
|
||||||
|
|
||||||
// Function used here so we don't need to redraw lines that are fairly close to each other.
|
|
||||||
fn approx_eq(compared: f32, other: f32) -> bool {
|
|
||||||
(compared - other).abs() < 0.001
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_border_axis(rect: LayoutRect) -> (f32, f32, f32, f32) {
|
|
||||||
let pos = rect.pos;
|
|
||||||
let size = rect.size;
|
|
||||||
let offset = pos + size;
|
|
||||||
(pos.x, offset.x, pos.y, offset.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
|
|
||||||
enum Dir {
|
|
||||||
Start,
|
|
||||||
End,
|
|
||||||
}
|
|
||||||
impl Dir {
|
|
||||||
const fn increments(self) -> i64 {
|
|
||||||
match self {
|
|
||||||
Dir::Start => 1,
|
|
||||||
Dir::End => -1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<i64> for Dir {
|
|
||||||
fn from(value: i64) -> Self {
|
|
||||||
if value.is_positive() {
|
|
||||||
Dir::Start
|
|
||||||
} else {
|
|
||||||
Dir::End
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Collection of axis aligned "lines" (actually just their coordinate on
|
|
||||||
/// a given axis).
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct DrawnLines {
|
|
||||||
lines: HashMap<i64, Dir>,
|
|
||||||
width: f32,
|
|
||||||
}
|
|
||||||
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
|
|
||||||
impl DrawnLines {
|
|
||||||
fn new(width: f32) -> Self {
|
|
||||||
DrawnLines {
|
|
||||||
lines: HashMap::default(),
|
|
||||||
width,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Return `value` offset by as many `increment`s as necessary to make it
|
|
||||||
/// not overlap with already drawn lines.
|
|
||||||
fn inset(&self, value: f32) -> f32 {
|
|
||||||
let scaled = value / self.width;
|
|
||||||
let fract = scaled.fract();
|
|
||||||
let mut on_grid = scaled.floor() as i64;
|
|
||||||
for _ in 0..10 {
|
|
||||||
let Some(dir) = self.lines.get(&on_grid) else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
// TODO(clean): This fixes a panic, but I'm not sure how valid this is
|
|
||||||
let Some(added) = on_grid.checked_add(dir.increments()) else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
on_grid = added;
|
|
||||||
}
|
|
||||||
((on_grid as f32) + fract) * self.width
|
|
||||||
}
|
|
||||||
/// Remove a line from the collection of drawn lines.
|
|
||||||
///
|
|
||||||
/// Typically, we only care for pre-existing lines when drawing the children
|
|
||||||
/// of a container, nothing more. So we remove it after we are done with
|
|
||||||
/// the children.
|
|
||||||
fn remove(&mut self, value: f32, increment: i64) {
|
|
||||||
let mut on_grid = (value / self.width).floor() as i64;
|
|
||||||
loop {
|
|
||||||
// TODO(clean): This fixes a panic, but I'm not sure how valid this is
|
|
||||||
let Some(next_cell) = on_grid.checked_add(increment) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if !self.lines.contains_key(&next_cell) {
|
|
||||||
self.lines.remove(&on_grid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
on_grid = next_cell;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Add a line from the collection of drawn lines.
|
|
||||||
fn add(&mut self, value: f32, increment: i64) {
|
|
||||||
let mut on_grid = (value / self.width).floor() as i64;
|
|
||||||
loop {
|
|
||||||
let old_value = self.lines.insert(on_grid, increment.into());
|
|
||||||
if old_value.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO(clean): This fixes a panic, but I'm not sure how valid this is
|
|
||||||
let Some(added) = on_grid.checked_add(increment) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
on_grid = added;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GizmoConfigGroup, Reflect, Default)]
|
|
||||||
pub struct UiGizmosDebug;
|
|
||||||
|
|
||||||
pub(super) struct InsetGizmo<'w, 's> {
|
|
||||||
draw: Gizmos<'w, 's, UiGizmosDebug>,
|
|
||||||
cam: CameraQuery<'w, 's>,
|
|
||||||
known_y: DrawnLines,
|
|
||||||
known_x: DrawnLines,
|
|
||||||
}
|
|
||||||
impl<'w, 's> InsetGizmo<'w, 's> {
|
|
||||||
pub(super) fn new(
|
|
||||||
draw: Gizmos<'w, 's, UiGizmosDebug>,
|
|
||||||
cam: CameraQuery<'w, 's>,
|
|
||||||
line_width: f32,
|
|
||||||
) -> Self {
|
|
||||||
InsetGizmo {
|
|
||||||
draw,
|
|
||||||
cam,
|
|
||||||
known_y: DrawnLines::new(line_width),
|
|
||||||
known_x: DrawnLines::new(line_width),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn relative(&self, mut position: Vec2) -> Vec2 {
|
|
||||||
let zero = GlobalTransform::IDENTITY;
|
|
||||||
let Ok(cam) = self.cam.get_single() else {
|
|
||||||
return Vec2::ZERO;
|
|
||||||
};
|
|
||||||
if let Ok(new_position) = cam.world_to_viewport(&zero, position.extend(0.)) {
|
|
||||||
position = new_position;
|
|
||||||
};
|
|
||||||
position.xy()
|
|
||||||
}
|
|
||||||
fn line_2d(&mut self, mut start: Vec2, mut end: Vec2, color: Color) {
|
|
||||||
if approx_eq(start.x, end.x) {
|
|
||||||
start.x = self.known_x.inset(start.x);
|
|
||||||
end.x = start.x;
|
|
||||||
} else if approx_eq(start.y, end.y) {
|
|
||||||
start.y = self.known_y.inset(start.y);
|
|
||||||
end.y = start.y;
|
|
||||||
}
|
|
||||||
let (start, end) = (self.relative(start), self.relative(end));
|
|
||||||
self.draw.line_2d(start, end, color);
|
|
||||||
}
|
|
||||||
pub(super) fn set_scope(&mut self, rect: LayoutRect) {
|
|
||||||
let (left, right, top, bottom) = rect_border_axis(rect);
|
|
||||||
self.known_x.add(left, 1);
|
|
||||||
self.known_x.add(right, -1);
|
|
||||||
self.known_y.add(top, 1);
|
|
||||||
self.known_y.add(bottom, -1);
|
|
||||||
}
|
|
||||||
pub(super) fn clear_scope(&mut self, rect: LayoutRect) {
|
|
||||||
let (left, right, top, bottom) = rect_border_axis(rect);
|
|
||||||
self.known_x.remove(left, 1);
|
|
||||||
self.known_x.remove(right, -1);
|
|
||||||
self.known_y.remove(top, 1);
|
|
||||||
self.known_y.remove(bottom, -1);
|
|
||||||
}
|
|
||||||
pub(super) fn rect_2d(&mut self, rect: LayoutRect, color: Color) {
|
|
||||||
let (left, right, top, bottom) = rect_border_axis(rect);
|
|
||||||
if approx_eq(left, right) {
|
|
||||||
self.line_2d(Vec2::new(left, top), Vec2::new(left, bottom), color);
|
|
||||||
} else if approx_eq(top, bottom) {
|
|
||||||
self.line_2d(Vec2::new(left, top), Vec2::new(right, top), color);
|
|
||||||
} else {
|
|
||||||
let inset_x = |v| self.known_x.inset(v);
|
|
||||||
let inset_y = |v| self.known_y.inset(v);
|
|
||||||
let (left, right) = (inset_x(left), inset_x(right));
|
|
||||||
let (top, bottom) = (inset_y(top), inset_y(bottom));
|
|
||||||
let strip = [
|
|
||||||
Vec2::new(left, top),
|
|
||||||
Vec2::new(left, bottom),
|
|
||||||
Vec2::new(right, bottom),
|
|
||||||
Vec2::new(right, top),
|
|
||||||
Vec2::new(left, top),
|
|
||||||
]
|
|
||||||
.map(|v| self.relative(v));
|
|
||||||
self.draw.linestrip_2d(strip, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,278 +0,0 @@
|
|||||||
//! A visual representation of UI node sizes.
|
|
||||||
use core::any::{Any, TypeId};
|
|
||||||
|
|
||||||
use bevy_app::{App, Plugin, PostUpdate};
|
|
||||||
use bevy_color::Hsla;
|
|
||||||
use bevy_core::Name;
|
|
||||||
use bevy_core_pipeline::core_2d::Camera2d;
|
|
||||||
use bevy_ecs::{prelude::*, system::SystemParam};
|
|
||||||
use bevy_gizmos::{config::GizmoConfigStore, prelude::Gizmos, AppGizmoBuilder};
|
|
||||||
use bevy_hierarchy::{Children, Parent};
|
|
||||||
use bevy_math::{Vec2, Vec3Swizzles};
|
|
||||||
use bevy_render::{
|
|
||||||
camera::RenderTarget,
|
|
||||||
prelude::*,
|
|
||||||
view::{RenderLayers, VisibilitySystems},
|
|
||||||
};
|
|
||||||
use bevy_transform::{prelude::GlobalTransform, TransformSystem};
|
|
||||||
use bevy_ui::{ComputedNode, DefaultUiCamera, Display, Node, TargetCamera, UiScale};
|
|
||||||
use bevy_utils::{default, warn_once};
|
|
||||||
use bevy_window::{PrimaryWindow, Window, WindowRef};
|
|
||||||
|
|
||||||
use inset::InsetGizmo;
|
|
||||||
|
|
||||||
use self::inset::UiGizmosDebug;
|
|
||||||
|
|
||||||
mod inset;
|
|
||||||
|
|
||||||
/// The [`Camera::order`] index used by the layout debug camera.
|
|
||||||
pub const LAYOUT_DEBUG_CAMERA_ORDER: isize = 255;
|
|
||||||
/// The [`RenderLayers`] used by the debug gizmos and the debug camera.
|
|
||||||
pub const LAYOUT_DEBUG_LAYERS: RenderLayers = RenderLayers::layer(16);
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct LayoutRect {
|
|
||||||
pos: Vec2,
|
|
||||||
size: Vec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutRect {
|
|
||||||
fn new(trans: &GlobalTransform, node: &ComputedNode, scale: f32) -> Self {
|
|
||||||
let mut this = Self {
|
|
||||||
pos: trans.translation().xy() * scale,
|
|
||||||
size: node.size() * scale,
|
|
||||||
};
|
|
||||||
this.pos -= this.size / 2.;
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component, Debug, Clone, Default)]
|
|
||||||
struct DebugOverlayCamera;
|
|
||||||
|
|
||||||
/// The debug overlay options.
|
|
||||||
#[derive(Resource, Clone, Default)]
|
|
||||||
pub struct UiDebugOptions {
|
|
||||||
/// Whether the overlay is enabled.
|
|
||||||
pub enabled: bool,
|
|
||||||
layout_gizmos_camera: Option<Entity>,
|
|
||||||
}
|
|
||||||
impl UiDebugOptions {
|
|
||||||
/// This will toggle the enabled field, setting it to false if true and true if false.
|
|
||||||
pub fn toggle(&mut self) {
|
|
||||||
self.enabled = !self.enabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The system responsible to change the [`Camera`] config based on changes in [`UiDebugOptions`] and [`GizmoConfig`](bevy_gizmos::prelude::GizmoConfig).
|
|
||||||
fn update_debug_camera(
|
|
||||||
mut gizmo_config: ResMut<GizmoConfigStore>,
|
|
||||||
mut options: ResMut<UiDebugOptions>,
|
|
||||||
mut cmds: Commands,
|
|
||||||
mut debug_cams: Query<&mut Camera, With<DebugOverlayCamera>>,
|
|
||||||
) {
|
|
||||||
if !options.is_changed() && !gizmo_config.is_changed() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !options.enabled {
|
|
||||||
let Some(cam) = options.layout_gizmos_camera else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok(mut cam) = debug_cams.get_mut(cam) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
cam.is_active = false;
|
|
||||||
if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::<UiGizmosDebug>()) {
|
|
||||||
config.enabled = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let spawn_cam = || {
|
|
||||||
cmds.spawn((
|
|
||||||
Camera2d,
|
|
||||||
OrthographicProjection {
|
|
||||||
far: 1000.0,
|
|
||||||
viewport_origin: Vec2::new(0.0, 0.0),
|
|
||||||
..OrthographicProjection::default_3d()
|
|
||||||
},
|
|
||||||
Camera {
|
|
||||||
order: LAYOUT_DEBUG_CAMERA_ORDER,
|
|
||||||
clear_color: ClearColorConfig::None,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
LAYOUT_DEBUG_LAYERS.clone(),
|
|
||||||
DebugOverlayCamera,
|
|
||||||
Name::new("Layout Debug Camera"),
|
|
||||||
))
|
|
||||||
.id()
|
|
||||||
};
|
|
||||||
if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::<UiGizmosDebug>()) {
|
|
||||||
config.enabled = true;
|
|
||||||
config.render_layers = LAYOUT_DEBUG_LAYERS.clone();
|
|
||||||
}
|
|
||||||
let cam = *options.layout_gizmos_camera.get_or_insert_with(spawn_cam);
|
|
||||||
let Ok(mut cam) = debug_cams.get_mut(cam) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
cam.is_active = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The function that goes over every children of given [`Entity`], skipping the not visible ones and drawing the gizmos outlines.
|
|
||||||
fn outline_nodes(outline: &OutlineParam, draw: &mut InsetGizmo, this_entity: Entity, scale: f32) {
|
|
||||||
let Ok(to_iter) = outline.children.get(this_entity) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (entity, trans, node, computed_node, children) in outline.nodes.iter_many(to_iter) {
|
|
||||||
if matches!(node.display, Display::None) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(view_visibility) = outline.view_visibility.get(entity) {
|
|
||||||
if !view_visibility.get() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let rect = LayoutRect::new(trans, computed_node, scale);
|
|
||||||
outline_node(entity, rect, draw);
|
|
||||||
if children.is_some() {
|
|
||||||
outline_nodes(outline, draw, entity, scale);
|
|
||||||
}
|
|
||||||
draw.clear_scope(rect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type NodesQuery = (
|
|
||||||
Entity,
|
|
||||||
&'static GlobalTransform,
|
|
||||||
&'static Node,
|
|
||||||
&'static ComputedNode,
|
|
||||||
Option<&'static Children>,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(SystemParam)]
|
|
||||||
struct OutlineParam<'w, 's> {
|
|
||||||
gizmo_config: Res<'w, GizmoConfigStore>,
|
|
||||||
children: Query<'w, 's, &'static Children>,
|
|
||||||
nodes: Query<'w, 's, NodesQuery>,
|
|
||||||
view_visibility: Query<'w, 's, &'static ViewVisibility>,
|
|
||||||
ui_scale: Res<'w, UiScale>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type CameraQuery<'w, 's> = Query<'w, 's, &'static Camera, With<DebugOverlayCamera>>;
|
|
||||||
|
|
||||||
#[derive(SystemParam)]
|
|
||||||
struct CameraParam<'w, 's> {
|
|
||||||
debug_camera: Query<'w, 's, &'static Camera, With<DebugOverlayCamera>>,
|
|
||||||
cameras: Query<'w, 's, &'static Camera, Without<DebugOverlayCamera>>,
|
|
||||||
primary_window: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
|
|
||||||
default_ui_camera: DefaultUiCamera<'w, 's>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// system responsible for drawing the gizmos lines around all the node roots, iterating recursively through all visible children.
|
|
||||||
fn outline_roots(
|
|
||||||
outline: OutlineParam,
|
|
||||||
draw: Gizmos<UiGizmosDebug>,
|
|
||||||
cam: CameraParam,
|
|
||||||
roots: Query<
|
|
||||||
(
|
|
||||||
Entity,
|
|
||||||
&GlobalTransform,
|
|
||||||
&ComputedNode,
|
|
||||||
Option<&ViewVisibility>,
|
|
||||||
Option<&TargetCamera>,
|
|
||||||
),
|
|
||||||
Without<Parent>,
|
|
||||||
>,
|
|
||||||
window: Query<&Window, With<PrimaryWindow>>,
|
|
||||||
nonprimary_windows: Query<&Window, Without<PrimaryWindow>>,
|
|
||||||
options: Res<UiDebugOptions>,
|
|
||||||
) {
|
|
||||||
if !options.enabled {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !nonprimary_windows.is_empty() {
|
|
||||||
warn_once!(
|
|
||||||
"The layout debug view only uses the primary window scale, \
|
|
||||||
you might notice gaps between container lines"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let window_scale = window.get_single().map_or(1., Window::scale_factor);
|
|
||||||
let scale_factor = outline.ui_scale.0;
|
|
||||||
|
|
||||||
// We let the line be defined by the window scale alone
|
|
||||||
let line_width = outline
|
|
||||||
.gizmo_config
|
|
||||||
.get_config_dyn(&UiGizmosDebug.type_id())
|
|
||||||
.map_or(2., |(config, _)| config.line.width)
|
|
||||||
/ window_scale;
|
|
||||||
let mut draw = InsetGizmo::new(draw, cam.debug_camera, line_width);
|
|
||||||
for (entity, trans, node, view_visibility, maybe_target_camera) in &roots {
|
|
||||||
if let Some(view_visibility) = view_visibility {
|
|
||||||
// If the entity isn't visible, we will not draw any lines.
|
|
||||||
if !view_visibility.get() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We skip ui in other windows that are not the primary one
|
|
||||||
if let Some(camera_entity) = maybe_target_camera
|
|
||||||
.map(|target| target.0)
|
|
||||||
.or(cam.default_ui_camera.get())
|
|
||||||
{
|
|
||||||
let Ok(camera) = cam.cameras.get(camera_entity) else {
|
|
||||||
// The camera wasn't found. Either the Camera don't exist or the Camera is the debug Camera, that we want to skip and warn
|
|
||||||
warn_once!("Camera {:?} wasn't found for debug overlay", camera_entity);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
match camera.target {
|
|
||||||
RenderTarget::Window(window_ref) => {
|
|
||||||
if let WindowRef::Entity(window_entity) = window_ref {
|
|
||||||
if cam.primary_window.get(window_entity).is_err() {
|
|
||||||
// This window isn't the primary, so we skip this root.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Hard to know the results of this, better skip this target.
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rect = LayoutRect::new(trans, node, scale_factor);
|
|
||||||
outline_node(entity, rect, &mut draw);
|
|
||||||
outline_nodes(&outline, &mut draw, entity, scale_factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Function responsible for drawing the gizmos lines around the given Entity
|
|
||||||
fn outline_node(entity: Entity, rect: LayoutRect, draw: &mut InsetGizmo) {
|
|
||||||
let color = Hsla::sequential_dispersed(entity.index());
|
|
||||||
|
|
||||||
draw.rect_2d(rect, color.into());
|
|
||||||
draw.set_scope(rect);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The debug overlay plugin.
|
|
||||||
///
|
|
||||||
/// This spawns a new camera with a low order, and draws gizmo.
|
|
||||||
///
|
|
||||||
/// Note that due to limitation with [`bevy_gizmos`], multiple windows with this feature
|
|
||||||
/// enabled isn't supported and the lines are only drawn in the [`PrimaryWindow`]
|
|
||||||
pub struct DebugUiPlugin;
|
|
||||||
impl Plugin for DebugUiPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.init_resource::<UiDebugOptions>()
|
|
||||||
.init_gizmo_group::<UiGizmosDebug>()
|
|
||||||
.add_systems(
|
|
||||||
PostUpdate,
|
|
||||||
(
|
|
||||||
update_debug_camera,
|
|
||||||
outline_roots
|
|
||||||
.after(TransformSystem::TransformPropagate)
|
|
||||||
// This needs to run before VisibilityPropagate so it can relies on ViewVisibility
|
|
||||||
.before(VisibilitySystems::VisibilityPropagate),
|
|
||||||
)
|
|
||||||
.chain(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -235,6 +235,9 @@ bevy_sprite_picking_backend = [
|
|||||||
# Provides a UI picking backend
|
# Provides a UI picking backend
|
||||||
bevy_ui_picking_backend = ["bevy_picking", "bevy_ui/bevy_ui_picking_backend"]
|
bevy_ui_picking_backend = ["bevy_picking", "bevy_ui/bevy_ui_picking_backend"]
|
||||||
|
|
||||||
|
# Provides a UI debug overlay
|
||||||
|
bevy_ui_debug = ["bevy_ui?/bevy_ui_debug"]
|
||||||
|
|
||||||
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
||||||
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
||||||
|
|
||||||
|
@ -43,8 +43,10 @@ smallvec = "1.11"
|
|||||||
accesskit = "0.17"
|
accesskit = "0.17"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = []
|
||||||
serialize = ["serde", "smallvec/serde", "bevy_math/serialize"]
|
serialize = ["serde", "smallvec/serde", "bevy_math/serialize"]
|
||||||
bevy_ui_picking_backend = ["bevy_picking"]
|
bevy_ui_picking_backend = ["bevy_picking"]
|
||||||
|
bevy_ui_debug = []
|
||||||
|
|
||||||
# Experimental features
|
# Experimental features
|
||||||
ghost_nodes = []
|
ghost_nodes = []
|
||||||
|
@ -40,12 +40,16 @@ pub use measurement::*;
|
|||||||
pub use render::*;
|
pub use render::*;
|
||||||
pub use ui_material::*;
|
pub use ui_material::*;
|
||||||
pub use ui_node::*;
|
pub use ui_node::*;
|
||||||
|
|
||||||
use widget::{ImageNode, ImageNodeSize};
|
use widget::{ImageNode, ImageNodeSize};
|
||||||
|
|
||||||
/// The UI prelude.
|
/// The UI prelude.
|
||||||
///
|
///
|
||||||
/// This includes the most common types in this crate, re-exported for your convenience.
|
/// This includes the most common types in this crate, re-exported for your convenience.
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
|
pub use crate::render::UiDebugOptions;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use crate::widget::TextBundle;
|
pub use crate::widget::TextBundle;
|
||||||
@ -230,6 +234,9 @@ impl Plugin for UiPlugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
|
app.init_resource::<UiDebugOptions>();
|
||||||
|
|
||||||
build_ui_render(app);
|
build_ui_render(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
109
crates/bevy_ui/src/render/debug_overlay.rs
Normal file
109
crates/bevy_ui/src/render/debug_overlay.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
use bevy_asset::AssetId;
|
||||||
|
use bevy_color::Hsla;
|
||||||
|
use bevy_ecs::entity::Entity;
|
||||||
|
use bevy_ecs::system::Commands;
|
||||||
|
use bevy_ecs::system::Query;
|
||||||
|
use bevy_ecs::system::Res;
|
||||||
|
use bevy_ecs::system::ResMut;
|
||||||
|
use bevy_ecs::system::Resource;
|
||||||
|
use bevy_math::Rect;
|
||||||
|
use bevy_math::Vec2;
|
||||||
|
use bevy_render::sync_world::RenderEntity;
|
||||||
|
use bevy_render::sync_world::TemporaryRenderEntity;
|
||||||
|
use bevy_render::Extract;
|
||||||
|
use bevy_sprite::BorderRect;
|
||||||
|
use bevy_transform::components::GlobalTransform;
|
||||||
|
|
||||||
|
use crate::ComputedNode;
|
||||||
|
use crate::DefaultUiCamera;
|
||||||
|
use crate::TargetCamera;
|
||||||
|
|
||||||
|
use super::ExtractedUiItem;
|
||||||
|
use super::ExtractedUiNode;
|
||||||
|
use super::ExtractedUiNodes;
|
||||||
|
use super::NodeType;
|
||||||
|
|
||||||
|
/// Configuration for the UI debug overlay
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct UiDebugOptions {
|
||||||
|
/// Set to true to enable the UI debug overlay
|
||||||
|
pub enabled: bool,
|
||||||
|
/// Width of the overlay's lines in logical pixels
|
||||||
|
pub line_width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiDebugOptions {
|
||||||
|
pub fn toggle(&mut self) {
|
||||||
|
self.enabled = !self.enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UiDebugOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
line_width: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn extract_debug_overlay(
|
||||||
|
mut commands: Commands,
|
||||||
|
debug_options: Extract<Res<UiDebugOptions>>,
|
||||||
|
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||||
|
default_ui_camera: Extract<DefaultUiCamera>,
|
||||||
|
uinode_query: Extract<
|
||||||
|
Query<(
|
||||||
|
Entity,
|
||||||
|
&ComputedNode,
|
||||||
|
&GlobalTransform,
|
||||||
|
Option<&TargetCamera>,
|
||||||
|
)>,
|
||||||
|
>,
|
||||||
|
mapping: Extract<Query<RenderEntity>>,
|
||||||
|
) {
|
||||||
|
if !debug_options.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, uinode, transform, camera) in &uinode_query {
|
||||||
|
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(render_camera_entity) = mapping.get(camera_entity) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract a border box to display an outline for every UI Node in the layout
|
||||||
|
extracted_uinodes.uinodes.insert(
|
||||||
|
commands.spawn(TemporaryRenderEntity).id(),
|
||||||
|
ExtractedUiNode {
|
||||||
|
// Add a large number to the UI node's stack index so that the overlay is always drawn on top
|
||||||
|
stack_index: uinode.stack_index + u32::MAX / 2,
|
||||||
|
color: Hsla::sequential_dispersed(entity.index()).into(),
|
||||||
|
rect: Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: uinode.size,
|
||||||
|
},
|
||||||
|
clip: None,
|
||||||
|
image: AssetId::default(),
|
||||||
|
camera_entity: render_camera_entity,
|
||||||
|
item: ExtractedUiItem::Node {
|
||||||
|
atlas_scaling: None,
|
||||||
|
transform: transform.compute_matrix(),
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
border: BorderRect::square(
|
||||||
|
debug_options.line_width / uinode.inverse_scale_factor(),
|
||||||
|
),
|
||||||
|
border_radius: uinode.border_radius(),
|
||||||
|
node_type: NodeType::Border,
|
||||||
|
},
|
||||||
|
main_entity: entity.into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,9 @@ mod render_pass;
|
|||||||
mod ui_material_pipeline;
|
mod ui_material_pipeline;
|
||||||
pub mod ui_texture_slice_pipeline;
|
pub mod ui_texture_slice_pipeline;
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
|
mod debug_overlay;
|
||||||
|
|
||||||
use crate::widget::ImageNode;
|
use crate::widget::ImageNode;
|
||||||
use crate::{
|
use crate::{
|
||||||
experimental::UiChildren, BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip,
|
experimental::UiChildren, BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip,
|
||||||
@ -41,6 +44,8 @@ use bevy_render::{
|
|||||||
};
|
};
|
||||||
use bevy_sprite::TextureAtlasLayout;
|
use bevy_sprite::TextureAtlasLayout;
|
||||||
use bevy_sprite::{BorderRect, SpriteAssetEvents};
|
use bevy_sprite::{BorderRect, SpriteAssetEvents};
|
||||||
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
|
pub use debug_overlay::UiDebugOptions;
|
||||||
|
|
||||||
use crate::{Display, Node};
|
use crate::{Display, Node};
|
||||||
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo};
|
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo};
|
||||||
@ -98,6 +103,7 @@ pub enum RenderUiSystem {
|
|||||||
ExtractTextureSlice,
|
ExtractTextureSlice,
|
||||||
ExtractBorders,
|
ExtractBorders,
|
||||||
ExtractText,
|
ExtractText,
|
||||||
|
ExtractDebug,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_ui_render(app: &mut App) {
|
pub fn build_ui_render(app: &mut App) {
|
||||||
@ -125,6 +131,7 @@ pub fn build_ui_render(app: &mut App) {
|
|||||||
RenderUiSystem::ExtractTextureSlice,
|
RenderUiSystem::ExtractTextureSlice,
|
||||||
RenderUiSystem::ExtractBorders,
|
RenderUiSystem::ExtractBorders,
|
||||||
RenderUiSystem::ExtractText,
|
RenderUiSystem::ExtractText,
|
||||||
|
RenderUiSystem::ExtractDebug,
|
||||||
)
|
)
|
||||||
.chain(),
|
.chain(),
|
||||||
)
|
)
|
||||||
@ -136,6 +143,8 @@ pub fn build_ui_render(app: &mut App) {
|
|||||||
extract_uinode_images.in_set(RenderUiSystem::ExtractImages),
|
extract_uinode_images.in_set(RenderUiSystem::ExtractImages),
|
||||||
extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders),
|
extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders),
|
||||||
extract_text_sections.in_set(RenderUiSystem::ExtractText),
|
extract_text_sections.in_set(RenderUiSystem::ExtractText),
|
||||||
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
|
debug_overlay::extract_debug_overlay.in_set(RenderUiSystem::ExtractDebug),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
|
@ -62,6 +62,7 @@ The default feature set enables most of the expected features of a game engine,
|
|||||||
|bevy_debug_stepping|Enable stepping-based debugging of Bevy systems|
|
|bevy_debug_stepping|Enable stepping-based debugging of Bevy systems|
|
||||||
|bevy_dev_tools|Provides a collection of developer tools|
|
|bevy_dev_tools|Provides a collection of developer tools|
|
||||||
|bevy_remote|Enable the Bevy Remote Protocol|
|
|bevy_remote|Enable the Bevy Remote Protocol|
|
||||||
|
|bevy_ui_debug|Provides a debug overlay for bevy UI|
|
||||||
|bmp|BMP image format support|
|
|bmp|BMP image format support|
|
||||||
|dds|DDS compressed texture support|
|
|dds|DDS compressed texture support|
|
||||||
|debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam|
|
|debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam|
|
||||||
|
@ -18,11 +18,8 @@ fn main() {
|
|||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(Update, update_scroll_position);
|
.add_systems(Update, update_scroll_position);
|
||||||
|
|
||||||
#[cfg(feature = "bevy_dev_tools")]
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
{
|
app.add_systems(Update, toggle_debug_overlay);
|
||||||
app.add_plugins(bevy::dev_tools::ui_debug_overlay::DebugUiPlugin)
|
|
||||||
.add_systems(Update, toggle_overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.run();
|
app.run();
|
||||||
}
|
}
|
||||||
@ -79,10 +76,10 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
Label,
|
Label,
|
||||||
));
|
));
|
||||||
|
|
||||||
#[cfg(feature = "bevy_dev_tools")]
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
// Debug overlay text
|
// Debug overlay text
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
Text::new("Press Space to enable debug outlines."),
|
Text::new("Press Space to toggle debug outlines."),
|
||||||
TextFont {
|
TextFont {
|
||||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||||
..default()
|
..default()
|
||||||
@ -90,9 +87,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
Label,
|
Label,
|
||||||
));
|
));
|
||||||
|
|
||||||
#[cfg(not(feature = "bevy_dev_tools"))]
|
#[cfg(not(feature = "bevy_ui_debug"))]
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
Text::new("Try enabling feature \"bevy_dev_tools\"."),
|
Text::new("Try enabling feature \"bevy_ui_debug\"."),
|
||||||
TextFont {
|
TextFont {
|
||||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||||
..default()
|
..default()
|
||||||
@ -347,12 +344,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "bevy_dev_tools")]
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
// The system that will enable/disable the debug outlines around the nodes
|
// The system that will enable/disable the debug outlines around the nodes
|
||||||
fn toggle_overlay(
|
fn toggle_debug_overlay(input: Res<ButtonInput<KeyCode>>, mut options: ResMut<UiDebugOptions>) {
|
||||||
input: Res<ButtonInput<KeyCode>>,
|
|
||||||
mut options: ResMut<bevy::dev_tools::ui_debug_overlay::UiDebugOptions>,
|
|
||||||
) {
|
|
||||||
info_once!("The debug outlines are enabled, press Space to turn them on/off");
|
info_once!("The debug outlines are enabled, press Space to turn them on/off");
|
||||||
if input.just_pressed(KeyCode::Space) {
|
if input.just_pressed(KeyCode::Space) {
|
||||||
// The toggle method will enable the debug_overlay if disabled and disable if enabled
|
// The toggle method will enable the debug_overlay if disabled and disable if enabled
|
||||||
|
Loading…
Reference in New Issue
Block a user