mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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
12 changed files with 977 additions and 92 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -2340,6 +2340,17 @@ description = "Demonstrates how to create a node with a border"
|
|||
category = "UI (User Interface)"
|
||||
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]]
|
||||
name = "button"
|
||||
path = "examples/ui/button.rs"
|
||||
|
|
|
@ -112,6 +112,7 @@ impl Plugin for UiPlugin {
|
|||
.register_type::<UiRect>()
|
||||
.register_type::<UiScale>()
|
||||
.register_type::<BorderColor>()
|
||||
.register_type::<BorderRadius>()
|
||||
.register_type::<widget::Button>()
|
||||
.register_type::<widget::Label>()
|
||||
.register_type::<ZIndex>()
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
use crate::widget::TextFlags;
|
||||
use crate::{
|
||||
widget::{Button, UiImageSize},
|
||||
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
|
||||
UiMaterial, ZIndex,
|
||||
BackgroundColor, BorderColor, BorderRadius, ContentSize, FocusPolicy, Interaction, Node, Style,
|
||||
UiImage, UiMaterial, ZIndex,
|
||||
};
|
||||
use bevy_asset::Handle;
|
||||
use bevy_color::Color;
|
||||
|
@ -34,6 +34,8 @@ pub struct NodeBundle {
|
|||
pub background_color: BackgroundColor,
|
||||
/// The color of the Node's border
|
||||
pub border_color: BorderColor,
|
||||
/// The border radius of the node
|
||||
pub border_radius: BorderRadius,
|
||||
/// Whether this node should block interaction with lower nodes
|
||||
pub focus_policy: FocusPolicy,
|
||||
/// The transform of the node
|
||||
|
@ -62,6 +64,7 @@ impl Default for NodeBundle {
|
|||
// Transparent background
|
||||
background_color: Color::NONE.into(),
|
||||
border_color: Color::NONE.into(),
|
||||
border_radius: BorderRadius::default(),
|
||||
node: Default::default(),
|
||||
style: Default::default(),
|
||||
focus_policy: Default::default(),
|
||||
|
@ -314,6 +317,8 @@ pub struct ButtonBundle {
|
|||
pub focus_policy: FocusPolicy,
|
||||
/// The color of the Node's border
|
||||
pub border_color: BorderColor,
|
||||
/// The border radius of the node
|
||||
pub border_radius: BorderRadius,
|
||||
/// The image of the node
|
||||
pub image: UiImage,
|
||||
/// The transform of the node
|
||||
|
@ -344,6 +349,7 @@ impl Default for ButtonBundle {
|
|||
interaction: Default::default(),
|
||||
focus_policy: FocusPolicy::Block,
|
||||
border_color: BorderColor(Color::NONE),
|
||||
border_radius: BorderRadius::default(),
|
||||
image: Default::default(),
|
||||
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_render::{render_phase::PhaseItem, view::ViewVisibility, ExtractSchedule, Render};
|
||||
use bevy_sprite::{SpriteAssetEvents, TextureAtlas};
|
||||
use bevy_window::{PrimaryWindow, Window};
|
||||
pub use pipeline::*;
|
||||
pub use render_pass::*;
|
||||
pub use ui_material_pipeline::*;
|
||||
|
||||
use crate::graph::{NodeUi, SubGraphUi};
|
||||
use crate::{
|
||||
texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, CalculatedClip,
|
||||
ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val,
|
||||
texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, BorderRadius,
|
||||
CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage,
|
||||
UiScale, Val,
|
||||
};
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
|
||||
use bevy_ecs::entity::EntityHashMap;
|
||||
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::{
|
||||
camera::Camera,
|
||||
render_asset::RenderAssets,
|
||||
|
@ -141,6 +143,14 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph {
|
|||
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 stack_index: u32,
|
||||
pub transform: Mat4,
|
||||
|
@ -155,6 +165,13 @@ pub struct ExtractedUiNode {
|
|||
// it is defaulted to a single camera if only one exists.
|
||||
// Nodes with ambiguous camera will be ignored.
|
||||
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)]
|
||||
|
@ -164,7 +181,9 @@ pub struct ExtractedUiNodes {
|
|||
|
||||
pub fn extract_uinode_background_colors(
|
||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||
windows: Extract<Query<&Window, With<PrimaryWindow>>>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
ui_scale: Extract<Res<UiScale>>,
|
||||
uinode_query: Extract<
|
||||
Query<(
|
||||
Entity,
|
||||
|
@ -174,11 +193,26 @@ pub fn extract_uinode_background_colors(
|
|||
Option<&CalculatedClip>,
|
||||
Option<&TargetCamera>,
|
||||
&BackgroundColor,
|
||||
&BorderRadius,
|
||||
)>,
|
||||
>,
|
||||
) {
|
||||
for (entity, uinode, transform, view_visibility, clip, camera, background_color) in
|
||||
&uinode_query
|
||||
let viewport_size = windows
|
||||
.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())
|
||||
else {
|
||||
|
@ -190,6 +224,9 @@ pub fn extract_uinode_background_colors(
|
|||
continue;
|
||||
}
|
||||
|
||||
let border_radius =
|
||||
resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0);
|
||||
|
||||
extracted_uinodes.uinodes.insert(
|
||||
entity,
|
||||
ExtractedUiNode {
|
||||
|
@ -206,6 +243,9 @@ pub fn extract_uinode_background_colors(
|
|||
flip_x: false,
|
||||
flip_y: false,
|
||||
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(
|
||||
mut commands: Commands,
|
||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||
windows: Extract<Query<&Window, With<PrimaryWindow>>>,
|
||||
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
|
||||
ui_scale: Extract<Res<UiScale>>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
uinode_query: Extract<
|
||||
Query<(
|
||||
|
@ -226,10 +268,19 @@ pub fn extract_uinode_images(
|
|||
&UiImage,
|
||||
Option<&TextureAtlas>,
|
||||
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())
|
||||
else {
|
||||
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(
|
||||
commands.spawn_empty().id(),
|
||||
ExtractedUiNode {
|
||||
|
@ -285,6 +339,9 @@ pub fn extract_uinode_images(
|
|||
flip_x: image.flip_x,
|
||||
flip_y: image.flip_y,
|
||||
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(
|
||||
mut commands: Commands,
|
||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||
|
@ -319,6 +425,7 @@ pub fn extract_uinode_borders(
|
|||
Option<&Parent>,
|
||||
&Style,
|
||||
&BorderColor,
|
||||
&BorderRadius,
|
||||
),
|
||||
Without<ContentSize>,
|
||||
>,
|
||||
|
@ -327,8 +434,17 @@ pub fn extract_uinode_borders(
|
|||
) {
|
||||
let image = AssetId::<Image>::default();
|
||||
|
||||
for (node, global_transform, view_visibility, clip, camera, parent, style, border_color) in
|
||||
&uinode_query
|
||||
for (
|
||||
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())
|
||||
else {
|
||||
|
@ -368,60 +484,40 @@ pub fn extract_uinode_borders(
|
|||
let bottom =
|
||||
resolve_border_thickness(style.border.bottom, parent_width, ui_logical_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 border = [left, top, right, bottom];
|
||||
|
||||
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();
|
||||
|
||||
for edge in border_rects {
|
||||
if edge.min.x < edge.max.x && edge.min.y < edge.max.y {
|
||||
extracted_uinodes.uinodes.insert(
|
||||
commands.spawn_empty().id(),
|
||||
ExtractedUiNode {
|
||||
stack_index: node.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.into(),
|
||||
rect: Rect {
|
||||
max: edge.size(),
|
||||
..Default::default()
|
||||
},
|
||||
image,
|
||||
atlas_size: None,
|
||||
clip: clip.map(|clip| clip.clip),
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
camera_entity,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
extracted_uinodes.uinodes.insert(
|
||||
commands.spawn_empty().id(),
|
||||
ExtractedUiNode {
|
||||
stack_index: node.stack_index,
|
||||
// This translates the uinode's transform to the center of the current border rectangle
|
||||
transform,
|
||||
color: border_color.0.into(),
|
||||
rect: Rect {
|
||||
max: node.size(),
|
||||
..Default::default()
|
||||
},
|
||||
image,
|
||||
atlas_size: None,
|
||||
clip: clip.map(|clip| clip.clip),
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
camera_entity,
|
||||
border_radius,
|
||||
border,
|
||||
node_type: NodeType::Border,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -490,7 +586,6 @@ pub fn extract_uinode_outlines(
|
|||
];
|
||||
|
||||
let transform = global_transform.compute_matrix();
|
||||
|
||||
for edge in outline_edges {
|
||||
if edge.min.x < edge.max.x && edge.min.y < edge.max.y {
|
||||
extracted_uinodes.uinodes.insert(
|
||||
|
@ -510,6 +605,9 @@ pub fn extract_uinode_outlines(
|
|||
flip_x: false,
|
||||
flip_y: false,
|
||||
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_y: false,
|
||||
camera_entity,
|
||||
border: [0.; 4],
|
||||
border_radius: [0.; 4],
|
||||
node_type: NodeType::Rect,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -692,12 +793,23 @@ struct UiVertex {
|
|||
pub position: [f32; 3],
|
||||
pub uv: [f32; 2],
|
||||
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)]
|
||||
pub struct UiMeta {
|
||||
vertices: BufferVec<UiVertex>,
|
||||
indices: BufferVec<u32>,
|
||||
view_bind_group: Option<BindGroup>,
|
||||
}
|
||||
|
||||
|
@ -705,6 +817,7 @@ impl Default for UiMeta {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
vertices: BufferVec::new(BufferUsages::VERTEX),
|
||||
indices: BufferVec::new(BufferUsages::INDEX),
|
||||
view_bind_group: None,
|
||||
}
|
||||
}
|
||||
|
@ -726,8 +839,14 @@ pub struct UiBatch {
|
|||
pub camera: Entity,
|
||||
}
|
||||
|
||||
const TEXTURED_QUAD: u32 = 0;
|
||||
const UNTEXTURED_QUAD: u32 = 1;
|
||||
/// The values here should match the values for the constants in `ui.wgsl`
|
||||
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)]
|
||||
pub fn queue_uinodes(
|
||||
|
@ -802,14 +921,17 @@ pub fn prepare_uinodes(
|
|||
let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len);
|
||||
|
||||
ui_meta.vertices.clear();
|
||||
ui_meta.indices.clear();
|
||||
ui_meta.view_bind_group = Some(render_device.create_bind_group(
|
||||
"ui_view_bind_group",
|
||||
&ui_pipeline.view_layout,
|
||||
&BindGroupEntries::single(view_binding),
|
||||
));
|
||||
|
||||
// Vertex buffer index
|
||||
let mut index = 0;
|
||||
// Buffer indexes
|
||||
let mut vertices_index = 0;
|
||||
let mut indices_index = 0;
|
||||
|
||||
for mut ui_phase in &mut phases {
|
||||
let mut batch_item_index = 0;
|
||||
let mut batch_image_handle = AssetId::invalid();
|
||||
|
@ -832,7 +954,7 @@ pub fn prepare_uinodes(
|
|||
batch_image_handle = extracted_uinode.image;
|
||||
|
||||
let new_batch = UiBatch {
|
||||
range: index..index,
|
||||
range: vertices_index..vertices_index,
|
||||
image: extracted_uinode.image,
|
||||
camera: extracted_uinode.camera_entity,
|
||||
};
|
||||
|
@ -882,10 +1004,10 @@ pub fn prepare_uinodes(
|
|||
}
|
||||
}
|
||||
|
||||
let mode = if extracted_uinode.image != AssetId::default() {
|
||||
TEXTURED_QUAD
|
||||
let mut flags = if extracted_uinode.image != AssetId::default() {
|
||||
shader_flags::TEXTURED
|
||||
} else {
|
||||
UNTEXTURED_QUAD
|
||||
shader_flags::UNTEXTURED
|
||||
};
|
||||
|
||||
let mut uinode_rect = extracted_uinode.rect;
|
||||
|
@ -946,7 +1068,7 @@ pub fn prepare_uinodes(
|
|||
continue;
|
||||
}
|
||||
}
|
||||
let uvs = if mode == UNTEXTURED_QUAD {
|
||||
let uvs = if flags == shader_flags::UNTEXTURED {
|
||||
[Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]
|
||||
} else {
|
||||
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();
|
||||
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 {
|
||||
position: positions_clipped[i].into(),
|
||||
uv: uvs[i].into(),
|
||||
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;
|
||||
} else {
|
||||
batch_image_handle = AssetId::invalid();
|
||||
|
@ -1003,6 +1139,7 @@ pub fn prepare_uinodes(
|
|||
}
|
||||
}
|
||||
ui_meta.vertices.write_buffer(&render_device, &render_queue);
|
||||
ui_meta.indices.write_buffer(&render_device, &render_queue);
|
||||
*previous_len = batches.len();
|
||||
commands.insert_or_spawn_batch(batches);
|
||||
}
|
||||
|
|
|
@ -65,6 +65,12 @@ impl SpecializedRenderPipeline for UiPipeline {
|
|||
VertexFormat::Float32x4,
|
||||
// mode
|
||||
VertexFormat::Uint32,
|
||||
// border radius
|
||||
VertexFormat::Float32x4,
|
||||
// border thickness
|
||||
VertexFormat::Float32x4,
|
||||
// border size
|
||||
VertexFormat::Float32x2,
|
||||
],
|
||||
);
|
||||
let shader_defs = Vec::new();
|
||||
|
|
|
@ -215,8 +215,17 @@ impl<P: PhaseItem> RenderCommand<P> for DrawUiNode {
|
|||
return RenderCommandResult::Failure;
|
||||
};
|
||||
|
||||
pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..));
|
||||
pass.draw(batch.range.clone(), 0..1);
|
||||
let ui_meta = ui_meta.into_inner();
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
#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;
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) uv: vec2<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>,
|
||||
};
|
||||
|
||||
|
@ -16,27 +30,285 @@ fn vertex(
|
|||
@location(0) vertex_position: vec3<f32>,
|
||||
@location(1) vertex_uv: vec2<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 {
|
||||
var out: VertexOutput;
|
||||
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.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;
|
||||
}
|
||||
|
||||
@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
|
||||
@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
|
||||
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// textureSample can only be called in unform control flow, not inside an if branch.
|
||||
var color = textureSample(sprite_texture, sprite_sampler, in.uv);
|
||||
if in.mode == TEXTURED_QUAD {
|
||||
color = in.color * color;
|
||||
} else {
|
||||
color = in.color;
|
||||
}
|
||||
return color;
|
||||
return draw(in);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice
|
|||
use bevy_transform::prelude::*;
|
||||
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`]
|
||||
///
|
||||
|
@ -68,6 +68,9 @@ impl ComputedTextureSlices {
|
|||
atlas_size,
|
||||
clip: clip.map(|clip| clip.clip),
|
||||
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)]
|
||||
mod tests {
|
||||
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
|
||||
[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
|
||||
[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.
|
||||
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
|
||||
[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()
|
||||
},
|
||||
border_color: BorderColor(Color::BLACK),
|
||||
border_radius: BorderRadius::MAX,
|
||||
image: UiImage::default().with_color(NORMAL_BUTTON),
|
||||
..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 a new issue