Add border radius to UI nodes (adopted) (#12500)
# Objective Implements border radius for UI nodes. Adopted from #8973, but excludes shadows. ## Solution - Add a component `BorderRadius` which contains a radius value for each corner of the UI node. - Use a fragment shader to generate the rounded corners using a signed distance function. <img width="50%" src="https://github.com/bevyengine/bevy/assets/26204416/16b2ba95-e274-4ce7-adb2-34cc41a776a5"></img> ## Changelog - `BorderRadius`: New component that holds the border radius values. - `NodeBundle` & `ButtonBundle`: Added a `border_radius: BorderRadius` field. - `extract_uinode_borders`: Stripped down, most of the work is done in the shader now. Borders are no longer assembled from multiple rects, instead the shader uses a signed distance function to draw the border. - `UiVertex`: Added size, border and radius fields. - `UiPipeline`: Added three vertex attributes to the vertex buffer layout, to accept the UI node's size, border thickness and border radius. - Examples: Added rounded corners to the UI element in the `button` example, and a `rounded_borders` example. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Zachary Harrold <zac@harrold.com.au> Co-authored-by: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com>
This commit is contained in:
parent
7c7d1e8a64
commit
e7a31d000e
11
Cargo.toml
11
Cargo.toml
@ -2340,6 +2340,17 @@ description = "Demonstrates how to create a node with a border"
|
|||||||
category = "UI (User Interface)"
|
category = "UI (User Interface)"
|
||||||
wasm = true
|
wasm = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "rounded_borders"
|
||||||
|
path = "examples/ui/rounded_borders.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[package.metadata.example.rounded_borders]
|
||||||
|
name = "Rounded Borders"
|
||||||
|
description = "Demonstrates how to create a node with a rounded border"
|
||||||
|
category = "UI (User Interface)"
|
||||||
|
wasm = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "button"
|
name = "button"
|
||||||
path = "examples/ui/button.rs"
|
path = "examples/ui/button.rs"
|
||||||
|
@ -112,6 +112,7 @@ impl Plugin for UiPlugin {
|
|||||||
.register_type::<UiRect>()
|
.register_type::<UiRect>()
|
||||||
.register_type::<UiScale>()
|
.register_type::<UiScale>()
|
||||||
.register_type::<BorderColor>()
|
.register_type::<BorderColor>()
|
||||||
|
.register_type::<BorderRadius>()
|
||||||
.register_type::<widget::Button>()
|
.register_type::<widget::Button>()
|
||||||
.register_type::<widget::Label>()
|
.register_type::<widget::Label>()
|
||||||
.register_type::<ZIndex>()
|
.register_type::<ZIndex>()
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
use crate::widget::TextFlags;
|
use crate::widget::TextFlags;
|
||||||
use crate::{
|
use crate::{
|
||||||
widget::{Button, UiImageSize},
|
widget::{Button, UiImageSize},
|
||||||
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
|
BackgroundColor, BorderColor, BorderRadius, ContentSize, FocusPolicy, Interaction, Node, Style,
|
||||||
UiMaterial, ZIndex,
|
UiImage, UiMaterial, ZIndex,
|
||||||
};
|
};
|
||||||
use bevy_asset::Handle;
|
use bevy_asset::Handle;
|
||||||
use bevy_color::Color;
|
use bevy_color::Color;
|
||||||
@ -34,6 +34,8 @@ pub struct NodeBundle {
|
|||||||
pub background_color: BackgroundColor,
|
pub background_color: BackgroundColor,
|
||||||
/// The color of the Node's border
|
/// The color of the Node's border
|
||||||
pub border_color: BorderColor,
|
pub border_color: BorderColor,
|
||||||
|
/// The border radius of the node
|
||||||
|
pub border_radius: BorderRadius,
|
||||||
/// Whether this node should block interaction with lower nodes
|
/// Whether this node should block interaction with lower nodes
|
||||||
pub focus_policy: FocusPolicy,
|
pub focus_policy: FocusPolicy,
|
||||||
/// The transform of the node
|
/// The transform of the node
|
||||||
@ -62,6 +64,7 @@ impl Default for NodeBundle {
|
|||||||
// Transparent background
|
// Transparent background
|
||||||
background_color: Color::NONE.into(),
|
background_color: Color::NONE.into(),
|
||||||
border_color: Color::NONE.into(),
|
border_color: Color::NONE.into(),
|
||||||
|
border_radius: BorderRadius::default(),
|
||||||
node: Default::default(),
|
node: Default::default(),
|
||||||
style: Default::default(),
|
style: Default::default(),
|
||||||
focus_policy: Default::default(),
|
focus_policy: Default::default(),
|
||||||
@ -314,6 +317,8 @@ pub struct ButtonBundle {
|
|||||||
pub focus_policy: FocusPolicy,
|
pub focus_policy: FocusPolicy,
|
||||||
/// The color of the Node's border
|
/// The color of the Node's border
|
||||||
pub border_color: BorderColor,
|
pub border_color: BorderColor,
|
||||||
|
/// The border radius of the node
|
||||||
|
pub border_radius: BorderRadius,
|
||||||
/// The image of the node
|
/// The image of the node
|
||||||
pub image: UiImage,
|
pub image: UiImage,
|
||||||
/// The transform of the node
|
/// The transform of the node
|
||||||
@ -344,6 +349,7 @@ impl Default for ButtonBundle {
|
|||||||
interaction: Default::default(),
|
interaction: Default::default(),
|
||||||
focus_policy: FocusPolicy::Block,
|
focus_policy: FocusPolicy::Block,
|
||||||
border_color: BorderColor(Color::NONE),
|
border_color: BorderColor(Color::NONE),
|
||||||
|
border_radius: BorderRadius::default(),
|
||||||
image: Default::default(),
|
image: Default::default(),
|
||||||
transform: Default::default(),
|
transform: Default::default(),
|
||||||
global_transform: Default::default(),
|
global_transform: Default::default(),
|
||||||
|
@ -9,21 +9,23 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
|
|||||||
use bevy_hierarchy::Parent;
|
use bevy_hierarchy::Parent;
|
||||||
use bevy_render::{render_phase::PhaseItem, view::ViewVisibility, ExtractSchedule, Render};
|
use bevy_render::{render_phase::PhaseItem, view::ViewVisibility, ExtractSchedule, Render};
|
||||||
use bevy_sprite::{SpriteAssetEvents, TextureAtlas};
|
use bevy_sprite::{SpriteAssetEvents, TextureAtlas};
|
||||||
|
use bevy_window::{PrimaryWindow, Window};
|
||||||
pub use pipeline::*;
|
pub use pipeline::*;
|
||||||
pub use render_pass::*;
|
pub use render_pass::*;
|
||||||
pub use ui_material_pipeline::*;
|
pub use ui_material_pipeline::*;
|
||||||
|
|
||||||
use crate::graph::{NodeUi, SubGraphUi};
|
use crate::graph::{NodeUi, SubGraphUi};
|
||||||
use crate::{
|
use crate::{
|
||||||
texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, CalculatedClip,
|
texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, BorderRadius,
|
||||||
ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val,
|
CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage,
|
||||||
|
UiScale, Val,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
|
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
|
||||||
use bevy_ecs::entity::EntityHashMap;
|
use bevy_ecs::entity::EntityHashMap;
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4Swizzles};
|
use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
camera::Camera,
|
camera::Camera,
|
||||||
render_asset::RenderAssets,
|
render_asset::RenderAssets,
|
||||||
@ -141,6 +143,14 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph {
|
|||||||
ui_graph
|
ui_graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The type of UI node.
|
||||||
|
/// This is used to determine how to render the UI node.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum NodeType {
|
||||||
|
Rect,
|
||||||
|
Border,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ExtractedUiNode {
|
pub struct ExtractedUiNode {
|
||||||
pub stack_index: u32,
|
pub stack_index: u32,
|
||||||
pub transform: Mat4,
|
pub transform: Mat4,
|
||||||
@ -155,6 +165,13 @@ pub struct ExtractedUiNode {
|
|||||||
// it is defaulted to a single camera if only one exists.
|
// it is defaulted to a single camera if only one exists.
|
||||||
// Nodes with ambiguous camera will be ignored.
|
// Nodes with ambiguous camera will be ignored.
|
||||||
pub camera_entity: Entity,
|
pub camera_entity: Entity,
|
||||||
|
/// Border radius of the UI node.
|
||||||
|
/// Ordering: top left, top right, bottom right, bottom left.
|
||||||
|
pub border_radius: [f32; 4],
|
||||||
|
/// Border thickness of the UI node.
|
||||||
|
/// Ordering: left, top, right, bottom.
|
||||||
|
pub border: [f32; 4],
|
||||||
|
pub node_type: NodeType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
@ -164,7 +181,9 @@ pub struct ExtractedUiNodes {
|
|||||||
|
|
||||||
pub fn extract_uinode_background_colors(
|
pub fn extract_uinode_background_colors(
|
||||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||||
|
windows: Extract<Query<&Window, With<PrimaryWindow>>>,
|
||||||
default_ui_camera: Extract<DefaultUiCamera>,
|
default_ui_camera: Extract<DefaultUiCamera>,
|
||||||
|
ui_scale: Extract<Res<UiScale>>,
|
||||||
uinode_query: Extract<
|
uinode_query: Extract<
|
||||||
Query<(
|
Query<(
|
||||||
Entity,
|
Entity,
|
||||||
@ -174,11 +193,26 @@ pub fn extract_uinode_background_colors(
|
|||||||
Option<&CalculatedClip>,
|
Option<&CalculatedClip>,
|
||||||
Option<&TargetCamera>,
|
Option<&TargetCamera>,
|
||||||
&BackgroundColor,
|
&BackgroundColor,
|
||||||
|
&BorderRadius,
|
||||||
)>,
|
)>,
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
for (entity, uinode, transform, view_visibility, clip, camera, background_color) in
|
let viewport_size = windows
|
||||||
&uinode_query
|
.get_single()
|
||||||
|
.map(|window| window.resolution.size())
|
||||||
|
.unwrap_or(Vec2::ZERO)
|
||||||
|
* ui_scale.0;
|
||||||
|
|
||||||
|
for (
|
||||||
|
entity,
|
||||||
|
uinode,
|
||||||
|
transform,
|
||||||
|
view_visibility,
|
||||||
|
clip,
|
||||||
|
camera,
|
||||||
|
background_color,
|
||||||
|
border_radius,
|
||||||
|
) in &uinode_query
|
||||||
{
|
{
|
||||||
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
||||||
else {
|
else {
|
||||||
@ -190,6 +224,9 @@ pub fn extract_uinode_background_colors(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let border_radius =
|
||||||
|
resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0);
|
||||||
|
|
||||||
extracted_uinodes.uinodes.insert(
|
extracted_uinodes.uinodes.insert(
|
||||||
entity,
|
entity,
|
||||||
ExtractedUiNode {
|
ExtractedUiNode {
|
||||||
@ -206,6 +243,9 @@ pub fn extract_uinode_background_colors(
|
|||||||
flip_x: false,
|
flip_x: false,
|
||||||
flip_y: false,
|
flip_y: false,
|
||||||
camera_entity,
|
camera_entity,
|
||||||
|
border: [0.; 4],
|
||||||
|
border_radius,
|
||||||
|
node_type: NodeType::Rect,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -214,7 +254,9 @@ pub fn extract_uinode_background_colors(
|
|||||||
pub fn extract_uinode_images(
|
pub fn extract_uinode_images(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||||
|
windows: Extract<Query<&Window, With<PrimaryWindow>>>,
|
||||||
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
|
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
|
||||||
|
ui_scale: Extract<Res<UiScale>>,
|
||||||
default_ui_camera: Extract<DefaultUiCamera>,
|
default_ui_camera: Extract<DefaultUiCamera>,
|
||||||
uinode_query: Extract<
|
uinode_query: Extract<
|
||||||
Query<(
|
Query<(
|
||||||
@ -226,10 +268,19 @@ pub fn extract_uinode_images(
|
|||||||
&UiImage,
|
&UiImage,
|
||||||
Option<&TextureAtlas>,
|
Option<&TextureAtlas>,
|
||||||
Option<&ComputedTextureSlices>,
|
Option<&ComputedTextureSlices>,
|
||||||
|
&BorderRadius,
|
||||||
)>,
|
)>,
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
for (uinode, transform, view_visibility, clip, camera, image, atlas, slices) in &uinode_query {
|
let viewport_size = windows
|
||||||
|
.get_single()
|
||||||
|
.map(|window| window.resolution.size())
|
||||||
|
.unwrap_or(Vec2::ZERO)
|
||||||
|
* ui_scale.0;
|
||||||
|
|
||||||
|
for (uinode, transform, view_visibility, clip, camera, image, atlas, slices, border_radius) in
|
||||||
|
&uinode_query
|
||||||
|
{
|
||||||
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
@ -272,6 +323,9 @@ pub fn extract_uinode_images(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let border_radius =
|
||||||
|
resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0);
|
||||||
|
|
||||||
extracted_uinodes.uinodes.insert(
|
extracted_uinodes.uinodes.insert(
|
||||||
commands.spawn_empty().id(),
|
commands.spawn_empty().id(),
|
||||||
ExtractedUiNode {
|
ExtractedUiNode {
|
||||||
@ -285,6 +339,9 @@ pub fn extract_uinode_images(
|
|||||||
flip_x: image.flip_x,
|
flip_x: image.flip_x,
|
||||||
flip_y: image.flip_y,
|
flip_y: image.flip_y,
|
||||||
camera_entity,
|
camera_entity,
|
||||||
|
border: [0.; 4],
|
||||||
|
border_radius,
|
||||||
|
node_type: NodeType::Rect,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -302,6 +359,55 @@ pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_border_radius(
|
||||||
|
&values: &BorderRadius,
|
||||||
|
node_size: Vec2,
|
||||||
|
viewport_size: Vec2,
|
||||||
|
ui_scale: f32,
|
||||||
|
) -> [f32; 4] {
|
||||||
|
let max_radius = 0.5 * node_size.min_element() * ui_scale;
|
||||||
|
[
|
||||||
|
values.top_left,
|
||||||
|
values.top_right,
|
||||||
|
values.bottom_right,
|
||||||
|
values.bottom_left,
|
||||||
|
]
|
||||||
|
.map(|value| {
|
||||||
|
match value {
|
||||||
|
Val::Auto => 0.,
|
||||||
|
Val::Px(px) => ui_scale * px,
|
||||||
|
Val::Percent(percent) => node_size.min_element() * percent / 100.,
|
||||||
|
Val::Vw(percent) => viewport_size.x * percent / 100.,
|
||||||
|
Val::Vh(percent) => viewport_size.y * percent / 100.,
|
||||||
|
Val::VMin(percent) => viewport_size.min_element() * percent / 100.,
|
||||||
|
Val::VMax(percent) => viewport_size.max_element() * percent / 100.,
|
||||||
|
}
|
||||||
|
.clamp(0., max_radius)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 {
|
||||||
|
let s = 0.5 * size + offset;
|
||||||
|
let sm = s.x.min(s.y);
|
||||||
|
r.min(sm)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn clamp_radius(
|
||||||
|
[top_left, top_right, bottom_right, bottom_left]: [f32; 4],
|
||||||
|
size: Vec2,
|
||||||
|
border: Vec4,
|
||||||
|
) -> [f32; 4] {
|
||||||
|
let s = size - border.xy() - border.zw();
|
||||||
|
[
|
||||||
|
clamp_corner(top_left, s, border.xy()),
|
||||||
|
clamp_corner(top_right, s, border.zy()),
|
||||||
|
clamp_corner(bottom_right, s, border.zw()),
|
||||||
|
clamp_corner(bottom_left, s, border.xw()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn extract_uinode_borders(
|
pub fn extract_uinode_borders(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||||
@ -319,6 +425,7 @@ pub fn extract_uinode_borders(
|
|||||||
Option<&Parent>,
|
Option<&Parent>,
|
||||||
&Style,
|
&Style,
|
||||||
&BorderColor,
|
&BorderColor,
|
||||||
|
&BorderRadius,
|
||||||
),
|
),
|
||||||
Without<ContentSize>,
|
Without<ContentSize>,
|
||||||
>,
|
>,
|
||||||
@ -327,8 +434,17 @@ pub fn extract_uinode_borders(
|
|||||||
) {
|
) {
|
||||||
let image = AssetId::<Image>::default();
|
let image = AssetId::<Image>::default();
|
||||||
|
|
||||||
for (node, global_transform, view_visibility, clip, camera, parent, style, border_color) in
|
for (
|
||||||
&uinode_query
|
node,
|
||||||
|
global_transform,
|
||||||
|
view_visibility,
|
||||||
|
clip,
|
||||||
|
camera,
|
||||||
|
parent,
|
||||||
|
style,
|
||||||
|
border_color,
|
||||||
|
border_radius,
|
||||||
|
) in &uinode_query
|
||||||
{
|
{
|
||||||
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
|
||||||
else {
|
else {
|
||||||
@ -368,60 +484,40 @@ pub fn extract_uinode_borders(
|
|||||||
let bottom =
|
let bottom =
|
||||||
resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size);
|
resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size);
|
||||||
|
|
||||||
// Calculate the border rects, ensuring no overlap.
|
let border = [left, top, right, bottom];
|
||||||
// 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 border_radius = resolve_border_radius(
|
||||||
|
border_radius,
|
||||||
|
node.size(),
|
||||||
|
ui_logical_viewport_size,
|
||||||
|
ui_scale.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let border_radius = clamp_radius(border_radius, node.size(), border.into());
|
||||||
let transform = global_transform.compute_matrix();
|
let transform = global_transform.compute_matrix();
|
||||||
|
|
||||||
for edge in border_rects {
|
extracted_uinodes.uinodes.insert(
|
||||||
if edge.min.x < edge.max.x && edge.min.y < edge.max.y {
|
commands.spawn_empty().id(),
|
||||||
extracted_uinodes.uinodes.insert(
|
ExtractedUiNode {
|
||||||
commands.spawn_empty().id(),
|
stack_index: node.stack_index,
|
||||||
ExtractedUiNode {
|
// This translates the uinode's transform to the center of the current border rectangle
|
||||||
stack_index: node.stack_index,
|
transform,
|
||||||
// This translates the uinode's transform to the center of the current border rectangle
|
color: border_color.0.into(),
|
||||||
transform: transform * Mat4::from_translation(edge.center().extend(0.)),
|
rect: Rect {
|
||||||
color: border_color.0.into(),
|
max: node.size(),
|
||||||
rect: Rect {
|
..Default::default()
|
||||||
max: edge.size(),
|
},
|
||||||
..Default::default()
|
image,
|
||||||
},
|
atlas_size: None,
|
||||||
image,
|
clip: clip.map(|clip| clip.clip),
|
||||||
atlas_size: None,
|
flip_x: false,
|
||||||
clip: clip.map(|clip| clip.clip),
|
flip_y: false,
|
||||||
flip_x: false,
|
camera_entity,
|
||||||
flip_y: false,
|
border_radius,
|
||||||
camera_entity,
|
border,
|
||||||
},
|
node_type: NodeType::Border,
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,7 +586,6 @@ pub fn extract_uinode_outlines(
|
|||||||
];
|
];
|
||||||
|
|
||||||
let transform = global_transform.compute_matrix();
|
let transform = global_transform.compute_matrix();
|
||||||
|
|
||||||
for edge in outline_edges {
|
for edge in outline_edges {
|
||||||
if edge.min.x < edge.max.x && edge.min.y < edge.max.y {
|
if edge.min.x < edge.max.x && edge.min.y < edge.max.y {
|
||||||
extracted_uinodes.uinodes.insert(
|
extracted_uinodes.uinodes.insert(
|
||||||
@ -510,6 +605,9 @@ pub fn extract_uinode_outlines(
|
|||||||
flip_x: false,
|
flip_x: false,
|
||||||
flip_y: false,
|
flip_y: false,
|
||||||
camera_entity,
|
camera_entity,
|
||||||
|
border: [0.; 4],
|
||||||
|
border_radius: [0.; 4],
|
||||||
|
node_type: NodeType::Rect,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -680,6 +778,9 @@ pub fn extract_uinode_text(
|
|||||||
flip_x: false,
|
flip_x: false,
|
||||||
flip_y: false,
|
flip_y: false,
|
||||||
camera_entity,
|
camera_entity,
|
||||||
|
border: [0.; 4],
|
||||||
|
border_radius: [0.; 4],
|
||||||
|
node_type: NodeType::Rect,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -692,12 +793,23 @@ struct UiVertex {
|
|||||||
pub position: [f32; 3],
|
pub position: [f32; 3],
|
||||||
pub uv: [f32; 2],
|
pub uv: [f32; 2],
|
||||||
pub color: [f32; 4],
|
pub color: [f32; 4],
|
||||||
pub mode: u32,
|
/// Shader flags to determine how to render the UI node.
|
||||||
|
/// See [`shader_flags`] for possible values.
|
||||||
|
pub flags: u32,
|
||||||
|
/// Border radius of the UI node.
|
||||||
|
/// Ordering: top left, top right, bottom right, bottom left.
|
||||||
|
pub radius: [f32; 4],
|
||||||
|
/// Border thickness of the UI node.
|
||||||
|
/// Ordering: left, top, right, bottom.
|
||||||
|
pub border: [f32; 4],
|
||||||
|
/// Size of the UI node.
|
||||||
|
pub size: [f32; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct UiMeta {
|
pub struct UiMeta {
|
||||||
vertices: BufferVec<UiVertex>,
|
vertices: BufferVec<UiVertex>,
|
||||||
|
indices: BufferVec<u32>,
|
||||||
view_bind_group: Option<BindGroup>,
|
view_bind_group: Option<BindGroup>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,6 +817,7 @@ impl Default for UiMeta {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
vertices: BufferVec::new(BufferUsages::VERTEX),
|
vertices: BufferVec::new(BufferUsages::VERTEX),
|
||||||
|
indices: BufferVec::new(BufferUsages::INDEX),
|
||||||
view_bind_group: None,
|
view_bind_group: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -726,8 +839,14 @@ pub struct UiBatch {
|
|||||||
pub camera: Entity,
|
pub camera: Entity,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEXTURED_QUAD: u32 = 0;
|
/// The values here should match the values for the constants in `ui.wgsl`
|
||||||
const UNTEXTURED_QUAD: u32 = 1;
|
pub mod shader_flags {
|
||||||
|
pub const UNTEXTURED: u32 = 0;
|
||||||
|
pub const TEXTURED: u32 = 1;
|
||||||
|
/// Ordering: top left, top right, bottom right, bottom left.
|
||||||
|
pub const CORNERS: [u32; 4] = [0, 2, 2 | 4, 4];
|
||||||
|
pub const BORDER: u32 = 8;
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn queue_uinodes(
|
pub fn queue_uinodes(
|
||||||
@ -802,14 +921,17 @@ pub fn prepare_uinodes(
|
|||||||
let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len);
|
let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len);
|
||||||
|
|
||||||
ui_meta.vertices.clear();
|
ui_meta.vertices.clear();
|
||||||
|
ui_meta.indices.clear();
|
||||||
ui_meta.view_bind_group = Some(render_device.create_bind_group(
|
ui_meta.view_bind_group = Some(render_device.create_bind_group(
|
||||||
"ui_view_bind_group",
|
"ui_view_bind_group",
|
||||||
&ui_pipeline.view_layout,
|
&ui_pipeline.view_layout,
|
||||||
&BindGroupEntries::single(view_binding),
|
&BindGroupEntries::single(view_binding),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Vertex buffer index
|
// Buffer indexes
|
||||||
let mut index = 0;
|
let mut vertices_index = 0;
|
||||||
|
let mut indices_index = 0;
|
||||||
|
|
||||||
for mut ui_phase in &mut phases {
|
for mut ui_phase in &mut phases {
|
||||||
let mut batch_item_index = 0;
|
let mut batch_item_index = 0;
|
||||||
let mut batch_image_handle = AssetId::invalid();
|
let mut batch_image_handle = AssetId::invalid();
|
||||||
@ -832,7 +954,7 @@ pub fn prepare_uinodes(
|
|||||||
batch_image_handle = extracted_uinode.image;
|
batch_image_handle = extracted_uinode.image;
|
||||||
|
|
||||||
let new_batch = UiBatch {
|
let new_batch = UiBatch {
|
||||||
range: index..index,
|
range: vertices_index..vertices_index,
|
||||||
image: extracted_uinode.image,
|
image: extracted_uinode.image,
|
||||||
camera: extracted_uinode.camera_entity,
|
camera: extracted_uinode.camera_entity,
|
||||||
};
|
};
|
||||||
@ -882,10 +1004,10 @@ pub fn prepare_uinodes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mode = if extracted_uinode.image != AssetId::default() {
|
let mut flags = if extracted_uinode.image != AssetId::default() {
|
||||||
TEXTURED_QUAD
|
shader_flags::TEXTURED
|
||||||
} else {
|
} else {
|
||||||
UNTEXTURED_QUAD
|
shader_flags::UNTEXTURED
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut uinode_rect = extracted_uinode.rect;
|
let mut uinode_rect = extracted_uinode.rect;
|
||||||
@ -946,7 +1068,7 @@ pub fn prepare_uinodes(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let uvs = if mode == UNTEXTURED_QUAD {
|
let uvs = if flags == shader_flags::UNTEXTURED {
|
||||||
[Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]
|
[Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]
|
||||||
} else {
|
} else {
|
||||||
let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
|
let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
|
||||||
@ -986,16 +1108,30 @@ pub fn prepare_uinodes(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let color = extracted_uinode.color.to_f32_array();
|
let color = extracted_uinode.color.to_f32_array();
|
||||||
for i in QUAD_INDICES {
|
if extracted_uinode.node_type == NodeType::Border {
|
||||||
|
flags |= shader_flags::BORDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..4 {
|
||||||
ui_meta.vertices.push(UiVertex {
|
ui_meta.vertices.push(UiVertex {
|
||||||
position: positions_clipped[i].into(),
|
position: positions_clipped[i].into(),
|
||||||
uv: uvs[i].into(),
|
uv: uvs[i].into(),
|
||||||
color,
|
color,
|
||||||
mode,
|
flags: flags | shader_flags::CORNERS[i],
|
||||||
|
radius: extracted_uinode.border_radius,
|
||||||
|
border: extracted_uinode.border,
|
||||||
|
size: transformed_rect_size.xy().into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
index += QUAD_INDICES.len() as u32;
|
|
||||||
existing_batch.unwrap().1.range.end = index;
|
for &i in &QUAD_INDICES {
|
||||||
|
ui_meta.indices.push(indices_index + i as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
vertices_index += 6;
|
||||||
|
indices_index += 4;
|
||||||
|
|
||||||
|
existing_batch.unwrap().1.range.end = vertices_index;
|
||||||
ui_phase.items[batch_item_index].batch_range_mut().end += 1;
|
ui_phase.items[batch_item_index].batch_range_mut().end += 1;
|
||||||
} else {
|
} else {
|
||||||
batch_image_handle = AssetId::invalid();
|
batch_image_handle = AssetId::invalid();
|
||||||
@ -1003,6 +1139,7 @@ pub fn prepare_uinodes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui_meta.vertices.write_buffer(&render_device, &render_queue);
|
ui_meta.vertices.write_buffer(&render_device, &render_queue);
|
||||||
|
ui_meta.indices.write_buffer(&render_device, &render_queue);
|
||||||
*previous_len = batches.len();
|
*previous_len = batches.len();
|
||||||
commands.insert_or_spawn_batch(batches);
|
commands.insert_or_spawn_batch(batches);
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,12 @@ impl SpecializedRenderPipeline for UiPipeline {
|
|||||||
VertexFormat::Float32x4,
|
VertexFormat::Float32x4,
|
||||||
// mode
|
// mode
|
||||||
VertexFormat::Uint32,
|
VertexFormat::Uint32,
|
||||||
|
// border radius
|
||||||
|
VertexFormat::Float32x4,
|
||||||
|
// border thickness
|
||||||
|
VertexFormat::Float32x4,
|
||||||
|
// border size
|
||||||
|
VertexFormat::Float32x2,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
let shader_defs = Vec::new();
|
let shader_defs = Vec::new();
|
||||||
|
@ -215,8 +215,17 @@ impl<P: PhaseItem> RenderCommand<P> for DrawUiNode {
|
|||||||
return RenderCommandResult::Failure;
|
return RenderCommandResult::Failure;
|
||||||
};
|
};
|
||||||
|
|
||||||
pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..));
|
let ui_meta = ui_meta.into_inner();
|
||||||
pass.draw(batch.range.clone(), 0..1);
|
// Store the vertices
|
||||||
|
pass.set_vertex_buffer(0, ui_meta.vertices.buffer().unwrap().slice(..));
|
||||||
|
// Define how to "connect" the vertices
|
||||||
|
pass.set_index_buffer(
|
||||||
|
ui_meta.indices.buffer().unwrap().slice(..),
|
||||||
|
0,
|
||||||
|
bevy_render::render_resource::IndexFormat::Uint32,
|
||||||
|
);
|
||||||
|
// Draw the vertices
|
||||||
|
pass.draw_indexed(batch.range.clone(), 0, 0..1);
|
||||||
RenderCommandResult::Success
|
RenderCommandResult::Success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,27 @@
|
|||||||
#import bevy_render::view::View
|
#import bevy_render::view::View
|
||||||
|
|
||||||
const TEXTURED_QUAD: u32 = 0u;
|
const TEXTURED = 1u;
|
||||||
|
const RIGHT_VERTEX = 2u;
|
||||||
|
const BOTTOM_VERTEX = 4u;
|
||||||
|
const BORDER: u32 = 8u;
|
||||||
|
|
||||||
|
fn enabled(flags: u32, mask: u32) -> bool {
|
||||||
|
return (flags & mask) != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> view: View;
|
@group(0) @binding(0) var<uniform> view: View;
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@location(0) uv: vec2<f32>,
|
@location(0) uv: vec2<f32>,
|
||||||
@location(1) color: vec4<f32>,
|
@location(1) color: vec4<f32>,
|
||||||
@location(3) @interpolate(flat) mode: u32,
|
|
||||||
|
@location(2) @interpolate(flat) size: vec2<f32>,
|
||||||
|
@location(3) @interpolate(flat) flags: u32,
|
||||||
|
@location(4) @interpolate(flat) radius: vec4<f32>,
|
||||||
|
@location(5) @interpolate(flat) border: vec4<f32>,
|
||||||
|
|
||||||
|
// Position relative to the center of the rectangle.
|
||||||
|
@location(6) point: vec2<f32>,
|
||||||
@builtin(position) position: vec4<f32>,
|
@builtin(position) position: vec4<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16,27 +30,285 @@ fn vertex(
|
|||||||
@location(0) vertex_position: vec3<f32>,
|
@location(0) vertex_position: vec3<f32>,
|
||||||
@location(1) vertex_uv: vec2<f32>,
|
@location(1) vertex_uv: vec2<f32>,
|
||||||
@location(2) vertex_color: vec4<f32>,
|
@location(2) vertex_color: vec4<f32>,
|
||||||
@location(3) mode: u32,
|
@location(3) flags: u32,
|
||||||
|
|
||||||
|
// x: top left, y: top right, z: bottom right, w: bottom left.
|
||||||
|
@location(4) radius: vec4<f32>,
|
||||||
|
|
||||||
|
// x: left, y: top, z: right, w: bottom.
|
||||||
|
@location(5) border: vec4<f32>,
|
||||||
|
@location(6) size: vec2<f32>,
|
||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
out.uv = vertex_uv;
|
out.uv = vertex_uv;
|
||||||
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
|
out.position = view.view_proj * vec4(vertex_position, 1.0);
|
||||||
out.color = vertex_color;
|
out.color = vertex_color;
|
||||||
out.mode = mode;
|
out.flags = flags;
|
||||||
|
out.radius = radius;
|
||||||
|
out.size = size;
|
||||||
|
out.border = border;
|
||||||
|
var point = 0.49999 * size;
|
||||||
|
if (flags & RIGHT_VERTEX) == 0u {
|
||||||
|
point.x *= -1.;
|
||||||
|
}
|
||||||
|
if (flags & BOTTOM_VERTEX) == 0u {
|
||||||
|
point.y *= -1.;
|
||||||
|
}
|
||||||
|
out.point = point;
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
|
@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
|
||||||
@group(1) @binding(1) var sprite_sampler: sampler;
|
@group(1) @binding(1) var sprite_sampler: sampler;
|
||||||
|
|
||||||
|
fn sigmoid(t: f32) -> f32 {
|
||||||
|
return 1.0 / (1.0 + exp(-t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The returned value is the shortest distance from the given point to the boundary of the rounded
|
||||||
|
// box.
|
||||||
|
//
|
||||||
|
// Negative values indicate that the point is inside the rounded box, positive values that the point
|
||||||
|
// is outside, and zero is exactly on the boundary.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// - `point` -> The function will return the distance from this point to the closest point on
|
||||||
|
// the boundary.
|
||||||
|
// - `size` -> The maximum width and height of the box.
|
||||||
|
// - `corner_radii` -> The radius of each rounded corner. Ordered counter clockwise starting
|
||||||
|
// top left:
|
||||||
|
// x: top left, y: top right, z: bottom right, w: bottom left.
|
||||||
|
fn sd_rounded_box(point: vec2<f32>, size: vec2<f32>, corner_radii: vec4<f32>) -> f32 {
|
||||||
|
// If 0.0 < y then select bottom left (w) and bottom right corner radius (z).
|
||||||
|
// Else select top left (x) and top right corner radius (y).
|
||||||
|
let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y);
|
||||||
|
// w and z are swapped so that both pairs are in left to right order, otherwise this second
|
||||||
|
// select statement would return the incorrect value for the bottom pair.
|
||||||
|
let radius = select(rs.x, rs.y, 0.0 < point.x);
|
||||||
|
// Vector from the corner closest to the point, to the point.
|
||||||
|
let corner_to_point = abs(point) - 0.5 * size;
|
||||||
|
// Vector from the center of the radius circle to the point.
|
||||||
|
let q = corner_to_point + radius;
|
||||||
|
// Length from center of the radius circle to the point, zeros a component if the point is not
|
||||||
|
// within the quadrant of the radius circle that is part of the curved corner.
|
||||||
|
let l = length(max(q, vec2(0.0)));
|
||||||
|
let m = min(max(q.x, q.y), 0.0);
|
||||||
|
return l + m - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sd_inset_rounded_box(point: vec2<f32>, size: vec2<f32>, radius: vec4<f32>, inset: vec4<f32>) -> f32 {
|
||||||
|
let inner_size = size - inset.xy - inset.zw;
|
||||||
|
let inner_center = inset.xy + 0.5 * inner_size - 0.5 * size;
|
||||||
|
let inner_point = point - inner_center;
|
||||||
|
|
||||||
|
var r = radius;
|
||||||
|
|
||||||
|
// Top left corner.
|
||||||
|
r.x = r.x - max(inset.x, inset.y);
|
||||||
|
|
||||||
|
// Top right corner.
|
||||||
|
r.y = r.y - max(inset.z, inset.y);
|
||||||
|
|
||||||
|
// Bottom right corner.
|
||||||
|
r.z = r.z - max(inset.z, inset.w);
|
||||||
|
|
||||||
|
// Bottom left corner.
|
||||||
|
r.w = r.w - max(inset.x, inset.w);
|
||||||
|
|
||||||
|
let half_size = inner_size * 0.5;
|
||||||
|
let min = min(half_size.x, half_size.y);
|
||||||
|
|
||||||
|
r = min(max(r, vec4(0.0)), vec4<f32>(min));
|
||||||
|
|
||||||
|
return sd_rounded_box(inner_point, inner_size, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CLAMP_INNER_CURVES
|
||||||
|
fn sd_inset_rounded_box(point: vec2<f32>, size: vec2<f32>, radius: vec4<f32>, inset: vec4<f32>) -> f32 {
|
||||||
|
let inner_size = size - inset.xy - inset.zw;
|
||||||
|
let inner_center = inset.xy + 0.5 * inner_size - 0.5 * size;
|
||||||
|
let inner_point = point - inner_center;
|
||||||
|
|
||||||
|
var r = radius;
|
||||||
|
|
||||||
|
if 0. < min(inset.x, inset.y) || inset.x + inset.y <= 0. {
|
||||||
|
// Top left corner.
|
||||||
|
r.x = r.x - max(inset.x, inset.y);
|
||||||
|
} else {
|
||||||
|
r.x = 0.;
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0. < min(inset.z, inset.y) || inset.z + inset.y <= 0. {
|
||||||
|
// Top right corner.
|
||||||
|
r.y = r.y - max(inset.z, inset.y);
|
||||||
|
} else {
|
||||||
|
r.y = 0.;
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0. < min(inset.z, inset.w) || inset.z + inset.w <= 0. {
|
||||||
|
// Bottom right corner.
|
||||||
|
r.z = r.z - max(inset.z, inset.w);
|
||||||
|
} else {
|
||||||
|
r.z = 0.;
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0. < min(inset.x, inset.w) || inset.x + inset.w <= 0. {
|
||||||
|
// Bottom left corner.
|
||||||
|
r.w = r.w - max(inset.x, inset.w);
|
||||||
|
} else {
|
||||||
|
r.w = 0.;
|
||||||
|
}
|
||||||
|
|
||||||
|
let half_size = inner_size * 0.5;
|
||||||
|
let min = min(half_size.x, half_size.y);
|
||||||
|
|
||||||
|
r = min(max(r, vec4<f32>(0.0)), vec4<f32>(min));
|
||||||
|
|
||||||
|
return sd_rounded_box(inner_point, inner_size, r);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const RED: vec4<f32> = vec4<f32>(1., 0., 0., 1.);
|
||||||
|
const GREEN: vec4<f32> = vec4<f32>(0., 1., 0., 1.);
|
||||||
|
const BLUE: vec4<f32> = vec4<f32>(0., 0., 1., 1.);
|
||||||
|
const WHITE = vec4<f32>(1., 1., 1., 1.);
|
||||||
|
const BLACK = vec4<f32>(0., 0., 0., 1.);
|
||||||
|
|
||||||
|
// Draw the border in white, rest of the rect black.
|
||||||
|
fn draw_border(in: VertexOutput) -> vec4<f32> {
|
||||||
|
// Distance from external border. Positive values outside.
|
||||||
|
let external_distance = sd_rounded_box(in.point, in.size, in.radius);
|
||||||
|
|
||||||
|
// Distance from internal border. Positive values inside.
|
||||||
|
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
|
||||||
|
|
||||||
|
// Distance from border, positive values inside border.
|
||||||
|
let border = max(-internal_distance, external_distance);
|
||||||
|
|
||||||
|
if border < 0.0 {
|
||||||
|
return WHITE;
|
||||||
|
} else {
|
||||||
|
return BLACK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw just the interior in white, rest of the rect black.
|
||||||
|
fn draw_interior(in: VertexOutput) -> vec4<f32> {
|
||||||
|
// Distance from external border. Positive values outside.
|
||||||
|
let external_distance = sd_rounded_box(in.point, in.size, in.radius);
|
||||||
|
|
||||||
|
if external_distance < 0.0 {
|
||||||
|
return WHITE;
|
||||||
|
} else {
|
||||||
|
return BLACK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw all the geometry.
|
||||||
|
fn draw_test(in: VertexOutput) -> vec4<f32> {
|
||||||
|
// Distance from external border. Negative inside
|
||||||
|
let external_distance = sd_rounded_box(in.point, in.size, in.radius);
|
||||||
|
|
||||||
|
// Distance from internal border.
|
||||||
|
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
|
||||||
|
|
||||||
|
// Distance from border.
|
||||||
|
let border = max(-internal_distance, external_distance);
|
||||||
|
|
||||||
|
// Draw the area outside the border in green.
|
||||||
|
if 0.0 < external_distance {
|
||||||
|
return GREEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the area inside the border in white.
|
||||||
|
if border < 0.0 {
|
||||||
|
return WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the interior in blue.
|
||||||
|
if internal_distance < 0.0 {
|
||||||
|
return BLUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill anything else with red (the presence of any red is a bug).
|
||||||
|
return RED;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_no_aa(in: VertexOutput) -> vec4<f32> {
|
||||||
|
let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv);
|
||||||
|
let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED));
|
||||||
|
|
||||||
|
// Negative value => point inside external border.
|
||||||
|
let external_distance = sd_rounded_box(in.point, in.size, in.radius);
|
||||||
|
// Negative value => point inside internal border.
|
||||||
|
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
|
||||||
|
// Negative value => point inside border.
|
||||||
|
let border = max(external_distance, -internal_distance);
|
||||||
|
|
||||||
|
if enabled(in.flags, BORDER) {
|
||||||
|
if border < 0.0 {
|
||||||
|
return color;
|
||||||
|
} else {
|
||||||
|
return vec4(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if external_distance < 0.0 {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec4(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(in: VertexOutput) -> vec4<f32> {
|
||||||
|
let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv);
|
||||||
|
|
||||||
|
// Only use the color sampled from the texture if the `TEXTURED` flag is enabled.
|
||||||
|
// This allows us to draw both textured and untextured shapes together in the same batch.
|
||||||
|
let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED));
|
||||||
|
|
||||||
|
// Signed distances. The magnitude is the distance of the point from the edge of the shape.
|
||||||
|
// * Negative values indicate that the point is inside the shape.
|
||||||
|
// * Zero values indicate the point is on on the edge of the shape.
|
||||||
|
// * Positive values indicate the point is outside the shape.
|
||||||
|
|
||||||
|
// Signed distance from the exterior boundary.
|
||||||
|
let external_distance = sd_rounded_box(in.point, in.size, in.radius);
|
||||||
|
|
||||||
|
// Signed distance from the border's internal edge (the signed distance is negative if the point
|
||||||
|
// is inside the rect but not on the border).
|
||||||
|
// If the border size is set to zero, this is the same as as the external distance.
|
||||||
|
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
|
||||||
|
|
||||||
|
// Signed distance from the border (the intersection of the rect with its border).
|
||||||
|
// Points inside the border have negative signed distance. Any point outside the border, whether
|
||||||
|
// outside the outside edge, or inside the inner edge have positive signed distance.
|
||||||
|
let border_distance = max(external_distance, -internal_distance);
|
||||||
|
|
||||||
|
// The `fwidth` function returns an approximation of the rate of change of the signed distance
|
||||||
|
// value that is used to ensure that the smooth alpha transition created by smoothstep occurs
|
||||||
|
// over a range of distance values that is proportional to how quickly the distance is changing.
|
||||||
|
let fborder = fwidth(border_distance);
|
||||||
|
let fexternal = fwidth(external_distance);
|
||||||
|
|
||||||
|
if enabled(in.flags, BORDER) {
|
||||||
|
// The item is a border
|
||||||
|
|
||||||
|
// At external edges with no border, `border_distance` is equal to zero.
|
||||||
|
// This select statement ensures we only perform anti-aliasing where a non-zero width border
|
||||||
|
// is present, otherwise an outline about the external boundary would be drawn even without
|
||||||
|
// a border.
|
||||||
|
let t = 1. - select(step(0.0, border_distance), smoothstep(0.0, fborder, border_distance), external_distance < internal_distance);
|
||||||
|
return vec4(color.rgb * t * color.a, t * color.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The item is a rectangle, draw normally with anti-aliasing at the edges.
|
||||||
|
let t = 1. - smoothstep(0.0, fexternal, external_distance);
|
||||||
|
return vec4(color.rgb * t * color.a, t * color.a);
|
||||||
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
// textureSample can only be called in unform control flow, not inside an if branch.
|
return draw(in);
|
||||||
var color = textureSample(sprite_texture, sprite_sampler, in.uv);
|
|
||||||
if in.mode == TEXTURED_QUAD {
|
|
||||||
color = in.color * color;
|
|
||||||
} else {
|
|
||||||
color = in.color;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice
|
|||||||
use bevy_transform::prelude::*;
|
use bevy_transform::prelude::*;
|
||||||
use bevy_utils::HashSet;
|
use bevy_utils::HashSet;
|
||||||
|
|
||||||
use crate::{CalculatedClip, ExtractedUiNode, Node, UiImage};
|
use crate::{CalculatedClip, ExtractedUiNode, Node, NodeType, UiImage};
|
||||||
|
|
||||||
/// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`]
|
/// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`]
|
||||||
///
|
///
|
||||||
@ -68,6 +68,9 @@ impl ComputedTextureSlices {
|
|||||||
atlas_size,
|
atlas_size,
|
||||||
clip: clip.map(|clip| clip.clip),
|
clip: clip.map(|clip| clip.clip),
|
||||||
camera_entity,
|
camera_entity,
|
||||||
|
border: [0.; 4],
|
||||||
|
border_radius: [0.; 4],
|
||||||
|
node_type: NodeType::Rect,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1812,6 +1812,268 @@ impl Default for ZIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used to add rounded corners to a UI node. You can set a UI node to have uniformly
|
||||||
|
/// rounded corners or specify different radii for each corner. If a given radius exceeds half
|
||||||
|
/// the length of the smallest dimension between the node's height or width, the radius will
|
||||||
|
/// calculated as half the smallest dimension.
|
||||||
|
///
|
||||||
|
/// Elliptical nodes are not supported yet. Percentage values are based on the node's smallest
|
||||||
|
/// dimension, either width or height.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_ecs::prelude::*;
|
||||||
|
/// # use bevy_ui::prelude::*;
|
||||||
|
/// # use bevy_color::palettes::basic::{BLUE};
|
||||||
|
/// fn setup_ui(mut commands: Commands) {
|
||||||
|
/// commands.spawn((
|
||||||
|
/// NodeBundle {
|
||||||
|
/// style: Style {
|
||||||
|
/// width: Val::Px(100.),
|
||||||
|
/// height: Val::Px(100.),
|
||||||
|
/// border: UiRect::all(Val::Px(2.)),
|
||||||
|
/// ..Default::default()
|
||||||
|
/// },
|
||||||
|
/// background_color: BLUE.into(),
|
||||||
|
/// border_radius: BorderRadius::new(
|
||||||
|
/// // top left
|
||||||
|
/// Val::Px(10.),
|
||||||
|
/// // top right
|
||||||
|
/// Val::Px(20.),
|
||||||
|
/// // bottom right
|
||||||
|
/// Val::Px(30.),
|
||||||
|
/// // bottom left
|
||||||
|
/// Val::Px(40.),
|
||||||
|
/// ),
|
||||||
|
/// ..Default::default()
|
||||||
|
/// },
|
||||||
|
/// ));
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius>
|
||||||
|
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
|
||||||
|
#[reflect(PartialEq, Default)]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serialize",
|
||||||
|
derive(serde::Serialize, serde::Deserialize),
|
||||||
|
reflect(Serialize, Deserialize)
|
||||||
|
)]
|
||||||
|
pub struct BorderRadius {
|
||||||
|
pub top_left: Val,
|
||||||
|
pub top_right: Val,
|
||||||
|
pub bottom_left: Val,
|
||||||
|
pub bottom_right: Val,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BorderRadius {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderRadius {
|
||||||
|
pub const DEFAULT: Self = Self::ZERO;
|
||||||
|
|
||||||
|
/// Zero curvature. All the corners will be right-angled.
|
||||||
|
pub const ZERO: Self = Self::all(Val::Px(0.));
|
||||||
|
|
||||||
|
/// Maximum curvature. The UI Node will take a capsule shape or circular if width and height are equal.
|
||||||
|
pub const MAX: Self = Self::all(Val::Px(f32::MAX));
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Set all four corners to the same curvature.
|
||||||
|
pub const fn all(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: radius,
|
||||||
|
top_right: radius,
|
||||||
|
bottom_left: radius,
|
||||||
|
bottom_right: radius,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub const fn new(top_left: Val, top_right: Val, bottom_right: Val, bottom_left: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left,
|
||||||
|
top_right,
|
||||||
|
bottom_right,
|
||||||
|
bottom_left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radii to logical pixel values.
|
||||||
|
pub const fn px(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: Val::Px(top_left),
|
||||||
|
top_right: Val::Px(top_right),
|
||||||
|
bottom_right: Val::Px(bottom_right),
|
||||||
|
bottom_left: Val::Px(bottom_left),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radii to percentage values.
|
||||||
|
pub const fn percent(
|
||||||
|
top_left: f32,
|
||||||
|
top_right: f32,
|
||||||
|
bottom_right: f32,
|
||||||
|
bottom_left: f32,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: Val::Px(top_left),
|
||||||
|
top_right: Val::Px(top_right),
|
||||||
|
bottom_right: Val::Px(bottom_right),
|
||||||
|
bottom_left: Val::Px(bottom_left),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radius for the top left corner.
|
||||||
|
/// Remaining corners will be right-angled.
|
||||||
|
pub const fn top_left(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: radius,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radius for the top right corner.
|
||||||
|
/// Remaining corners will be right-angled.
|
||||||
|
pub const fn top_right(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
top_right: radius,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radius for the bottom right corner.
|
||||||
|
/// Remaining corners will be right-angled.
|
||||||
|
pub const fn bottom_right(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
bottom_right: radius,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radius for the bottom left corner.
|
||||||
|
/// Remaining corners will be right-angled.
|
||||||
|
pub const fn bottom_left(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
bottom_left: radius,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radii for the top left and bottom left corners.
|
||||||
|
/// Remaining corners will be right-angled.
|
||||||
|
pub const fn left(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: radius,
|
||||||
|
bottom_left: radius,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radii for the top right and bottom right corners.
|
||||||
|
/// Remaining corners will be right-angled.
|
||||||
|
pub const fn right(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
top_right: radius,
|
||||||
|
bottom_right: radius,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radii for the top left and top right corners.
|
||||||
|
/// Remaining corners will be right-angled.
|
||||||
|
pub const fn top(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: radius,
|
||||||
|
top_right: radius,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Sets the radii for the bottom left and bottom right corners.
|
||||||
|
/// Remaining corners will be right-angled.
|
||||||
|
pub const fn bottom(radius: Val) -> Self {
|
||||||
|
Self {
|
||||||
|
bottom_left: radius,
|
||||||
|
bottom_right: radius,
|
||||||
|
..Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`BorderRadius`] with its `top_left` field set to the given value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_top_left(mut self, radius: Val) -> Self {
|
||||||
|
self.top_left = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`BorderRadius`] with its `top_right` field set to the given value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_top_right(mut self, radius: Val) -> Self {
|
||||||
|
self.top_right = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`BorderRadius`] with its `bottom_right` field set to the given value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_bottom_right(mut self, radius: Val) -> Self {
|
||||||
|
self.bottom_right = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`BorderRadius`] with its `bottom_left` field set to the given value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_bottom_left(mut self, radius: Val) -> Self {
|
||||||
|
self.bottom_left = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`BorderRadius`] with its `top_left` and `bottom_left` fields set to the given value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_left(mut self, radius: Val) -> Self {
|
||||||
|
self.top_left = radius;
|
||||||
|
self.bottom_left = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`BorderRadius`] with its `top_right` and `bottom_right` fields set to the given value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_right(mut self, radius: Val) -> Self {
|
||||||
|
self.top_right = radius;
|
||||||
|
self.bottom_right = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`BorderRadius`] with its `top_left` and `top_right` fields set to the given value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_top(mut self, radius: Val) -> Self {
|
||||||
|
self.top_left = radius;
|
||||||
|
self.top_right = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`BorderRadius`] with its `bottom_left` and `bottom_right` fields set to the given value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_bottom(mut self, radius: Val) -> Self {
|
||||||
|
self.bottom_left = radius;
|
||||||
|
self.bottom_right = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::GridPlacement;
|
use crate::GridPlacement;
|
||||||
|
@ -407,6 +407,7 @@ Example | Description
|
|||||||
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
|
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
|
||||||
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
|
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
|
||||||
[Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world
|
[Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world
|
||||||
|
[Rounded Borders](../examples/ui/rounded_borders.rs) | Demonstrates how to create a node with a rounded border
|
||||||
[Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node.
|
[Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node.
|
||||||
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
|
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
|
||||||
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
|
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
|
||||||
|
@ -74,6 +74,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
border_color: BorderColor(Color::BLACK),
|
border_color: BorderColor(Color::BLACK),
|
||||||
|
border_radius: BorderRadius::MAX,
|
||||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||||
..default()
|
..default()
|
||||||
})
|
})
|
||||||
|
176
examples/ui/rounded_borders.rs
Normal file
176
examples/ui/rounded_borders.rs
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
//! Example demonstrating rounded bordered UI nodes
|
||||||
|
|
||||||
|
use bevy::{color::palettes::css::*, prelude::*};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands) {
|
||||||
|
commands.spawn(Camera2dBundle::default());
|
||||||
|
let root = commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
margin: UiRect::all(Val::Px(25.0)),
|
||||||
|
align_self: AlignSelf::Stretch,
|
||||||
|
justify_self: JustifySelf::Stretch,
|
||||||
|
flex_wrap: FlexWrap::Wrap,
|
||||||
|
justify_content: JustifyContent::FlexStart,
|
||||||
|
align_items: AlignItems::FlexStart,
|
||||||
|
align_content: AlignContent::FlexStart,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: Color::srgb(0.25, 0.25, 0.25).into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// labels for the different border edges
|
||||||
|
let border_labels = [
|
||||||
|
"None",
|
||||||
|
"All",
|
||||||
|
"Left",
|
||||||
|
"Right",
|
||||||
|
"Top",
|
||||||
|
"Bottom",
|
||||||
|
"Left Right",
|
||||||
|
"Top Bottom",
|
||||||
|
"Top Left",
|
||||||
|
"Bottom Left",
|
||||||
|
"Top Right",
|
||||||
|
"Bottom Right",
|
||||||
|
"Top Bottom Right",
|
||||||
|
"Top Bottom Left",
|
||||||
|
"Top Left Right",
|
||||||
|
"Bottom Left Right",
|
||||||
|
];
|
||||||
|
|
||||||
|
// all the different combinations of border edges
|
||||||
|
// these correspond to the labels above
|
||||||
|
let borders = [
|
||||||
|
UiRect::default(),
|
||||||
|
UiRect::all(Val::Px(10.)),
|
||||||
|
UiRect::left(Val::Px(10.)),
|
||||||
|
UiRect::right(Val::Px(10.)),
|
||||||
|
UiRect::top(Val::Px(10.)),
|
||||||
|
UiRect::bottom(Val::Px(10.)),
|
||||||
|
UiRect::horizontal(Val::Px(10.)),
|
||||||
|
UiRect::vertical(Val::Px(10.)),
|
||||||
|
UiRect {
|
||||||
|
left: Val::Px(10.),
|
||||||
|
top: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
UiRect {
|
||||||
|
left: Val::Px(10.),
|
||||||
|
bottom: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
UiRect {
|
||||||
|
right: Val::Px(10.),
|
||||||
|
top: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
UiRect {
|
||||||
|
right: Val::Px(10.),
|
||||||
|
bottom: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
UiRect {
|
||||||
|
right: Val::Px(10.),
|
||||||
|
top: Val::Px(10.),
|
||||||
|
bottom: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
UiRect {
|
||||||
|
left: Val::Px(10.),
|
||||||
|
top: Val::Px(10.),
|
||||||
|
bottom: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
UiRect {
|
||||||
|
left: Val::Px(10.),
|
||||||
|
right: Val::Px(10.),
|
||||||
|
top: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
UiRect {
|
||||||
|
left: Val::Px(10.),
|
||||||
|
right: Val::Px(10.),
|
||||||
|
bottom: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (label, border) in border_labels.into_iter().zip(borders) {
|
||||||
|
let inner_spot = commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Px(10.),
|
||||||
|
height: Val::Px(10.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
border_radius: BorderRadius::MAX,
|
||||||
|
background_color: YELLOW.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.id();
|
||||||
|
let non_zero = |x, y| x != Val::Px(0.) && y != Val::Px(0.);
|
||||||
|
let border_size = |x, y| if non_zero(x, y) { f32::MAX } else { 0. };
|
||||||
|
let border_radius = BorderRadius::px(
|
||||||
|
border_size(border.left, border.top),
|
||||||
|
border_size(border.right, border.top),
|
||||||
|
border_size(border.right, border.bottom),
|
||||||
|
border_size(border.left, border.bottom),
|
||||||
|
);
|
||||||
|
let border_node = commands
|
||||||
|
.spawn((
|
||||||
|
NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Px(50.),
|
||||||
|
height: Val::Px(50.),
|
||||||
|
border,
|
||||||
|
margin: UiRect::all(Val::Px(20.)),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: MAROON.into(),
|
||||||
|
border_color: RED.into(),
|
||||||
|
border_radius,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Outline {
|
||||||
|
width: Val::Px(6.),
|
||||||
|
offset: Val::Px(6.),
|
||||||
|
color: Color::WHITE,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.add_child(inner_spot)
|
||||||
|
.id();
|
||||||
|
let label_node = commands
|
||||||
|
.spawn(TextBundle::from_section(
|
||||||
|
label,
|
||||||
|
TextStyle {
|
||||||
|
font_size: 9.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
let container = commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.push_children(&[border_node, label_node])
|
||||||
|
.id();
|
||||||
|
commands.entity(root).add_child(container);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user