Skip to content

Commit bf20c63

Browse files
authored
UI Node Gradients (#18139)
# Objective Allowing drawing of UI nodes with a gradient instead of a flat color. ## Solution The are three gradient structs corresponding to the three types of gradients supported: `LinearGradient`, `ConicGradient` and `RadialGradient`. These are then wrapped in a `Gradient` enum discriminator which has `Linear`, `Conic` and `Radial` variants. Each gradient type consists of the geometric properties for that gradient and a list of color stops. Color stops consist of a color, a position or angle and an optional hint. If no position is specified for a stop, it's evenly spaced between the previous and following stops. Color stop positions are absolute, if you specify a list of stops: ```vec![vec![ColorStop::new(RED, Val::Percent(90.), ColorStop::new(Color::GREEN, Val::Percent(10.))``` the colors will be reordered and the gradient will transition from green at 10% to red at 90%. Colors are interpolated between the stops in SRGB space. The hint is a normalized value that can be used to shift the mid-point where the colors are mixed 50-50. between the stop with the hint and the following stop. For sharp stops with no interpolated transition, place two stops at the same position. `ConicGradient`s and RadialGradient`s have a center which is set using the new `Position` type. `Position` consists of a normalized (relative to the UI node) `Vec2` anchor point and a responsive x, y offset. To draw a UI node with a gradient you insert the components `BackgroundGradient` and `BorderGradient`, which both newtype a vector of `Gradient`s. If you set a background color, the background color is drawn first and the gradient(s) are drawn on top. The implementation is deliberately simple and self contained. The shader draws the gradient in multiple passes which is quite inefficient for gradients with a very large number of color stops. It's simple though and there won't be any compatibility issues. We could make gradients a specialization for `UiPipeline` but I used a separate pipeline plugin for now to ensure that these changes don't break anything. #### Not supported in this PR * Interpolation in other color spaces besides SRGB. * Images and text: This would need some breaking changes like a `UiColor` enum type with `Color` and `Gradient` variants, to enable `BorderColor`, `TextColor`, `BackgroundColor` and `ImageNode::color` to take either a `Color` or a gradient. * Repeating gradients ## Testing Includes three examples that can be used for testing: ``` cargo run --example linear_gradients cargo run --example stacked_gradients cargo run --example radial_gradients ``` Most of the code except the components API is contained within the `bevy_ui/src/render/linear_gradients` module. There are no changes to any existing systems or plugins except for the addition of the gradients rendering systems to the render world schedule and the `Val` changes from #18164 . ## Showcase ![gradients](https://github.com/user-attachments/assets/a09c5bb2-f9dc-4bc5-9d17-21a6338519d3) ![stacked](https://github.com/user-attachments/assets/7a1ad28e-8ae0-41d5-85b2-aa62647aef03) ![rad](https://github.com/user-attachments/assets/48609cf1-52aa-453c-afba-3b4845f3ddec) Conic gradients can be used to draw simple pie charts like in CSS: ![PIE](https://github.com/user-attachments/assets/4594b96f-52ab-4974-911a-16d065d213bc)
1 parent c62ca1a commit bf20c63

File tree

16 files changed

+2487
-131
lines changed

16 files changed

+2487
-131
lines changed

Cargo.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3418,6 +3418,39 @@ description = "An example for CSS Grid layout"
34183418
category = "UI (User Interface)"
34193419
wasm = true
34203420

3421+
[[example]]
3422+
name = "gradients"
3423+
path = "examples/ui/gradients.rs"
3424+
doc-scrape-examples = true
3425+
3426+
[package.metadata.example.gradients]
3427+
name = "Gradients"
3428+
description = "An example demonstrating gradients"
3429+
category = "UI (User Interface)"
3430+
wasm = true
3431+
3432+
[[example]]
3433+
name = "stacked_gradients"
3434+
path = "examples/ui/stacked_gradients.rs"
3435+
doc-scrape-examples = true
3436+
3437+
[package.metadata.example.stacked_gradients]
3438+
name = "Stacked Gradients"
3439+
description = "An example demonstrating stacked gradients"
3440+
category = "UI (User Interface)"
3441+
wasm = true
3442+
3443+
[[example]]
3444+
name = "radial_gradients"
3445+
path = "examples/ui/radial_gradients.rs"
3446+
doc-scrape-examples = true
3447+
3448+
[package.metadata.example.radial_gradients]
3449+
name = "Radial Gradients"
3450+
description = "An example demonstrating radial gradients"
3451+
category = "UI (User Interface)"
3452+
wasm = true
3453+
34213454
[[example]]
34223455
name = "scroll"
34233456
path = "examples/ui/scroll.rs"

crates/bevy_ui/src/geometry.rs

Lines changed: 216 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use bevy_math::Vec2;
22
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
3+
use bevy_utils::default;
34
use core::ops::{Div, DivAssign, Mul, MulAssign, Neg};
45
use thiserror::Error;
56

@@ -255,19 +256,23 @@ pub enum ValArithmeticError {
255256
}
256257

257258
impl Val {
258-
/// Resolves a [`Val`] from the given context values and returns this as an [`f32`].
259-
/// The [`Val::Px`] value (if present), `parent_size` and `viewport_size` should all be in the same coordinate space.
260-
/// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value.
259+
/// Resolves this [`Val`] to a value in physical pixels from the given `scale_factor`, `physical_base_value`,
260+
/// and `physical_target_size` context values.
261261
///
262-
/// **Note:** If a [`Val::Px`] is resolved, its inner value is returned unchanged.
263-
pub fn resolve(self, parent_size: f32, viewport_size: Vec2) -> Result<f32, ValArithmeticError> {
262+
/// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value.
263+
pub fn resolve(
264+
self,
265+
scale_factor: f32,
266+
physical_base_value: f32,
267+
physical_target_size: Vec2,
268+
) -> Result<f32, ValArithmeticError> {
264269
match self {
265-
Val::Percent(value) => Ok(parent_size * value / 100.0),
266-
Val::Px(value) => Ok(value),
267-
Val::Vw(value) => Ok(viewport_size.x * value / 100.0),
268-
Val::Vh(value) => Ok(viewport_size.y * value / 100.0),
269-
Val::VMin(value) => Ok(viewport_size.min_element() * value / 100.0),
270-
Val::VMax(value) => Ok(viewport_size.max_element() * value / 100.0),
270+
Val::Percent(value) => Ok(physical_base_value * value / 100.0),
271+
Val::Px(value) => Ok(value * scale_factor),
272+
Val::Vw(value) => Ok(physical_target_size.x * value / 100.0),
273+
Val::Vh(value) => Ok(physical_target_size.y * value / 100.0),
274+
Val::VMin(value) => Ok(physical_target_size.min_element() * value / 100.0),
275+
Val::VMax(value) => Ok(physical_target_size.max_element() * value / 100.0),
271276
Val::Auto => Err(ValArithmeticError::NonEvaluable),
272277
}
273278
}
@@ -678,6 +683,179 @@ impl Default for UiRect {
678683
}
679684
}
680685

686+
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
687+
#[reflect(Default, Debug, PartialEq)]
688+
#[cfg_attr(
689+
feature = "serialize",
690+
derive(serde::Serialize, serde::Deserialize),
691+
reflect(Serialize, Deserialize)
692+
)]
693+
/// Responsive position relative to a UI node.
694+
pub struct Position {
695+
/// Normalized anchor point
696+
pub anchor: Vec2,
697+
/// Responsive horizontal position relative to the anchor point
698+
pub x: Val,
699+
/// Responsive vertical position relative to the anchor point
700+
pub y: Val,
701+
}
702+
703+
impl Default for Position {
704+
fn default() -> Self {
705+
Self::CENTER
706+
}
707+
}
708+
709+
impl Position {
710+
/// Position at the given normalized anchor point
711+
pub const fn anchor(anchor: Vec2) -> Self {
712+
Self {
713+
anchor,
714+
x: Val::ZERO,
715+
y: Val::ZERO,
716+
}
717+
}
718+
719+
/// Position at the top-left corner
720+
pub const TOP_LEFT: Self = Self::anchor(Vec2::new(-0.5, -0.5));
721+
722+
/// Position at the center of the left edge
723+
pub const LEFT: Self = Self::anchor(Vec2::new(-0.5, 0.0));
724+
725+
/// Position at the bottom-left corner
726+
pub const BOTTOM_LEFT: Self = Self::anchor(Vec2::new(-0.5, 0.5));
727+
728+
/// Position at the center of the top edge
729+
pub const TOP: Self = Self::anchor(Vec2::new(0.0, -0.5));
730+
731+
/// Position at the center of the element
732+
pub const CENTER: Self = Self::anchor(Vec2::new(0.0, 0.0));
733+
734+
/// Position at the center of the bottom edge
735+
pub const BOTTOM: Self = Self::anchor(Vec2::new(0.0, 0.5));
736+
737+
/// Position at the top-right corner
738+
pub const TOP_RIGHT: Self = Self::anchor(Vec2::new(0.5, -0.5));
739+
740+
/// Position at the center of the right edge
741+
pub const RIGHT: Self = Self::anchor(Vec2::new(0.5, 0.0));
742+
743+
/// Position at the bottom-right corner
744+
pub const BOTTOM_RIGHT: Self = Self::anchor(Vec2::new(0.5, 0.5));
745+
746+
/// Create a new position
747+
pub const fn new(anchor: Vec2, x: Val, y: Val) -> Self {
748+
Self { anchor, x, y }
749+
}
750+
751+
/// Creates a position from self with the given `x` and `y` coordinates
752+
pub const fn at(self, x: Val, y: Val) -> Self {
753+
Self { x, y, ..self }
754+
}
755+
756+
/// Creates a position from self with the given `x` coordinate
757+
pub const fn at_x(self, x: Val) -> Self {
758+
Self { x, ..self }
759+
}
760+
761+
/// Creates a position from self with the given `y` coordinate
762+
pub const fn at_y(self, y: Val) -> Self {
763+
Self { y, ..self }
764+
}
765+
766+
/// Creates a position in logical pixels from self with the given `x` and `y` coordinates
767+
pub const fn at_px(self, x: f32, y: f32) -> Self {
768+
self.at(Val::Px(x), Val::Px(y))
769+
}
770+
771+
/// Creates a percentage position from self with the given `x` and `y` coordinates
772+
pub const fn at_percent(self, x: f32, y: f32) -> Self {
773+
self.at(Val::Percent(x), Val::Percent(y))
774+
}
775+
776+
/// Creates a position from self with the given `anchor` point
777+
pub const fn with_anchor(self, anchor: Vec2) -> Self {
778+
Self { anchor, ..self }
779+
}
780+
781+
/// Position relative to the top-left corner
782+
pub const fn top_left(x: Val, y: Val) -> Self {
783+
Self::TOP_LEFT.at(x, y)
784+
}
785+
786+
/// Position relative to the left edge
787+
pub const fn left(x: Val, y: Val) -> Self {
788+
Self::LEFT.at(x, y)
789+
}
790+
791+
/// Position relative to the bottom-left corner
792+
pub const fn bottom_left(x: Val, y: Val) -> Self {
793+
Self::BOTTOM_LEFT.at(x, y)
794+
}
795+
796+
/// Position relative to the top edge
797+
pub const fn top(x: Val, y: Val) -> Self {
798+
Self::TOP.at(x, y)
799+
}
800+
801+
/// Position relative to the center
802+
pub const fn center(x: Val, y: Val) -> Self {
803+
Self::CENTER.at(x, y)
804+
}
805+
806+
/// Position relative to the bottom edge
807+
pub const fn bottom(x: Val, y: Val) -> Self {
808+
Self::BOTTOM.at(x, y)
809+
}
810+
811+
/// Position relative to the top-right corner
812+
pub const fn top_right(x: Val, y: Val) -> Self {
813+
Self::TOP_RIGHT.at(x, y)
814+
}
815+
816+
/// Position relative to the right edge
817+
pub const fn right(x: Val, y: Val) -> Self {
818+
Self::RIGHT.at(x, y)
819+
}
820+
821+
/// Position relative to the bottom-right corner
822+
pub const fn bottom_right(x: Val, y: Val) -> Self {
823+
Self::BOTTOM_RIGHT.at(x, y)
824+
}
825+
826+
/// Resolves the `Position` into physical coordinates.
827+
pub fn resolve(
828+
self,
829+
scale_factor: f32,
830+
physical_size: Vec2,
831+
physical_target_size: Vec2,
832+
) -> Vec2 {
833+
let d = self.anchor.map(|p| if 0. < p { -1. } else { 1. });
834+
835+
physical_size * self.anchor
836+
+ d * Vec2::new(
837+
self.x
838+
.resolve(scale_factor, physical_size.x, physical_target_size)
839+
.unwrap_or(0.),
840+
self.y
841+
.resolve(scale_factor, physical_size.y, physical_target_size)
842+
.unwrap_or(0.),
843+
)
844+
}
845+
}
846+
847+
impl From<Val> for Position {
848+
fn from(x: Val) -> Self {
849+
Self { x, ..default() }
850+
}
851+
}
852+
853+
impl From<(Val, Val)> for Position {
854+
fn from((x, y): (Val, Val)) -> Self {
855+
Self { x, y, ..default() }
856+
}
857+
}
858+
681859
#[cfg(test)]
682860
mod tests {
683861
use crate::geometry::*;
@@ -687,7 +865,7 @@ mod tests {
687865
fn val_evaluate() {
688866
let size = 250.;
689867
let viewport_size = vec2(1000., 500.);
690-
let result = Val::Percent(80.).resolve(size, viewport_size).unwrap();
868+
let result = Val::Percent(80.).resolve(1., size, viewport_size).unwrap();
691869

692870
assert_eq!(result, size * 0.8);
693871
}
@@ -696,7 +874,7 @@ mod tests {
696874
fn val_resolve_px() {
697875
let size = 250.;
698876
let viewport_size = vec2(1000., 500.);
699-
let result = Val::Px(10.).resolve(size, viewport_size).unwrap();
877+
let result = Val::Px(10.).resolve(1., size, viewport_size).unwrap();
700878

701879
assert_eq!(result, 10.);
702880
}
@@ -709,33 +887,45 @@ mod tests {
709887
for value in (-10..10).map(|value| value as f32) {
710888
// for a square viewport there should be no difference between `Vw` and `Vh` and between `Vmin` and `Vmax`.
711889
assert_eq!(
712-
Val::Vw(value).resolve(size, viewport_size),
713-
Val::Vh(value).resolve(size, viewport_size)
890+
Val::Vw(value).resolve(1., size, viewport_size),
891+
Val::Vh(value).resolve(1., size, viewport_size)
714892
);
715893
assert_eq!(
716-
Val::VMin(value).resolve(size, viewport_size),
717-
Val::VMax(value).resolve(size, viewport_size)
894+
Val::VMin(value).resolve(1., size, viewport_size),
895+
Val::VMax(value).resolve(1., size, viewport_size)
718896
);
719897
assert_eq!(
720-
Val::VMin(value).resolve(size, viewport_size),
721-
Val::Vw(value).resolve(size, viewport_size)
898+
Val::VMin(value).resolve(1., size, viewport_size),
899+
Val::Vw(value).resolve(1., size, viewport_size)
722900
);
723901
}
724902

725903
let viewport_size = vec2(1000., 500.);
726-
assert_eq!(Val::Vw(100.).resolve(size, viewport_size).unwrap(), 1000.);
727-
assert_eq!(Val::Vh(100.).resolve(size, viewport_size).unwrap(), 500.);
728-
assert_eq!(Val::Vw(60.).resolve(size, viewport_size).unwrap(), 600.);
729-
assert_eq!(Val::Vh(40.).resolve(size, viewport_size).unwrap(), 200.);
730-
assert_eq!(Val::VMin(50.).resolve(size, viewport_size).unwrap(), 250.);
731-
assert_eq!(Val::VMax(75.).resolve(size, viewport_size).unwrap(), 750.);
904+
assert_eq!(
905+
Val::Vw(100.).resolve(1., size, viewport_size).unwrap(),
906+
1000.
907+
);
908+
assert_eq!(
909+
Val::Vh(100.).resolve(1., size, viewport_size).unwrap(),
910+
500.
911+
);
912+
assert_eq!(Val::Vw(60.).resolve(1., size, viewport_size).unwrap(), 600.);
913+
assert_eq!(Val::Vh(40.).resolve(1., size, viewport_size).unwrap(), 200.);
914+
assert_eq!(
915+
Val::VMin(50.).resolve(1., size, viewport_size).unwrap(),
916+
250.
917+
);
918+
assert_eq!(
919+
Val::VMax(75.).resolve(1., size, viewport_size).unwrap(),
920+
750.
921+
);
732922
}
733923

734924
#[test]
735925
fn val_auto_is_non_evaluable() {
736926
let size = 250.;
737927
let viewport_size = vec2(1000., 500.);
738-
let resolve_auto = Val::Auto.resolve(size, viewport_size);
928+
let resolve_auto = Val::Auto.resolve(1., size, viewport_size);
739929

740930
assert_eq!(resolve_auto, Err(ValArithmeticError::NonEvaluable));
741931
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy