Skip to content

Commit

Permalink
Add a gizmo-based overlay to show UI node outlines
Browse files Browse the repository at this point in the history
  • Loading branch information
nicopap committed Nov 6, 2023
1 parent 32a5c7d commit d929a43
Show file tree
Hide file tree
Showing 6 changed files with 455 additions and 4 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ bevy_ui = [
"bevy_sprite",
]

# Enable the bevy_ui debug overlay
bevy_ui_debug = ["bevy_internal/bevy_ui_debug"]

# winit window and input backend
bevy_winit = ["bevy_internal/bevy_winit"]

Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ file_watcher = ["bevy_asset?/file_watcher"]
# Enables watching embedded files for Bevy Asset hot-reloading
embedded_watcher = ["bevy_asset?/embedded_watcher"]

# Enable the bevy_ui debug overlay
bevy_ui_debug = ["bevy_ui?/debug"]

[dependencies]
# bevy
bevy_a11y = { path = "../bevy_a11y", version = "0.12.0" }
Expand Down
12 changes: 8 additions & 4 deletions crates/bevy_ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,31 @@ repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]

[features]
default = []
debug = ["bevy_gizmos", "bevy_core"]

[dependencies]
# bevy
bevy_a11y = { path = "../bevy_a11y", version = "0.12.0" }
bevy_app = { path = "../bevy_app", version = "0.12.0" }
bevy_asset = { path = "../bevy_asset", version = "0.12.0" }
bevy_core = { path = "../bevy_core", version = "0.12.0", optional = true }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.12.0" }
bevy_derive = { path = "../bevy_derive", version = "0.12.0" }
bevy_ecs = { path = "../bevy_ecs", version = "0.12.0" }
bevy_gizmos = { path = "../bevy_gizmos", version = "0.12.0", optional = true }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.12.0" }
bevy_input = { path = "../bevy_input", version = "0.12.0" }
bevy_log = { path = "../bevy_log", version = "0.12.0" }
bevy_math = { path = "../bevy_math", version = "0.12.0" }
bevy_reflect = { path = "../bevy_reflect", version = "0.12.0", features = [
"bevy",
] }
bevy_reflect = { path = "../bevy_reflect", version = "0.12.0", features = ["bevy"] }
bevy_render = { path = "../bevy_render", version = "0.12.0" }
bevy_sprite = { path = "../bevy_sprite", version = "0.12.0" }
bevy_text = { path = "../bevy_text", version = "0.12.0", optional = true }
bevy_transform = { path = "../bevy_transform", version = "0.12.0" }
bevy_window = { path = "../bevy_window", version = "0.12.0" }
bevy_utils = { path = "../bevy_utils", version = "0.12.0" }
bevy_window = { path = "../bevy_window", version = "0.12.0" }

# other
taffy = { version = "0.3.10" }
Expand Down
189 changes: 189 additions & 0 deletions crates/bevy_ui/src/debug_overlay/inset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use bevy_gizmos::prelude::Gizmos;
use bevy_math::{Vec2, Vec2Swizzles};
use bevy_render::prelude::Color;
use bevy_transform::prelude::GlobalTransform;
use bevy_utils::HashMap;

use super::{CameraQuery, LayoutRect};

trait ApproxF32 {
fn is(self, other: f32) -> bool;
}
impl ApproxF32 for f32 {
fn is(self, other: f32) -> bool {
let diff = (self - other).abs();
diff < 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;
}
}
}

pub(super) struct InsetGizmo<'w, 's> {
draw: Gizmos<'s>,
cam: CameraQuery<'w, 's>,
known_y: DrawnLines,
known_x: DrawnLines,
}
impl<'w, 's> InsetGizmo<'w, 's> {
pub(super) fn new(draw: Gizmos<'s>, 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 start.x.is(end.x) {
start.x = self.known_x.inset(start.x);
end.x = start.x;
} else if start.y.is(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 left.is(right) {
self.line_2d(Vec2::new(left, top), Vec2::new(left, bottom), color);
} else if top.is(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);
}
}
}
Loading

0 comments on commit d929a43

Please sign in to comment.