Skip to content

Commit f7aa83a

Browse files
ickshonpenicoburns
andauthored
Ui Node Borders (#7795)
# Objective Implement borders for UI nodes. Relevant discussion: #7785 Related: #5924, #3991 <img width="283" alt="borders" src="https://user-images.githubusercontent.com/27962798/220968899-7661d5ec-6f5b-4b0f-af29-bf9af02259b5.PNG"> ## Solution Add an extraction function to draw the borders. --- Can only do one colour rectangular borders due to the limitations of the Bevy UI renderer. Maybe it can be combined with #3991 eventually to add curved border support. ## Changelog * Added a component `BorderColor`. * Added the `extract_uinode_borders` system to the UI Render App. * Added the UI example `borders` --------- Co-authored-by: Nico Burns <[email protected]>
1 parent 2551ccb commit f7aa83a

File tree

10 files changed

+547
-4
lines changed

10 files changed

+547
-4
lines changed

Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,6 +1762,16 @@ category = "Transforms"
17621762
wasm = true
17631763

17641764
# UI (User Interface)
1765+
[[example]]
1766+
name = "borders"
1767+
path = "examples/ui/borders.rs"
1768+
1769+
[package.metadata.example.borders]
1770+
name = "Borders"
1771+
description = "Demonstrates how to create a node with a border"
1772+
category = "UI (User Interface)"
1773+
wasm = true
1774+
17651775
[[example]]
17661776
name = "button"
17671777
path = "examples/ui/button.rs"
@@ -1923,6 +1933,16 @@ description = "Illustrates how to scale the UI"
19231933
category = "UI (User Interface)"
19241934
wasm = true
19251935

1936+
[[example]]
1937+
name = "viewport_debug"
1938+
path = "examples/ui/viewport_debug.rs"
1939+
1940+
[package.metadata.example.viewport_debug]
1941+
name = "Viewport Debug"
1942+
description = "An example for debugging viewport coordinates"
1943+
category = "UI (User Interface)"
1944+
wasm = true
1945+
19261946
# Window
19271947
[[example]]
19281948
name = "clear_color"

crates/bevy_ui/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ impl Plugin for UiPlugin {
116116
.register_type::<UiImageSize>()
117117
.register_type::<UiRect>()
118118
.register_type::<Val>()
119+
.register_type::<BorderColor>()
119120
.register_type::<widget::Button>()
120121
.register_type::<widget::Label>()
121122
.register_type::<ZIndex>()

crates/bevy_ui/src/node_bundles.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
33
use crate::{
44
widget::{Button, TextFlags, UiImageSize},
5-
BackgroundColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex,
5+
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
6+
ZIndex,
67
};
78
use bevy_ecs::bundle::Bundle;
89
use bevy_render::{
@@ -25,6 +26,8 @@ pub struct NodeBundle {
2526
pub style: Style,
2627
/// The background color, which serves as a "fill" for this node
2728
pub background_color: BackgroundColor,
29+
/// The color of the Node's border
30+
pub border_color: BorderColor,
2831
/// Whether this node should block interaction with lower nodes
2932
pub focus_policy: FocusPolicy,
3033
/// The transform of the node
@@ -50,6 +53,7 @@ impl Default for NodeBundle {
5053
NodeBundle {
5154
// Transparent background
5255
background_color: Color::NONE.into(),
56+
border_color: Color::NONE.into(),
5357
node: Default::default(),
5458
style: Default::default(),
5559
focus_policy: Default::default(),
@@ -225,6 +229,8 @@ pub struct ButtonBundle {
225229
///
226230
/// When combined with `UiImage`, tints the provided image.
227231
pub background_color: BackgroundColor,
232+
/// The color of the Node's border
233+
pub border_color: BorderColor,
228234
/// The image of the node
229235
pub image: UiImage,
230236
/// The transform of the node
@@ -252,6 +258,7 @@ impl Default for ButtonBundle {
252258
node: Default::default(),
253259
button: Default::default(),
254260
style: Default::default(),
261+
border_color: BorderColor(Color::NONE),
255262
interaction: Default::default(),
256263
background_color: Default::default(),
257264
image: Default::default(),

crates/bevy_ui/src/render/mod.rs

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ mod pipeline;
22
mod render_pass;
33

44
use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
5+
use bevy_hierarchy::Parent;
56
use bevy_render::{ExtractSchedule, Render};
67
#[cfg(feature = "bevy_text")]
78
use bevy_window::{PrimaryWindow, Window};
89
pub use pipeline::*;
910
pub use render_pass::*;
1011

11-
use crate::{prelude::UiCameraConfig, BackgroundColor, CalculatedClip, Node, UiImage, UiStack};
12+
use crate::{
13+
prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack,
14+
};
15+
use crate::{ContentSize, Style, Val};
1216
use bevy_app::prelude::*;
1317
use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped};
1418
use bevy_ecs::prelude::*;
@@ -78,6 +82,7 @@ pub fn build_ui_render(app: &mut App) {
7882
extract_default_ui_camera_view::<Camera2d>,
7983
extract_default_ui_camera_view::<Camera3d>,
8084
extract_uinodes.in_set(RenderUiSystem::ExtractNode),
85+
extract_uinode_borders.after(RenderUiSystem::ExtractNode),
8186
#[cfg(feature = "bevy_text")]
8287
extract_text_uinodes.after(RenderUiSystem::ExtractNode),
8388
),
@@ -161,6 +166,123 @@ pub struct ExtractedUiNodes {
161166
pub uinodes: Vec<ExtractedUiNode>,
162167
}
163168

169+
fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 {
170+
match value {
171+
Val::Auto => 0.,
172+
Val::Px(px) => px.max(0.),
173+
Val::Percent(percent) => (parent_width * percent / 100.).max(0.),
174+
Val::Vw(percent) => (viewport_size.x * percent / 100.).max(0.),
175+
Val::Vh(percent) => (viewport_size.y * percent / 100.).max(0.),
176+
Val::VMin(percent) => (viewport_size.min_element() * percent / 100.).max(0.),
177+
Val::VMax(percent) => (viewport_size.max_element() * percent / 100.).max(0.),
178+
}
179+
}
180+
181+
pub fn extract_uinode_borders(
182+
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
183+
windows: Extract<Query<&Window, With<PrimaryWindow>>>,
184+
ui_stack: Extract<Res<UiStack>>,
185+
uinode_query: Extract<
186+
Query<
187+
(
188+
&Node,
189+
&GlobalTransform,
190+
&Style,
191+
&BorderColor,
192+
Option<&Parent>,
193+
&ComputedVisibility,
194+
Option<&CalculatedClip>,
195+
),
196+
Without<ContentSize>,
197+
>,
198+
>,
199+
parent_node_query: Extract<Query<&Node, With<Parent>>>,
200+
) {
201+
let image = bevy_render::texture::DEFAULT_IMAGE_HANDLE.typed();
202+
203+
let viewport_size = windows
204+
.get_single()
205+
.map(|window| Vec2::new(window.resolution.width(), window.resolution.height()))
206+
.unwrap_or(Vec2::ZERO);
207+
208+
for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
209+
if let Ok((node, global_transform, style, border_color, parent, visibility, clip)) =
210+
uinode_query.get(*entity)
211+
{
212+
// Skip invisible borders
213+
if !visibility.is_visible()
214+
|| border_color.0.a() == 0.0
215+
|| node.size().x <= 0.
216+
|| node.size().y <= 0.
217+
{
218+
continue;
219+
}
220+
221+
// Both vertical and horizontal percentage border values are calculated based on the width of the parent node
222+
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
223+
let parent_width = parent
224+
.and_then(|parent| parent_node_query.get(parent.get()).ok())
225+
.map(|parent_node| parent_node.size().x)
226+
.unwrap_or(viewport_size.x);
227+
let left = resolve_border_thickness(style.border.left, parent_width, viewport_size);
228+
let right = resolve_border_thickness(style.border.right, parent_width, viewport_size);
229+
let top = resolve_border_thickness(style.border.top, parent_width, viewport_size);
230+
let bottom = resolve_border_thickness(style.border.bottom, parent_width, viewport_size);
231+
232+
// Calculate the border rects, ensuring no overlap.
233+
// The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value.
234+
let max = 0.5 * node.size();
235+
let min = -max;
236+
let inner_min = min + Vec2::new(left, top);
237+
let inner_max = (max - Vec2::new(right, bottom)).max(inner_min);
238+
let border_rects = [
239+
// Left border
240+
Rect {
241+
min,
242+
max: Vec2::new(inner_min.x, max.y),
243+
},
244+
// Right border
245+
Rect {
246+
min: Vec2::new(inner_max.x, min.y),
247+
max,
248+
},
249+
// Top border
250+
Rect {
251+
min: Vec2::new(inner_min.x, min.y),
252+
max: Vec2::new(inner_max.x, inner_min.y),
253+
},
254+
// Bottom border
255+
Rect {
256+
min: Vec2::new(inner_min.x, inner_max.y),
257+
max: Vec2::new(inner_max.x, max.y),
258+
},
259+
];
260+
261+
let transform = global_transform.compute_matrix();
262+
263+
for edge in border_rects {
264+
if edge.min.x < edge.max.x && edge.min.y < edge.max.y {
265+
extracted_uinodes.uinodes.push(ExtractedUiNode {
266+
stack_index,
267+
// This translates the uinode's transform to the center of the current border rectangle
268+
transform: transform * Mat4::from_translation(edge.center().extend(0.)),
269+
color: border_color.0,
270+
rect: Rect {
271+
max: edge.size(),
272+
..Default::default()
273+
},
274+
image: image.clone_weak(),
275+
atlas_size: None,
276+
clip: clip.map(|clip| clip.clip),
277+
flip_x: false,
278+
flip_y: false,
279+
});
280+
}
281+
}
282+
}
283+
}
284+
}
285+
164286
pub fn extract_uinodes(
165287
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
166288
images: Extract<Res<Assets<Image>>>,
@@ -177,6 +299,7 @@ pub fn extract_uinodes(
177299
>,
178300
) {
179301
extracted_uinodes.uinodes.clear();
302+
180303
for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
181304
if let Ok((uinode, transform, color, maybe_image, visibility, clip)) =
182305
uinode_query.get(*entity)

crates/bevy_ui/src/ui_node.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1563,6 +1563,27 @@ impl From<Color> for BackgroundColor {
15631563
}
15641564
}
15651565

1566+
/// The border color of the UI node.
1567+
#[derive(Component, Copy, Clone, Debug, Reflect, FromReflect)]
1568+
#[reflect(FromReflect, Component, Default)]
1569+
pub struct BorderColor(pub Color);
1570+
1571+
impl From<Color> for BorderColor {
1572+
fn from(color: Color) -> Self {
1573+
Self(color)
1574+
}
1575+
}
1576+
1577+
impl BorderColor {
1578+
pub const DEFAULT: Self = BorderColor(Color::WHITE);
1579+
}
1580+
1581+
impl Default for BorderColor {
1582+
fn default() -> Self {
1583+
Self::DEFAULT
1584+
}
1585+
}
1586+
15661587
/// The 2D texture displayed for this UI node
15671588
#[derive(Component, Clone, Debug, Reflect)]
15681589
#[reflect(Component, Default)]

examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ Example | Description
335335

336336
Example | Description
337337
--- | ---
338+
[Borders](../examples/ui/borders.rs) | Demonstrates how to create a node with a border
338339
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
339340
[CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout
340341
[Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text
@@ -350,6 +351,7 @@ Example | Description
350351
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI
351352
[UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI
352353
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
354+
[Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates
353355
[Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality.
354356

355357
## Window

0 commit comments

Comments
 (0)