mirror of
https://github.com/bevyengine/bevy
synced 2024-12-18 09:03:07 +00:00
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
12 changed files with 142 additions and 497 deletions
|
@ -156,6 +156,9 @@ 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
|
||||
dynamic_linking = ["dep:bevy_dylib", "bevy_internal/dynamic_linking"]
|
||||
|
||||
|
|
|
@ -9,29 +9,19 @@ license = "MIT OR Apache-2.0"
|
|||
keywords = ["bevy"]
|
||||
|
||||
[features]
|
||||
default = ["bevy_ui_debug"]
|
||||
bevy_ci_testing = ["serde", "ron"]
|
||||
bevy_ui_debug = []
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", 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_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_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_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_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_ui = { path = "../bevy_ui", 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;
|
||||
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
pub mod ui_debug_overlay;
|
||||
|
||||
pub mod states;
|
||||
|
||||
/// 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
|
||||
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
|
||||
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
||||
|
||||
|
|
|
@ -43,8 +43,10 @@ smallvec = "1.11"
|
|||
accesskit = "0.17"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serialize = ["serde", "smallvec/serde", "bevy_math/serialize"]
|
||||
bevy_ui_picking_backend = ["bevy_picking"]
|
||||
bevy_ui_debug = []
|
||||
|
||||
# Experimental features
|
||||
ghost_nodes = []
|
||||
|
|
|
@ -40,12 +40,16 @@ pub use measurement::*;
|
|||
pub use render::*;
|
||||
pub use ui_material::*;
|
||||
pub use ui_node::*;
|
||||
|
||||
use widget::{ImageNode, ImageNodeSize};
|
||||
|
||||
/// The UI prelude.
|
||||
///
|
||||
/// This includes the most common types in this crate, re-exported for your convenience.
|
||||
pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
pub use crate::render::UiDebugOptions;
|
||||
#[allow(deprecated)]
|
||||
#[doc(hidden)]
|
||||
pub use crate::widget::TextBundle;
|
||||
|
@ -230,6 +234,9 @@ impl Plugin for UiPlugin {
|
|||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
app.init_resource::<UiDebugOptions>();
|
||||
|
||||
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;
|
||||
pub mod ui_texture_slice_pipeline;
|
||||
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
mod debug_overlay;
|
||||
|
||||
use crate::widget::ImageNode;
|
||||
use crate::{
|
||||
experimental::UiChildren, BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip,
|
||||
|
@ -41,6 +44,8 @@ use bevy_render::{
|
|||
};
|
||||
use bevy_sprite::TextureAtlasLayout;
|
||||
use bevy_sprite::{BorderRect, SpriteAssetEvents};
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
pub use debug_overlay::UiDebugOptions;
|
||||
|
||||
use crate::{Display, Node};
|
||||
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo};
|
||||
|
@ -98,6 +103,7 @@ pub enum RenderUiSystem {
|
|||
ExtractTextureSlice,
|
||||
ExtractBorders,
|
||||
ExtractText,
|
||||
ExtractDebug,
|
||||
}
|
||||
|
||||
pub fn build_ui_render(app: &mut App) {
|
||||
|
@ -125,6 +131,7 @@ pub fn build_ui_render(app: &mut App) {
|
|||
RenderUiSystem::ExtractTextureSlice,
|
||||
RenderUiSystem::ExtractBorders,
|
||||
RenderUiSystem::ExtractText,
|
||||
RenderUiSystem::ExtractDebug,
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
|
@ -136,6 +143,8 @@ pub fn build_ui_render(app: &mut App) {
|
|||
extract_uinode_images.in_set(RenderUiSystem::ExtractImages),
|
||||
extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders),
|
||||
extract_text_sections.in_set(RenderUiSystem::ExtractText),
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
debug_overlay::extract_debug_overlay.in_set(RenderUiSystem::ExtractDebug),
|
||||
),
|
||||
)
|
||||
.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_dev_tools|Provides a collection of developer tools|
|
||||
|bevy_remote|Enable the Bevy Remote Protocol|
|
||||
|bevy_ui_debug|Provides a debug overlay for bevy UI|
|
||||
|bmp|BMP image format support|
|
||||
|dds|DDS compressed texture support|
|
||||
|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(Update, update_scroll_position);
|
||||
|
||||
#[cfg(feature = "bevy_dev_tools")]
|
||||
{
|
||||
app.add_plugins(bevy::dev_tools::ui_debug_overlay::DebugUiPlugin)
|
||||
.add_systems(Update, toggle_overlay);
|
||||
}
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
app.add_systems(Update, toggle_debug_overlay);
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
@ -79,10 +76,10 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
Label,
|
||||
));
|
||||
|
||||
#[cfg(feature = "bevy_dev_tools")]
|
||||
#[cfg(feature = "bevy_ui_debug")]
|
||||
// Debug overlay text
|
||||
parent.spawn((
|
||||
Text::new("Press Space to enable debug outlines."),
|
||||
Text::new("Press Space to toggle debug outlines."),
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
..default()
|
||||
|
@ -90,9 +87,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
Label,
|
||||
));
|
||||
|
||||
#[cfg(not(feature = "bevy_dev_tools"))]
|
||||
#[cfg(not(feature = "bevy_ui_debug"))]
|
||||
parent.spawn((
|
||||
Text::new("Try enabling feature \"bevy_dev_tools\"."),
|
||||
Text::new("Try enabling feature \"bevy_ui_debug\"."),
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
..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
|
||||
fn toggle_overlay(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut options: ResMut<bevy::dev_tools::ui_debug_overlay::UiDebugOptions>,
|
||||
) {
|
||||
fn toggle_debug_overlay(input: Res<ButtonInput<KeyCode>>, mut options: ResMut<UiDebugOptions>) {
|
||||
info_once!("The debug outlines are enabled, press Space to turn them on/off");
|
||||
if input.just_pressed(KeyCode::Space) {
|
||||
// The toggle method will enable the debug_overlay if disabled and disable if enabled
|
||||
|
|
Loading…
Reference in a new issue