From cba0e932957f85237d504e71a627203532b4dc95 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 24 Nov 2025 09:07:02 +0000 Subject: [PATCH 1/7] Added new `UiDebugOptions`: * `show_padding_box` * `show_content_box` * `show_scrollbars` * `ignore_border_radius` Implemented support for the `ignore_border_radius` option. --- crates/bevy_ui_render/src/debug_overlay.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index dacc52a195791..ac609204ae981 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -24,6 +24,7 @@ use bevy_ui::ui_transform::UiGlobalTransform; use bevy_ui::CalculatedClip; use bevy_ui::ComputedNode; use bevy_ui::ComputedUiTargetCamera; +use bevy_ui::ResolvedBorderRadius; use bevy_ui::UiStack; /// Configuration for the UI debug overlay @@ -32,12 +33,22 @@ use bevy_ui::UiStack; pub struct UiDebugOptions { /// Set to true to enable the UI debug overlay pub enabled: bool, + /// Show outlines for the border boxes of UI nodes + pub show_border_box: bool, + /// Show outlines for the padding boxes of UI nodes + pub show_padding_box: bool, + /// Show outlines for the content boxes of UI nodes + pub show_content_box: bool, + /// Show outlines for the scrollbar regions of UI nodes + pub show_scrollbars: bool, /// Width of the overlay's lines in logical pixels pub line_width: f32, /// Show outlines for non-visible UI nodes pub show_hidden: bool, /// Show outlines for clipped sections of UI nodes pub show_clipped: bool, + /// Draw outlines without curved corners + pub ignore_border_radius: bool, } impl UiDebugOptions { @@ -53,6 +64,11 @@ impl Default for UiDebugOptions { line_width: 1., show_hidden: false, show_clipped: false, + ignore_border_radius: false, + show_border_box: true, + show_padding_box: false, + show_content_box: false, + show_scrollbars: false, } } } @@ -110,7 +126,11 @@ pub fn extract_debug_overlay( flip_x: false, flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), - border_radius: uinode.border_radius(), + border_radius: if debug_options.ignore_border_radius { + ResolvedBorderRadius::ZERO + } else { + uinode.border_radius() + }, node_type: NodeType::Border(shader_flags::BORDER_ALL), }, main_entity: entity.into(), From 85e409b57881afdc938f238dd7602eda869c8cca Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 24 Nov 2025 10:02:48 +0000 Subject: [PATCH 2/7] Added rendering for other new `UiDebugOptions`. `UiDebugOptions` is now both a `Component` and `Resource`, add it as a component to override the options from the resource for an individual UI node. --- crates/bevy_ui_render/src/debug_overlay.rs | 148 ++++++++++++++++----- 1 file changed, 116 insertions(+), 32 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index ac609204ae981..1f0e153fda614 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -8,12 +8,15 @@ use bevy_asset::AssetId; use bevy_camera::visibility::InheritedVisibility; use bevy_color::Hsla; use bevy_ecs::entity::Entity; +use bevy_ecs::prelude::Component; +use bevy_ecs::prelude::ReflectComponent; use bevy_ecs::prelude::ReflectResource; use bevy_ecs::resource::Resource; use bevy_ecs::system::Commands; use bevy_ecs::system::Query; use bevy_ecs::system::Res; use bevy_ecs::system::ResMut; +use bevy_math::Affine2; use bevy_math::Rect; use bevy_math::Vec2; use bevy_reflect::Reflect; @@ -28,8 +31,8 @@ use bevy_ui::ResolvedBorderRadius; use bevy_ui::UiStack; /// Configuration for the UI debug overlay -#[derive(Resource, Reflect)] -#[reflect(Resource)] +#[derive(Component, Resource, Reflect)] +#[reflect(Component, Resource)] pub struct UiDebugOptions { /// Set to true to enable the UI debug overlay pub enabled: bool, @@ -85,6 +88,7 @@ pub fn extract_debug_overlay( &InheritedVisibility, Option<&CalculatedClip>, &ComputedUiTargetCamera, + Option<&UiDebugOptions>, )>, >, ui_stack: Extract>, @@ -96,7 +100,9 @@ pub fn extract_debug_overlay( let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, transform, visibility, maybe_clip, computed_target) in &uinode_query { + for (entity, uinode, transform, visibility, maybe_clip, computed_target, debug) in &uinode_query + { + let debug_options = debug.unwrap_or(&debug_options); if !debug_options.show_hidden && !visibility.get() { continue; } @@ -105,35 +111,113 @@ pub fn extract_debug_overlay( continue; }; - // Extract a border box to display an outline for every UI Node in the layout - extracted_uinodes.uinodes.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity).id(), - // Add a large number to the UI node's stack index so that the overlay is always drawn on top - z_order: (ui_stack.uinodes.len() as u32 + uinode.stack_index()) as f32, - clip: maybe_clip - .filter(|_| !debug_options.show_clipped) - .map(|clip| clip.clip), - image: AssetId::default(), - extracted_camera_entity, - transform: transform.into(), - item: ExtractedUiItem::Node { - color: Hsla::sequential_dispersed(entity.index_u32()).into(), - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), - border_radius: if debug_options.ignore_border_radius { - ResolvedBorderRadius::ZERO - } else { - uinode.border_radius() + let color = Hsla::sequential_dispersed(entity.index_u32()).into(); + let z_order = (ui_stack.uinodes.len() as u32 + uinode.stack_index()) as f32; + let border = BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()); + let transform = transform.affine(); + + let mut push_outline = |rect: Rect, radius: ResolvedBorderRadius| { + if rect.is_empty() { + return; + } + + extracted_uinodes.uinodes.push(ExtractedUiNode { + render_entity: commands.spawn(TemporaryRenderEntity).id(), + // Keep all overlays above UI, and nudge each type slightly in Z so ordering is stable. + z_order, + clip: maybe_clip + .filter(|_| !debug_options.show_clipped) + .map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + transform: transform * Affine2::from_translation(rect.center()), + item: ExtractedUiItem::Node { + color, + rect: Rect { + min: Vec2::ZERO, + max: rect.size(), + }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border, + border_radius: radius, + node_type: NodeType::Border(shader_flags::BORDER_ALL), }, - node_type: NodeType::Border(shader_flags::BORDER_ALL), - }, - main_entity: entity.into(), - }); + main_entity: entity.into(), + }); + }; + + let border_box = Rect::from_center_size(Vec2::ZERO, uinode.size); + + if debug_options.show_border_box { + push_outline(border_box, uinode.border_radius()); + } + + if debug_options.show_padding_box { + let mut padding_box = border_box; + padding_box.min.x += uinode.border.left; + padding_box.max.x -= uinode.border.right; + padding_box.min.y += uinode.border.top; + padding_box.max.y -= uinode.border.bottom; + push_outline(padding_box, uinode.inner_radius()); + } + + if debug_options.show_content_box { + let mut content_box = border_box; + let content_inset = uinode.content_inset(); + content_box.min.x += content_inset.left; + content_box.max.x -= content_inset.right; + content_box.min.y += content_inset.top; + content_box.max.y -= content_inset.bottom; + push_outline(content_box, ResolvedBorderRadius::ZERO); + } + + if debug_options.show_scrollbars { + if uinode.scrollbar_size.y <= 0. { + let content_inset = uinode.content_inset(); + let half_size = 0.5 * uinode.size; + let min_x = -half_size.x + content_inset.left; + let max_x = half_size.x - content_inset.right - uinode.scrollbar_size.x; + let max_y = half_size.y - content_inset.bottom; + let min_y = max_y - uinode.scrollbar_size.y; + let gutter = Rect { + min: Vec2::new(min_x, min_y), + max: Vec2::new(max_x, max_y), + }; + let gutter_length = gutter.size().x; + let thumb_min = + gutter.min.x + gutter_length * uinode.scroll_position.x / uinode.content_size.x; + let thumb_max = thumb_min + gutter_length * gutter_length / uinode.content_size.x; + let thumb = Rect { + min: Vec2::new(thumb_min, gutter.min.y), + max: Vec2::new(thumb_max, gutter.max.y), + }; + push_outline(gutter, ResolvedBorderRadius::ZERO); + push_outline(thumb, ResolvedBorderRadius::ZERO); + } + if uinode.scrollbar_size.x <= 0. { + let content_inset = uinode.content_inset(); + let half_size = 0.5 * uinode.size; + let max_x = half_size.x - content_inset.right; + let min_x = max_x - uinode.scrollbar_size.x; + let min_y = -half_size.y + content_inset.top; + let max_y = half_size.y - content_inset.bottom - uinode.scrollbar_size.y; + let gutter = Rect { + min: Vec2::new(min_x, min_y), + max: Vec2::new(max_x, max_y), + }; + let gutter_length = gutter.size().y; + let thumb_min = + gutter.min.y + gutter_length * uinode.scroll_position.y / uinode.content_size.y; + let thumb_max = thumb_min + gutter_length * gutter_length / uinode.content_size.y; + let thumb = Rect { + min: Vec2::new(gutter.min.x, thumb_min), + max: Vec2::new(gutter.max.x, thumb_max), + }; + push_outline(gutter, ResolvedBorderRadius::ZERO); + push_outline(thumb, ResolvedBorderRadius::ZERO); + } + } } } From 62e6a2438dc3573ab7f098663813c89ba8a996b4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 24 Nov 2025 10:21:02 +0000 Subject: [PATCH 3/7] * Fixed scrollbars size check * Moved debug options enabled check inside uinode loop --- crates/bevy_ui_render/src/debug_overlay.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 1f0e153fda614..1856d0fe7aa6c 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -94,15 +94,14 @@ pub fn extract_debug_overlay( ui_stack: Extract>, camera_map: Extract, ) { - if !debug_options.enabled { - return; - } - let mut camera_mapper = camera_map.get_mapper(); for (entity, uinode, transform, visibility, maybe_clip, computed_target, debug) in &uinode_query { let debug_options = debug.unwrap_or(&debug_options); + if !debug_options.enabled { + continue; + } if !debug_options.show_hidden && !visibility.get() { continue; } @@ -174,7 +173,7 @@ pub fn extract_debug_overlay( } if debug_options.show_scrollbars { - if uinode.scrollbar_size.y <= 0. { + if 0. <= uinode.scrollbar_size.y { let content_inset = uinode.content_inset(); let half_size = 0.5 * uinode.size; let min_x = -half_size.x + content_inset.left; @@ -196,7 +195,7 @@ pub fn extract_debug_overlay( push_outline(gutter, ResolvedBorderRadius::ZERO); push_outline(thumb, ResolvedBorderRadius::ZERO); } - if uinode.scrollbar_size.x <= 0. { + if 0. <= uinode.scrollbar_size.x { let content_inset = uinode.content_inset(); let half_size = 0.5 * uinode.size; let max_x = half_size.x - content_inset.right; From ec7068a59e0bda6ee870702262ddc14696ffad74 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 24 Nov 2025 10:22:12 +0000 Subject: [PATCH 4/7] Display scrollbar in "scroll" example using debug overlay when "bevy_ui_debug" feature is enabled. --- examples/ui/scroll.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index e81644708b9a4..944ed80fa52e1 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -12,6 +12,7 @@ use bevy::{ fn main() { let mut app = App::new(); + app.add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, send_scroll_events) @@ -230,8 +231,21 @@ fn vertically_scrolling_list(font_handle: Handle) -> impl Bundle { align_self: AlignSelf::Stretch, height: percent(50), overflow: Overflow::scroll_y(), // n.b. + scrollbar_width: 20., ..default() }, + #[cfg(feature = "bevy_ui_debug")] + UiDebugOptions { + enabled: true, + show_border_box: false, + show_padding_box: false, + show_content_box: false, + show_scrollbars: true, + line_width: 2., + show_hidden: false, + show_clipped: true, + ignore_border_radius: true + }, BackgroundColor(Color::srgb(0.10, 0.10, 0.10)), Children::spawn(SpawnIter((0..25).map(move |i| { ( From ac4fe9b6ced55c8040239d3663d67492c1b7791a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 11:37:36 +0000 Subject: [PATCH 5/7] Renamed `show_*` region fields on `UiDebugOptions` to `outline_*`. --- crates/bevy_ui_render/src/debug_overlay.rs | 24 +++++++++++----------- examples/ui/scroll.rs | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 1856d0fe7aa6c..edb281ccaaba2 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -37,13 +37,13 @@ pub struct UiDebugOptions { /// Set to true to enable the UI debug overlay pub enabled: bool, /// Show outlines for the border boxes of UI nodes - pub show_border_box: bool, + pub outline_border_box: bool, /// Show outlines for the padding boxes of UI nodes - pub show_padding_box: bool, + pub outline_padding_box: bool, /// Show outlines for the content boxes of UI nodes - pub show_content_box: bool, + pub outline_content_box: bool, /// Show outlines for the scrollbar regions of UI nodes - pub show_scrollbars: bool, + pub outline_scrollbars: bool, /// Width of the overlay's lines in logical pixels pub line_width: f32, /// Show outlines for non-visible UI nodes @@ -68,10 +68,10 @@ impl Default for UiDebugOptions { show_hidden: false, show_clipped: false, ignore_border_radius: false, - show_border_box: true, - show_padding_box: false, - show_content_box: false, - show_scrollbars: false, + outline_border_box: true, + outline_padding_box: false, + outline_content_box: false, + outline_scrollbars: false, } } } @@ -149,11 +149,11 @@ pub fn extract_debug_overlay( let border_box = Rect::from_center_size(Vec2::ZERO, uinode.size); - if debug_options.show_border_box { + if debug_options.outline_border_box { push_outline(border_box, uinode.border_radius()); } - if debug_options.show_padding_box { + if debug_options.outline_padding_box { let mut padding_box = border_box; padding_box.min.x += uinode.border.left; padding_box.max.x -= uinode.border.right; @@ -162,7 +162,7 @@ pub fn extract_debug_overlay( push_outline(padding_box, uinode.inner_radius()); } - if debug_options.show_content_box { + if debug_options.outline_content_box { let mut content_box = border_box; let content_inset = uinode.content_inset(); content_box.min.x += content_inset.left; @@ -172,7 +172,7 @@ pub fn extract_debug_overlay( push_outline(content_box, ResolvedBorderRadius::ZERO); } - if debug_options.show_scrollbars { + if debug_options.outline_scrollbars { if 0. <= uinode.scrollbar_size.y { let content_inset = uinode.content_inset(); let half_size = 0.5 * uinode.size; diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index 944ed80fa52e1..30c8aa3bb3c97 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -237,10 +237,10 @@ fn vertically_scrolling_list(font_handle: Handle) -> impl Bundle { #[cfg(feature = "bevy_ui_debug")] UiDebugOptions { enabled: true, - show_border_box: false, - show_padding_box: false, - show_content_box: false, - show_scrollbars: true, + outline_border_box: false, + outline_padding_box: false, + outline_content_box: false, + outline_scrollbars: true, line_width: 2., show_hidden: false, show_clipped: true, From 2322f7c4fe4380ef3e8c892750053d72b155060c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 11:51:45 +0000 Subject: [PATCH 6/7] Added release not --- .../release-notes/new_ui_debug_overlay_features.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 release-content/release-notes/new_ui_debug_overlay_features.md diff --git a/release-content/release-notes/new_ui_debug_overlay_features.md b/release-content/release-notes/new_ui_debug_overlay_features.md new file mode 100644 index 0000000000000..b7b86da8b666f --- /dev/null +++ b/release-content/release-notes/new_ui_debug_overlay_features.md @@ -0,0 +1,9 @@ +--- +title: "New UI debug overlay features" +authors: ["@ickshonpe"] +pull_requests: [21931] +--- + +`UiDebugOptions` now lets you toggle outlines for border, padding, content and scrollbar regions, and optionally ignore border radius to render node outlines without curved corners. It can be used both as a `Resource` (global defaults) and as a `Component` (per-node overrides). + +The scroll example was updated to outline the scrollbar bounds when the `bevy_ui_debug` feature is enabled. \ No newline at end of file From 18203aeb069dae38364240cc196161a7e087f7f9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 11:52:04 +0000 Subject: [PATCH 7/7] Added trailing line to the release note. --- release-content/release-notes/new_ui_debug_overlay_features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/new_ui_debug_overlay_features.md b/release-content/release-notes/new_ui_debug_overlay_features.md index b7b86da8b666f..a3f79b9758a47 100644 --- a/release-content/release-notes/new_ui_debug_overlay_features.md +++ b/release-content/release-notes/new_ui_debug_overlay_features.md @@ -6,4 +6,4 @@ pull_requests: [21931] `UiDebugOptions` now lets you toggle outlines for border, padding, content and scrollbar regions, and optionally ignore border radius to render node outlines without curved corners. It can be used both as a `Resource` (global defaults) and as a `Component` (per-node overrides). -The scroll example was updated to outline the scrollbar bounds when the `bevy_ui_debug` feature is enabled. \ No newline at end of file +The scroll example was updated to outline the scrollbar bounds when the `bevy_ui_debug` feature is enabled.