Skip to content

Commit 1af9bc8

Browse files
pablo-luanicopapPablo Reinhardtalice-i-cecile
authored
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 ![image](https://github.com/bevyengine/bevy/assets/126117294/ce9d70e6-0a57-4fa9-9753-ff5a9d82c009) Ui example with debug_overlay enabled ![image](https://github.com/bevyengine/bevy/assets/126117294/e945015c-5bab-4d7f-9273-472aabaf25a9) And disabled --------- Co-authored-by: Nicola Papale <[email protected]> Co-authored-by: Pablo Reinhardt <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent 289a02c commit 1af9bc8

File tree

5 files changed

+543
-11
lines changed

5 files changed

+543
-11
lines changed

crates/bevy_dev_tools/Cargo.toml

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,31 @@ license = "MIT OR Apache-2.0"
99
keywords = ["bevy"]
1010

1111
[features]
12+
default = ["bevy_ui_debug"]
1213
bevy_ci_testing = ["serde", "ron"]
14+
bevy_ui_debug = []
1315

1416
[dependencies]
1517
# bevy
1618
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
17-
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
19+
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
20+
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
21+
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
22+
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
23+
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
1824
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
25+
bevy_gizmos = { path = "../bevy_gizmos", version = "0.14.0-dev" }
26+
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
27+
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
28+
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
29+
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
1930
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
2031
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
21-
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
22-
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
32+
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
2333
bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev" }
34+
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
35+
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
2436
bevy_text = { path = "../bevy_text", version = "0.14.0-dev" }
25-
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
26-
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
27-
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
2837

2938
# other
3039
serde = { version = "1.0", features = ["derive"], optional = true }
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use bevy_color::Color;
2+
use bevy_gizmos::{config::GizmoConfigGroup, prelude::Gizmos};
3+
use bevy_math::{Vec2, Vec2Swizzles};
4+
use bevy_reflect::Reflect;
5+
use bevy_transform::prelude::GlobalTransform;
6+
use bevy_utils::HashMap;
7+
8+
use super::{CameraQuery, LayoutRect};
9+
10+
// Function used here so we don't need to redraw lines that are fairly close to each other.
11+
fn approx_eq(compared: f32, other: f32) -> bool {
12+
(compared - other).abs() < 0.001
13+
}
14+
15+
fn rect_border_axis(rect: LayoutRect) -> (f32, f32, f32, f32) {
16+
let pos = rect.pos;
17+
let size = rect.size;
18+
let offset = pos + size;
19+
(pos.x, offset.x, pos.y, offset.y)
20+
}
21+
22+
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
23+
enum Dir {
24+
Start,
25+
End,
26+
}
27+
impl Dir {
28+
const fn increments(self) -> i64 {
29+
match self {
30+
Dir::Start => 1,
31+
Dir::End => -1,
32+
}
33+
}
34+
}
35+
impl From<i64> for Dir {
36+
fn from(value: i64) -> Self {
37+
if value.is_positive() {
38+
Dir::Start
39+
} else {
40+
Dir::End
41+
}
42+
}
43+
}
44+
/// Collection of axis aligned "lines" (actually just their coordinate on
45+
/// a given axis).
46+
#[derive(Debug, Clone)]
47+
struct DrawnLines {
48+
lines: HashMap<i64, Dir>,
49+
width: f32,
50+
}
51+
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
52+
impl DrawnLines {
53+
fn new(width: f32) -> Self {
54+
DrawnLines {
55+
lines: HashMap::new(),
56+
width,
57+
}
58+
}
59+
/// Return `value` offset by as many `increment`s as necessary to make it
60+
/// not overlap with already drawn lines.
61+
fn inset(&self, value: f32) -> f32 {
62+
let scaled = value / self.width;
63+
let fract = scaled.fract();
64+
let mut on_grid = scaled.floor() as i64;
65+
for _ in 0..10 {
66+
let Some(dir) = self.lines.get(&on_grid) else {
67+
break;
68+
};
69+
// TODO(clean): This fixes a panic, but I'm not sure how valid this is
70+
let Some(added) = on_grid.checked_add(dir.increments()) else {
71+
break;
72+
};
73+
on_grid = added;
74+
}
75+
((on_grid as f32) + fract) * self.width
76+
}
77+
/// Remove a line from the collection of drawn lines.
78+
///
79+
/// Typically, we only care for pre-existing lines when drawing the children
80+
/// of a container, nothing more. So we remove it after we are done with
81+
/// the children.
82+
fn remove(&mut self, value: f32, increment: i64) {
83+
let mut on_grid = (value / self.width).floor() as i64;
84+
loop {
85+
// TODO(clean): This fixes a panic, but I'm not sure how valid this is
86+
let Some(next_cell) = on_grid.checked_add(increment) else {
87+
return;
88+
};
89+
if !self.lines.contains_key(&next_cell) {
90+
self.lines.remove(&on_grid);
91+
return;
92+
}
93+
on_grid = next_cell;
94+
}
95+
}
96+
/// Add a line from the collection of drawn lines.
97+
fn add(&mut self, value: f32, increment: i64) {
98+
let mut on_grid = (value / self.width).floor() as i64;
99+
loop {
100+
let old_value = self.lines.insert(on_grid, increment.into());
101+
if old_value.is_none() {
102+
return;
103+
}
104+
// TODO(clean): This fixes a panic, but I'm not sure how valid this is
105+
let Some(added) = on_grid.checked_add(increment) else {
106+
return;
107+
};
108+
on_grid = added;
109+
}
110+
}
111+
}
112+
113+
#[derive(GizmoConfigGroup, Reflect, Default)]
114+
pub struct UiGizmosDebug;
115+
116+
pub(super) struct InsetGizmo<'w, 's> {
117+
draw: Gizmos<'w, 's, UiGizmosDebug>,
118+
cam: CameraQuery<'w, 's>,
119+
known_y: DrawnLines,
120+
known_x: DrawnLines,
121+
}
122+
impl<'w, 's> InsetGizmo<'w, 's> {
123+
pub(super) fn new(
124+
draw: Gizmos<'w, 's, UiGizmosDebug>,
125+
cam: CameraQuery<'w, 's>,
126+
line_width: f32,
127+
) -> Self {
128+
InsetGizmo {
129+
draw,
130+
cam,
131+
known_y: DrawnLines::new(line_width),
132+
known_x: DrawnLines::new(line_width),
133+
}
134+
}
135+
fn relative(&self, mut position: Vec2) -> Vec2 {
136+
let zero = GlobalTransform::IDENTITY;
137+
let Ok(cam) = self.cam.get_single() else {
138+
return Vec2::ZERO;
139+
};
140+
if let Some(new_position) = cam.world_to_viewport(&zero, position.extend(0.)) {
141+
position = new_position;
142+
};
143+
position.xy()
144+
}
145+
fn line_2d(&mut self, mut start: Vec2, mut end: Vec2, color: Color) {
146+
if approx_eq(start.x, end.x) {
147+
start.x = self.known_x.inset(start.x);
148+
end.x = start.x;
149+
} else if approx_eq(start.y, end.y) {
150+
start.y = self.known_y.inset(start.y);
151+
end.y = start.y;
152+
}
153+
let (start, end) = (self.relative(start), self.relative(end));
154+
self.draw.line_2d(start, end, color);
155+
}
156+
pub(super) fn set_scope(&mut self, rect: LayoutRect) {
157+
let (left, right, top, bottom) = rect_border_axis(rect);
158+
self.known_x.add(left, 1);
159+
self.known_x.add(right, -1);
160+
self.known_y.add(top, 1);
161+
self.known_y.add(bottom, -1);
162+
}
163+
pub(super) fn clear_scope(&mut self, rect: LayoutRect) {
164+
let (left, right, top, bottom) = rect_border_axis(rect);
165+
self.known_x.remove(left, 1);
166+
self.known_x.remove(right, -1);
167+
self.known_y.remove(top, 1);
168+
self.known_y.remove(bottom, -1);
169+
}
170+
pub(super) fn rect_2d(&mut self, rect: LayoutRect, color: Color) {
171+
let (left, right, top, bottom) = rect_border_axis(rect);
172+
if approx_eq(left, right) {
173+
self.line_2d(Vec2::new(left, top), Vec2::new(left, bottom), color);
174+
} else if approx_eq(top, bottom) {
175+
self.line_2d(Vec2::new(left, top), Vec2::new(right, top), color);
176+
} else {
177+
let inset_x = |v| self.known_x.inset(v);
178+
let inset_y = |v| self.known_y.inset(v);
179+
let (left, right) = (inset_x(left), inset_x(right));
180+
let (top, bottom) = (inset_y(top), inset_y(bottom));
181+
let strip = [
182+
Vec2::new(left, top),
183+
Vec2::new(left, bottom),
184+
Vec2::new(right, bottom),
185+
Vec2::new(right, top),
186+
Vec2::new(left, top),
187+
];
188+
self.draw
189+
.linestrip_2d(strip.map(|v| self.relative(v)), color);
190+
}
191+
}
192+
}

0 commit comments

Comments
 (0)