Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ui Node Borders #7795

Merged
merged 34 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
46b7e5e
changes:
ickshonpe Feb 23, 2023
f91e4f3
fix up borders example
ickshonpe Feb 23, 2023
b485e17
changes:
ickshonpe Feb 23, 2023
de7c9d8
lint fix
ickshonpe Feb 23, 2023
caa72fd
improvements to example
ickshonpe Feb 23, 2023
d77ebe9
removed duplicate child insertion from example
ickshonpe Feb 23, 2023
df56157
force inner_min <= inner_max to prevent overlap of opposite edge bord…
ickshonpe Feb 23, 2023
ba63de0
cargo fmt --all
ickshonpe Feb 23, 2023
f3f9a16
move the image handle conversion out of the loop in the extraction fu…
ickshonpe Feb 24, 2023
457648c
Added borders to the buttons in the `many_buttons` stress test example.
ickshonpe Feb 24, 2023
e2b214c
move `compute_matrix()` out of loop
ickshonpe Feb 24, 2023
51e8088
fmt
ickshonpe Feb 24, 2023
6630107
changes:
ickshonpe Mar 1, 2023
0e7fdff
changes:
ickshonpe Mar 3, 2023
d806d33
changes:
ickshonpe Mar 11, 2023
4202bde
Set the default `ButtonBundle` `border_color` to `Color::NONE`
ickshonpe Mar 11, 2023
9eb6262
Merge branch 'main' into borders
ickshonpe Apr 14, 2023
98c88b6
format and build pages
ickshonpe Apr 14, 2023
29e2205
fix for explicit_iter_loop lint
ickshonpe Apr 14, 2023
ac9278f
Fixed `resolve_border_thickness` so its uses the `.max` function not …
ickshonpe Apr 14, 2023
f496b54
Update examples/ui/borders.rs
ickshonpe Apr 18, 2023
e886d7b
Merge branch 'main' into borders
ickshonpe Apr 18, 2023
4f0e38b
changes:
ickshonpe Apr 18, 2023
5dcae37
Merge branch 'main' into borders
ickshonpe May 31, 2023
d045842
Fixed some renamings after merge.
ickshonpe May 31, 2023
1550763
cargo fmt --all
ickshonpe May 31, 2023
ad17940
some minor fixes:
ickshonpe May 31, 2023
f8878cf
Fixed unused imports.
ickshonpe May 31, 2023
e3dcf7c
fixed viewport_debug example
ickshonpe May 31, 2023
af4c565
Added a comment explaining that percentage border values are calculat…
ickshonpe Jun 9, 2023
fbf687e
Replaced logical size calculations with `WindowResolution::width` and…
ickshonpe Jun 9, 2023
7adbf2c
Added `FromReflect` derive, and `reflect(FromReflect, Default)` to `B…
ickshonpe Jun 11, 2023
d2bc8c6
Added a comment explaning the purpose of the translation inside the i…
ickshonpe Jun 11, 2023
b34f363
cargo fmt --all
ickshonpe Jun 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -115,6 +115,7 @@ impl Plugin for UiPlugin {
.register_type::<UiImage>()
.register_type::<UiImageSize>()
.register_type::<Val>()
.register_type::<BorderColor>()
.register_type::<widget::Button>()
.register_type::<widget::Label>()
.add_systems(
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
127 changes: 126 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,125 @@ 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.),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see a comment here about why we decided to use parent width specifically. IIRC it's consistent with CSS, but it's arbitrary enough that it's helpful to call out.

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.physical_width() as f32,
ickshonpe marked this conversation as resolved.
Show resolved Hide resolved
window.resolution.physical_height() as f32,
) / window.resolution.scale_factor() as f32
})
.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;
}

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);
ickshonpe marked this conversation as resolved.
Show resolved Hide resolved

// 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is quite unclear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just the regular transform of the uinode. In the loop it's translated to the center of each rectangle that constitutes the border. It feels to me like it might be confusing to call it anything else.


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,
transform: transform * Mat4::from_translation(edge.center().extend(0.)),
ickshonpe marked this conversation as resolved.
Show resolved Hide resolved
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 +301,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)]
#[reflect(Component)]
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