Add a gizmo-based overlay to show UI node outlines (Adopted) (#11237)
# Objective - This is an adopted version of #10420 - The objective is to help debugging the Ui layout tree with helpful outlines, that can be easily enabled/disabled ## Solution - Like #10420, the solution is using the bevy_gizmos in outlining the nodes --- ## Changelog ### Added - Added debug_overlay mod to `bevy_dev_tools` - Added bevy_ui_debug feature to `bevy_dev_tools` ## How to use - The user must use `bevy_dev_tools` feature in TOML - The user must use the plugin UiDebugPlugin, that can be found on `bevy::dev_tools::debug_overlay` - Finally, to enable the function, the user must set `UiDebugOptions::enabled` to true Someone can easily toggle the function with something like: ```rust fn toggle_overlay(input: Res<ButtonInput<KeyCode>>, options: ResMut<UiDebugOptions>) { if input.just_pressed(KeyCode::Space) { // The toggle method will enable if disabled and disable if enabled options.toggle(); } } ``` Note that this feature can be disabled from dev_tools, as its in fact behind a default feature there, being the feature bevy_ui_debug. # Limitations Currently, due to limitations with gizmos itself, it's not possible to support this feature to more the one window, so this tool is limited to the primary window only. # Showcase  Ui example with debug_overlay enabled  And disabled --------- Co-authored-by: Nicola Papale <nico@nicopap.ch> Co-authored-by: Pablo Reinhardt <pabloreinhardt@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
parent
289a02cad6
commit
1af9bc853b
@ -9,22 +9,31 @@ 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.14.0-dev" }
|
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
||||||
|
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
|
||||||
|
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
||||||
|
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
|
||||||
|
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
|
||||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||||
|
bevy_gizmos = { path = "../bevy_gizmos", version = "0.14.0-dev" }
|
||||||
|
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
|
||||||
|
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
|
||||||
|
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||||
|
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
|
||||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||||
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
|
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
|
||||||
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
|
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
|
||||||
bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev" }
|
bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev" }
|
||||||
|
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||||
|
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
|
||||||
bevy_text = { path = "../bevy_text", version = "0.14.0-dev" }
|
bevy_text = { path = "../bevy_text", version = "0.14.0-dev" }
|
||||||
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
|
|
||||||
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
|
|
||||||
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
|
|
||||||
|
|
||||||
# other
|
# other
|
||||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
|
192
crates/bevy_dev_tools/src/debug_overlay/inset.rs
Normal file
192
crates/bevy_dev_tools/src/debug_overlay/inset.rs
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
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::new(),
|
||||||
|
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 Some(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),
|
||||||
|
];
|
||||||
|
self.draw
|
||||||
|
.linestrip_2d(strip.map(|v| self.relative(v)), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
280
crates/bevy_dev_tools/src/debug_overlay/mod.rs
Normal file
280
crates/bevy_dev_tools/src/debug_overlay/mod.rs
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
//! A visual representation of UI node sizes.
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
|
|
||||||
|
use bevy_app::{App, Plugin, PostUpdate};
|
||||||
|
use bevy_color::Hsla;
|
||||||
|
use bevy_core::Name;
|
||||||
|
use bevy_core_pipeline::core_2d::Camera2dBundle;
|
||||||
|
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::{DefaultUiCamera, Display, Node, Style, 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::none().with(16);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct LayoutRect {
|
||||||
|
pos: Vec2,
|
||||||
|
size: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutRect {
|
||||||
|
fn new(trans: &GlobalTransform, node: &Node, 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((
|
||||||
|
Camera2dBundle {
|
||||||
|
projection: OrthographicProjection {
|
||||||
|
far: 1000.0,
|
||||||
|
viewport_origin: Vec2::new(0.0, 0.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
camera: Camera {
|
||||||
|
order: LAYOUT_DEBUG_CAMERA_ORDER,
|
||||||
|
clear_color: ClearColorConfig::None,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
LAYOUT_DEBUG_LAYERS,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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, style, children) in outline.nodes.iter_many(to_iter) {
|
||||||
|
if style.is_none() || style.is_some_and(|s| matches!(s.display, Display::None)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(view_visibility) = outline.view_visibility.get(entity) {
|
||||||
|
if !view_visibility.get() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rect = LayoutRect::new(trans, 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,
|
||||||
|
Option<&'static Style>,
|
||||||
|
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,
|
||||||
|
&Node,
|
||||||
|
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 = window_scale * 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,9 @@ use bevy_app::prelude::*;
|
|||||||
pub mod ci_testing;
|
pub mod ci_testing;
|
||||||
pub mod fps_overlay;
|
pub mod fps_overlay;
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_ui_debug")]
|
||||||
|
pub mod debug_overlay;
|
||||||
|
|
||||||
/// 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`
|
||||||
/// feature.
|
/// feature.
|
||||||
///
|
///
|
||||||
|
@ -12,18 +12,25 @@ use bevy::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
let mut app = App::new();
|
||||||
.add_plugins(DefaultPlugins)
|
app.add_plugins(DefaultPlugins)
|
||||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||||
.insert_resource(WinitSettings::desktop_app())
|
.insert_resource(WinitSettings::desktop_app())
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(Update, mouse_scroll)
|
.add_systems(Update, mouse_scroll);
|
||||||
.run();
|
|
||||||
|
#[cfg(feature = "bevy_dev_tools")]
|
||||||
|
{
|
||||||
|
app.add_plugins(bevy::dev_tools::debug_overlay::DebugUiPlugin)
|
||||||
|
.add_systems(Update, toggle_overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
// Camera
|
// Camera
|
||||||
commands.spawn(Camera2dBundle::default());
|
commands.spawn((Camera2dBundle::default(), IsDefaultUiCamera));
|
||||||
|
|
||||||
// root node
|
// root node
|
||||||
commands
|
commands
|
||||||
@ -54,6 +61,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
.spawn(NodeBundle {
|
.spawn(NodeBundle {
|
||||||
style: Style {
|
style: Style {
|
||||||
width: Val::Percent(100.),
|
width: Val::Percent(100.),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
background_color: Color::srgb(0.15, 0.15, 0.15).into(),
|
background_color: Color::srgb(0.15, 0.15, 0.15).into(),
|
||||||
@ -79,6 +87,33 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
// for accessibility to treat the text accordingly.
|
// for accessibility to treat the text accordingly.
|
||||||
Label,
|
Label,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_dev_tools")]
|
||||||
|
// Debug overlay text
|
||||||
|
parent.spawn((
|
||||||
|
TextBundle::from_section(
|
||||||
|
"Press Space to enable debug outlines.",
|
||||||
|
TextStyle {
|
||||||
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||||
|
font_size: 20.,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Label,
|
||||||
|
));
|
||||||
|
|
||||||
|
#[cfg(not(feature = "bevy_dev_tools"))]
|
||||||
|
parent.spawn((
|
||||||
|
TextBundle::from_section(
|
||||||
|
"Try enabling feature \"bevy_dev_tools\".",
|
||||||
|
TextStyle {
|
||||||
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||||
|
font_size: 20.,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Label,
|
||||||
|
));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// right vertical fill
|
// right vertical fill
|
||||||
@ -334,3 +369,16 @@ fn mouse_scroll(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bevy_dev_tools")]
|
||||||
|
// 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::debug_overlay::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
|
||||||
|
options.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user