Skip to content

Commit

Permalink
Multiple box shadow support (#16502)
Browse files Browse the repository at this point in the history
# 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<ShadowStyle>`.
* 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
```

<img width="460" alt="four-shadow"
src="https://github.com/user-attachments/assets/2f728c47-33b4-42e1-96ba-28a774b94b24">

## 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 <[email protected]>
  • Loading branch information
ickshonpe and alice-i-cecile authored Dec 1, 2024
1 parent 206f4f7 commit 56d5591
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 67 deletions.
105 changes: 56 additions & 49 deletions crates/bevy_ui/src/render/box_shadow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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(),
},
);
);
}
}
}

Expand Down
44 changes: 41 additions & 3 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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<ShadowStyle>);

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<ShadowStyle> 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
Expand All @@ -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,
Expand Down
16 changes: 8 additions & 8 deletions examples/testbed/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
});
});

let shadow = BoxShadow {
let shadow_style = ShadowStyle {
color: Color::BLACK.with_alpha(0.5),
blur_radius: Val::Px(2.),
x_offset: Val::Px(10.),
Expand Down Expand Up @@ -218,7 +218,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
BackgroundColor(Color::srgb(1.0, 0.0, 0.)),
shadow,
BoxShadow::from(shadow_style),
))
.with_children(|parent| {
parent.spawn((
Expand All @@ -232,7 +232,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
BackgroundColor(Color::srgb(1.0, 0.3, 0.3)),
shadow,
BoxShadow::from(shadow_style),
));
parent.spawn((
Node {
Expand All @@ -244,7 +244,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
BackgroundColor(Color::srgb(1.0, 0.5, 0.5)),
shadow,
BoxShadow::from(shadow_style),
));
parent.spawn((
Node {
Expand All @@ -256,7 +256,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
BackgroundColor(Color::srgb(0.0, 0.7, 0.7)),
shadow,
BoxShadow::from(shadow_style),
));
// alpha test
parent.spawn((
Expand All @@ -269,10 +269,10 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..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
}),
));
});
});
Expand Down
61 changes: 54 additions & 7 deletions examples/ui/box_shadow.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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.),
},
]),
));
});
}

Expand All @@ -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),
),
)
}

0 comments on commit 56d5591

Please sign in to comment.