diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index dacc52a195791..edb281ccaaba2 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; @@ -24,20 +27,31 @@ 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 -#[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, + /// Show outlines for the border boxes of UI nodes + pub outline_border_box: bool, + /// Show outlines for the padding boxes of UI nodes + pub outline_padding_box: bool, + /// Show outlines for the content boxes of UI nodes + pub outline_content_box: bool, + /// Show outlines for the scrollbar regions of UI nodes + pub outline_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 +67,11 @@ impl Default for UiDebugOptions { line_width: 1., show_hidden: false, show_clipped: false, + ignore_border_radius: false, + outline_border_box: true, + outline_padding_box: false, + outline_content_box: false, + outline_scrollbars: false, } } } @@ -69,18 +88,20 @@ pub fn extract_debug_overlay( &InheritedVisibility, Option<&CalculatedClip>, &ComputedUiTargetCamera, + Option<&UiDebugOptions>, )>, >, 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) 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.enabled { + continue; + } if !debug_options.show_hidden && !visibility.get() { continue; } @@ -89,31 +110,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, + 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), }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), - border_radius: uinode.border_radius(), - 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.outline_border_box { + push_outline(border_box, uinode.border_radius()); + } + + 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; + padding_box.min.y += uinode.border.top; + padding_box.max.y -= uinode.border.bottom; + push_outline(padding_box, uinode.inner_radius()); + } + + 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; + 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.outline_scrollbars { + 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; + 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 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; + 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); + } + } } } diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index e81644708b9a4..30c8aa3bb3c97 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, + outline_border_box: false, + outline_padding_box: false, + outline_content_box: false, + outline_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| { ( 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..a3f79b9758a47 --- /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.