From 56d559102858d4ce8a5b5e3bccd7053dd687a197 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Dec 2024 21:30:08 +0000 Subject: [PATCH] Multiple box shadow support (#16502) # Objective Add support for multiple box shadows on a single `Node`. ## Solution * Rename `BoxShadow` to `ShadowStyle` and remove its `Component` derive. * Create a new `BoxShadow` component that newtypes a `Vec`. * Add a `new` constructor method to `BoxShadow` for single shadows. * Change `extract_shadows` to iterate through a list of shadows per node. Render order is determined implicitly from the order of the shadows stored in the `BoxShadow` component, back-to-front. Might be more efficient to use a `SmallVec<[ShadowStyle; 1]>` for the list of shadows but not sure if the extra friction is worth it. ## Testing Added a node with four differently coloured shadows to the `box_shadow` example. --- ## Showcase ``` cargo run --example box_shadow ``` four-shadow ## Migration Guide Bevy UI now supports multiple shadows per node. A new struct `ShadowStyle` is used to set the style for each shadow. And the `BoxShadow` component is changed to a tuple struct wrapping a vector containing a list of `ShadowStyle`s. To spawn a node with a single shadow you can use the `new` constructor function: ```rust commands.spawn(( Node::default(), BoxShadow::new( Color::BLACK.with_alpha(0.8), Val::Percent(offset.x), Val::Percent(offset.y), Val::Percent(spread), Val::Px(blur), ) )); ``` --------- Co-authored-by: Alice Cecile --- crates/bevy_ui/src/render/box_shadow.rs | 105 +++++++++++++----------- crates/bevy_ui/src/ui_node.rs | 44 +++++++++- examples/testbed/ui.rs | 16 ++-- examples/ui/box_shadow.rs | 61 ++++++++++++-- 4 files changed, 159 insertions(+), 67 deletions(-) diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 51536ad981e22..e850db3b4ab0e 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -263,7 +263,7 @@ pub fn extract_shadows( }; // Skip invisible images - if !view_visibility.get() || box_shadow.color.is_fully_transparent() || uinode.is_empty() { + if !view_visibility.get() || box_shadow.is_empty() || uinode.is_empty() { continue; } @@ -278,57 +278,64 @@ pub fn extract_shadows( let scale_factor = uinode.inverse_scale_factor.recip(); - let resolve_val = |val, base, scale_factor| match val { - Val::Auto => 0., - Val::Px(px) => px * scale_factor, - Val::Percent(percent) => percent / 100. * base, - Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x, - Val::Vh(percent) => percent / 100. * ui_physical_viewport_size.y, - Val::VMin(percent) => percent / 100. * ui_physical_viewport_size.min_element(), - Val::VMax(percent) => percent / 100. * ui_physical_viewport_size.max_element(), - }; - - let spread_x = resolve_val(box_shadow.spread_radius, uinode.size().x, scale_factor); - let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x; - - let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y); - - let blur_radius = resolve_val(box_shadow.blur_radius, uinode.size().x, scale_factor); - let offset = vec2( - resolve_val(box_shadow.x_offset, uinode.size().x, scale_factor), - resolve_val(box_shadow.y_offset, uinode.size().y, scale_factor), - ); - - let shadow_size = uinode.size() + spread; - if shadow_size.cmple(Vec2::ZERO).any() { - continue; - } + for drop_shadow in box_shadow.iter() { + if drop_shadow.color.is_fully_transparent() { + continue; + } - let radius = ResolvedBorderRadius { - top_left: uinode.border_radius.top_left * spread_ratio, - top_right: uinode.border_radius.top_right * spread_ratio, - bottom_left: uinode.border_radius.bottom_left * spread_ratio, - bottom_right: uinode.border_radius.bottom_right * spread_ratio, - }; + let resolve_val = |val, base, scale_factor| match val { + Val::Auto => 0., + Val::Px(px) => px * scale_factor, + Val::Percent(percent) => percent / 100. * base, + Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x, + Val::Vh(percent) => percent / 100. * ui_physical_viewport_size.y, + Val::VMin(percent) => percent / 100. * ui_physical_viewport_size.min_element(), + Val::VMax(percent) => percent / 100. * ui_physical_viewport_size.max_element(), + }; + + let spread_x = resolve_val(drop_shadow.spread_radius, uinode.size().x, scale_factor); + let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x; + + let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y); + + let blur_radius = resolve_val(drop_shadow.blur_radius, uinode.size().x, scale_factor); + let offset = vec2( + resolve_val(drop_shadow.x_offset, uinode.size().x, scale_factor), + resolve_val(drop_shadow.y_offset, uinode.size().y, scale_factor), + ); + + let shadow_size = uinode.size() + spread; + if shadow_size.cmple(Vec2::ZERO).any() { + continue; + } - extracted_box_shadows.box_shadows.insert( - commands.spawn(TemporaryRenderEntity).id(), - ExtractedBoxShadow { - stack_index: uinode.stack_index, - transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)), - color: box_shadow.color.into(), - rect: Rect { - min: Vec2::ZERO, - max: shadow_size + 6. * blur_radius, + let radius = ResolvedBorderRadius { + top_left: uinode.border_radius.top_left * spread_ratio, + top_right: uinode.border_radius.top_right * spread_ratio, + bottom_left: uinode.border_radius.bottom_left * spread_ratio, + bottom_right: uinode.border_radius.bottom_right * spread_ratio, + }; + + extracted_box_shadows.box_shadows.insert( + commands.spawn(TemporaryRenderEntity).id(), + ExtractedBoxShadow { + stack_index: uinode.stack_index, + transform: transform.compute_matrix() + * Mat4::from_translation(offset.extend(0.)), + color: drop_shadow.color.into(), + rect: Rect { + min: Vec2::ZERO, + max: shadow_size + 6. * blur_radius, + }, + clip: clip.map(|clip| clip.clip), + camera_entity, + radius, + blur_radius, + size: shadow_size, + main_entity: entity.into(), }, - clip: clip.map(|clip| clip.clip), - camera_entity, - radius, - blur_radius, - size: shadow_size, - main_entity: entity.into(), - }, - ); + ); + } } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 6877dd3118c47..b35bf42dfad9c 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1,5 +1,6 @@ use crate::{FocusPolicy, UiRect, Val}; use bevy_color::Color; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_math::{vec4, Rect, Vec2, Vec4Swizzles}; use bevy_reflect::prelude::*; @@ -2419,14 +2420,51 @@ impl ResolvedBorderRadius { }; } -#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[derive(Component, Clone, Debug, Default, PartialEq, Reflect, Deref, DerefMut)] #[reflect(Component, PartialEq, Default)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub struct BoxShadow { +/// List of shadows to draw for a [`Node`]. +/// +/// Draw order is determined implicitly from the vector of [`ShadowStyle`]s, back-to-front. +pub struct BoxShadow(pub Vec); + +impl BoxShadow { + /// A single drop shadow + pub fn new( + color: Color, + x_offset: Val, + y_offset: Val, + spread_radius: Val, + blur_radius: Val, + ) -> Self { + Self(vec![ShadowStyle { + color, + x_offset, + y_offset, + spread_radius, + blur_radius, + }]) + } +} + +impl From for BoxShadow { + fn from(value: ShadowStyle) -> Self { + Self(vec![value]) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct ShadowStyle { /// The shadow's color pub color: Color, /// Horizontal offset @@ -2442,7 +2480,7 @@ pub struct BoxShadow { pub blur_radius: Val, } -impl Default for BoxShadow { +impl Default for ShadowStyle { fn default() -> Self { Self { color: Color::BLACK, diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index eed51332a6fc8..098afe75230f2 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -190,7 +190,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }); }); - let shadow = BoxShadow { + let shadow_style = ShadowStyle { color: Color::BLACK.with_alpha(0.5), blur_radius: Val::Px(2.), x_offset: Val::Px(10.), @@ -218,7 +218,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgb(1.0, 0.0, 0.)), - shadow, + BoxShadow::from(shadow_style), )) .with_children(|parent| { parent.spawn(( @@ -232,7 +232,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgb(1.0, 0.3, 0.3)), - shadow, + BoxShadow::from(shadow_style), )); parent.spawn(( Node { @@ -244,7 +244,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgb(1.0, 0.5, 0.5)), - shadow, + BoxShadow::from(shadow_style), )); parent.spawn(( Node { @@ -256,7 +256,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgb(0.0, 0.7, 0.7)), - shadow, + BoxShadow::from(shadow_style), )); // alpha test parent.spawn(( @@ -269,10 +269,10 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgba(1.0, 0.9, 0.9, 0.4)), - BoxShadow { + BoxShadow::from(ShadowStyle { color: Color::BLACK.with_alpha(0.3), - ..shadow - }, + ..shadow_style + }), )); }); }); diff --git a/examples/ui/box_shadow.rs b/examples/ui/box_shadow.rs index 64956a3e5f73b..d3715ab383deb 100644 --- a/examples/ui/box_shadow.rs +++ b/examples/ui/box_shadow.rs @@ -1,8 +1,12 @@ //! This example shows how to create a node with a shadow use argh::FromArgs; +use bevy::color::palettes::css::BLUE; use bevy::color::palettes::css::DEEP_SKY_BLUE; +use bevy::color::palettes::css::GREEN; use bevy::color::palettes::css::LIGHT_SKY_BLUE; +use bevy::color::palettes::css::RED; +use bevy::color::palettes::css::YELLOW; use bevy::prelude::*; use bevy::winit::WinitSettings; @@ -189,6 +193,49 @@ fn setup(mut commands: Commands) { border_radius, )); } + + // Demonstrate multiple shadows on one node + commands.spawn(( + Node { + width: Val::Px(40.), + height: Val::Px(40.), + border: UiRect::all(Val::Px(4.)), + ..default() + }, + BorderColor(LIGHT_SKY_BLUE.into()), + BorderRadius::all(Val::Px(20.)), + BackgroundColor(DEEP_SKY_BLUE.into()), + BoxShadow(vec![ + ShadowStyle { + color: RED.with_alpha(0.7).into(), + x_offset: Val::Px(-20.), + y_offset: Val::Px(-5.), + spread_radius: Val::Percent(10.), + blur_radius: Val::Px(3.), + }, + ShadowStyle { + color: BLUE.with_alpha(0.7).into(), + x_offset: Val::Px(-5.), + y_offset: Val::Px(-20.), + spread_radius: Val::Percent(10.), + blur_radius: Val::Px(3.), + }, + ShadowStyle { + color: YELLOW.with_alpha(0.7).into(), + x_offset: Val::Px(20.), + y_offset: Val::Px(5.), + spread_radius: Val::Percent(10.), + blur_radius: Val::Px(3.), + }, + ShadowStyle { + color: GREEN.with_alpha(0.7).into(), + x_offset: Val::Px(5.), + y_offset: Val::Px(20.), + spread_radius: Val::Percent(10.), + blur_radius: Val::Px(3.), + }, + ]), + )); }); } @@ -209,12 +256,12 @@ fn box_shadow_node_bundle( BorderColor(LIGHT_SKY_BLUE.into()), border_radius, BackgroundColor(DEEP_SKY_BLUE.into()), - BoxShadow { - color: Color::BLACK.with_alpha(0.8), - x_offset: Val::Percent(offset.x), - y_offset: Val::Percent(offset.y), - spread_radius: Val::Percent(spread), - blur_radius: Val::Px(blur), - }, + BoxShadow::new( + Color::BLACK.with_alpha(0.8), + Val::Percent(offset.x), + Val::Percent(offset.y), + Val::Percent(spread), + Val::Px(blur), + ), ) }