Skip to content

Commit

Permalink
Ui Node Borders (#7795)
Browse files Browse the repository at this point in the history
# 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]>
  • Loading branch information
ickshonpe and nicoburns authored Jun 14, 2023
1 parent 2551ccb commit f7aa83a
Show file tree
Hide file tree
Showing 10 changed files with 547 additions and 4 deletions.
20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1762,6 +1762,16 @@ category = "Transforms"
wasm = true

# UI (User Interface)
[[example]]
name = "borders"
path = "examples/ui/borders.rs"

[package.metadata.example.borders]
name = "Borders"
description = "Demonstrates how to create a node with a border"
category = "UI (User Interface)"
wasm = true

[[example]]
name = "button"
path = "examples/ui/button.rs"
Expand Down Expand Up @@ -1923,6 +1933,16 @@ description = "Illustrates how to scale the UI"
category = "UI (User Interface)"
wasm = true

[[example]]
name = "viewport_debug"
path = "examples/ui/viewport_debug.rs"

[package.metadata.example.viewport_debug]
name = "Viewport Debug"
description = "An example for debugging viewport coordinates"
category = "UI (User Interface)"
wasm = true

# Window
[[example]]
name = "clear_color"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ impl Plugin for UiPlugin {
.register_type::<UiImageSize>()
.register_type::<UiRect>()
.register_type::<Val>()
.register_type::<BorderColor>()
.register_type::<widget::Button>()
.register_type::<widget::Label>()
.register_type::<ZIndex>()
Expand Down
9 changes: 8 additions & 1 deletion crates/bevy_ui/src/node_bundles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

use crate::{
widget::{Button, TextFlags, UiImageSize},
BackgroundColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex,
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
ZIndex,
};
use bevy_ecs::bundle::Bundle;
use bevy_render::{
Expand All @@ -25,6 +26,8 @@ pub struct NodeBundle {
pub style: Style,
/// The background color, which serves as a "fill" for this node
pub background_color: BackgroundColor,
/// The color of the Node's border
pub border_color: BorderColor,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The transform of the node
Expand All @@ -50,6 +53,7 @@ impl Default for NodeBundle {
NodeBundle {
// Transparent background
background_color: Color::NONE.into(),
border_color: Color::NONE.into(),
node: Default::default(),
style: Default::default(),
focus_policy: Default::default(),
Expand Down Expand Up @@ -225,6 +229,8 @@ pub struct ButtonBundle {
///
/// When combined with `UiImage`, tints the provided image.
pub background_color: BackgroundColor,
/// The color of the Node's border
pub border_color: BorderColor,
/// The image of the node
pub image: UiImage,
/// The transform of the node
Expand Down Expand Up @@ -252,6 +258,7 @@ impl Default for ButtonBundle {
node: Default::default(),
button: Default::default(),
style: Default::default(),
border_color: BorderColor(Color::NONE),
interaction: Default::default(),
background_color: Default::default(),
image: Default::default(),
Expand Down
125 changes: 124 additions & 1 deletion crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ mod pipeline;
mod render_pass;

use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
use bevy_hierarchy::Parent;
use bevy_render::{ExtractSchedule, Render};
#[cfg(feature = "bevy_text")]
use bevy_window::{PrimaryWindow, Window};
pub use pipeline::*;
pub use render_pass::*;

use crate::{prelude::UiCameraConfig, BackgroundColor, CalculatedClip, Node, UiImage, UiStack};
use crate::{
prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack,
};
use crate::{ContentSize, Style, Val};
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped};
use bevy_ecs::prelude::*;
Expand Down Expand Up @@ -78,6 +82,7 @@ pub fn build_ui_render(app: &mut App) {
extract_default_ui_camera_view::<Camera2d>,
extract_default_ui_camera_view::<Camera3d>,
extract_uinodes.in_set(RenderUiSystem::ExtractNode),
extract_uinode_borders.after(RenderUiSystem::ExtractNode),
#[cfg(feature = "bevy_text")]
extract_text_uinodes.after(RenderUiSystem::ExtractNode),
),
Expand Down Expand Up @@ -161,6 +166,123 @@ pub struct ExtractedUiNodes {
pub uinodes: Vec<ExtractedUiNode>,
}

fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 {
match value {
Val::Auto => 0.,
Val::Px(px) => px.max(0.),
Val::Percent(percent) => (parent_width * percent / 100.).max(0.),
Val::Vw(percent) => (viewport_size.x * percent / 100.).max(0.),
Val::Vh(percent) => (viewport_size.y * percent / 100.).max(0.),
Val::VMin(percent) => (viewport_size.min_element() * percent / 100.).max(0.),
Val::VMax(percent) => (viewport_size.max_element() * percent / 100.).max(0.),
}
}

pub fn extract_uinode_borders(
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
windows: Extract<Query<&Window, With<PrimaryWindow>>>,
ui_stack: Extract<Res<UiStack>>,
uinode_query: Extract<
Query<
(
&Node,
&GlobalTransform,
&Style,
&BorderColor,
Option<&Parent>,
&ComputedVisibility,
Option<&CalculatedClip>,
),
Without<ContentSize>,
>,
>,
parent_node_query: Extract<Query<&Node, With<Parent>>>,
) {
let image = bevy_render::texture::DEFAULT_IMAGE_HANDLE.typed();

let viewport_size = windows
.get_single()
.map(|window| Vec2::new(window.resolution.width(), window.resolution.height()))
.unwrap_or(Vec2::ZERO);

for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
if let Ok((node, global_transform, style, border_color, parent, visibility, clip)) =
uinode_query.get(*entity)
{
// Skip invisible borders
if !visibility.is_visible()
|| border_color.0.a() == 0.0
|| node.size().x <= 0.
|| node.size().y <= 0.
{
continue;
}

// Both vertical and horizontal percentage border values are calculated based on the width of the parent node
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
let parent_width = parent
.and_then(|parent| parent_node_query.get(parent.get()).ok())
.map(|parent_node| parent_node.size().x)
.unwrap_or(viewport_size.x);
let left = resolve_border_thickness(style.border.left, parent_width, viewport_size);
let right = resolve_border_thickness(style.border.right, parent_width, viewport_size);
let top = resolve_border_thickness(style.border.top, parent_width, viewport_size);
let bottom = resolve_border_thickness(style.border.bottom, parent_width, viewport_size);

// Calculate the border rects, ensuring no overlap.
// 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.
let max = 0.5 * node.size();
let min = -max;
let inner_min = min + Vec2::new(left, top);
let inner_max = (max - Vec2::new(right, bottom)).max(inner_min);
let border_rects = [
// Left border
Rect {
min,
max: Vec2::new(inner_min.x, max.y),
},
// Right border
Rect {
min: Vec2::new(inner_max.x, min.y),
max,
},
// Top border
Rect {
min: Vec2::new(inner_min.x, min.y),
max: Vec2::new(inner_max.x, inner_min.y),
},
// Bottom border
Rect {
min: Vec2::new(inner_min.x, inner_max.y),
max: Vec2::new(inner_max.x, max.y),
},
];

let transform = global_transform.compute_matrix();

for edge in border_rects {
if edge.min.x < edge.max.x && edge.min.y < edge.max.y {
extracted_uinodes.uinodes.push(ExtractedUiNode {
stack_index,
// This translates the uinode's transform to the center of the current border rectangle
transform: transform * Mat4::from_translation(edge.center().extend(0.)),
color: border_color.0,
rect: Rect {
max: edge.size(),
..Default::default()
},
image: image.clone_weak(),
atlas_size: None,
clip: clip.map(|clip| clip.clip),
flip_x: false,
flip_y: false,
});
}
}
}
}
}

pub fn extract_uinodes(
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
images: Extract<Res<Assets<Image>>>,
Expand All @@ -177,6 +299,7 @@ pub fn extract_uinodes(
>,
) {
extracted_uinodes.uinodes.clear();

for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
if let Ok((uinode, transform, color, maybe_image, visibility, clip)) =
uinode_query.get(*entity)
Expand Down
21 changes: 21 additions & 0 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,27 @@ impl From<Color> for BackgroundColor {
}
}

/// The border color of the UI node.
#[derive(Component, Copy, Clone, Debug, Reflect, FromReflect)]
#[reflect(FromReflect, Component, Default)]
pub struct BorderColor(pub Color);

impl From<Color> for BorderColor {
fn from(color: Color) -> Self {
Self(color)
}
}

impl BorderColor {
pub const DEFAULT: Self = BorderColor(Color::WHITE);
}

impl Default for BorderColor {
fn default() -> Self {
Self::DEFAULT
}
}

/// The 2D texture displayed for this UI node
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Default)]
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ Example | Description

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

## Window
Expand Down
Loading

0 comments on commit f7aa83a

Please sign in to comment.