use crate::{UiPosition, Val}; use bevy_color::{Color, Srgba}; use bevy_ecs::component::Component; use bevy_math::Vec2; use bevy_reflect::prelude::*; use core::{f32, f32::consts::TAU}; /// A color stop for a gradient #[derive(Debug, Copy, Clone, PartialEq, Reflect)] #[reflect(Default, PartialEq, Debug)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] pub struct ColorStop { /// Color pub color: Color, /// Logical position along the gradient line. /// Stop positions are relative to the start of the gradient and not other stops. pub point: Val, /// Normalized position between this and the following stop of the interpolation midpoint. pub hint: f32, } impl ColorStop { /// Create a new color stop pub fn new(color: impl Into, point: Val) -> Self { Self { color: color.into(), point, hint: 0.5, } } /// An automatic color stop. /// The positions of automatic stops are interpolated evenly between explicit stops. pub fn auto(color: impl Into) -> Self { Self { color: color.into(), point: Val::Auto, hint: 0.5, } } // Set the interpolation midpoint between this and the following stop pub fn with_hint(mut self, hint: f32) -> Self { self.hint = hint; self } } impl From<(Color, Val)> for ColorStop { fn from((color, stop): (Color, Val)) -> Self { Self { color, point: stop, hint: 0.5, } } } impl From for ColorStop { fn from(color: Color) -> Self { Self { color, point: Val::Auto, hint: 0.5, } } } impl From for ColorStop { fn from(color: Srgba) -> Self { Self { color: color.into(), point: Val::Auto, hint: 0.5, } } } impl Default for ColorStop { fn default() -> Self { Self { color: Color::WHITE, point: Val::Auto, hint: 0.5, } } } /// An angular color stop for a conic gradient #[derive(Debug, Copy, Clone, PartialEq, Reflect)] #[reflect(Default, PartialEq, Debug)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] pub struct AngularColorStop { /// Color of the stop pub color: Color, /// The angle of the stop. /// Angles are relative to the start of the gradient and not other stops. /// If set to `None` the angle of the stop will be interpolated between the explicit stops or 0 and 2 PI degrees if there no explicit stops. /// Given angles are clamped to between `0.`, and [`TAU`]. /// This means that a list of stops: /// ``` /// # use std::f32::consts::TAU; /// # use bevy_ui::AngularColorStop; /// # use bevy_color::{Color, palettes::css::{RED, BLUE}}; /// let stops = [ /// AngularColorStop::new(Color::WHITE, 0.), /// AngularColorStop::new(Color::BLACK, -1.), /// AngularColorStop::new(RED, 2. * TAU), /// AngularColorStop::new(BLUE, TAU), /// ]; /// ``` /// is equivalent to: /// ``` /// # use std::f32::consts::TAU; /// # use bevy_ui::AngularColorStop; /// # use bevy_color::{Color, palettes::css::{RED, BLUE}}; /// let stops = [ /// AngularColorStop::new(Color::WHITE, 0.), /// AngularColorStop::new(Color::BLACK, 0.), /// AngularColorStop::new(RED, TAU), /// AngularColorStop::new(BLUE, TAU), /// ]; /// ``` /// Resulting in a black to red gradient, not white to blue. pub angle: Option, /// Normalized angle between this and the following stop of the interpolation midpoint. pub hint: f32, } impl AngularColorStop { // Create a new color stop pub fn new(color: impl Into, angle: f32) -> Self { Self { color: color.into(), angle: Some(angle), hint: 0.5, } } /// An angular stop without an explicit angle. The angles of automatic stops /// are interpolated evenly between explicit stops. pub fn auto(color: impl Into) -> Self { Self { color: color.into(), angle: None, hint: 0.5, } } // Set the interpolation midpoint between this and the following stop pub fn with_hint(mut self, hint: f32) -> Self { self.hint = hint; self } } impl From<(Color, f32)> for AngularColorStop { fn from((color, angle): (Color, f32)) -> Self { Self { color, angle: Some(angle), hint: 0.5, } } } impl From for AngularColorStop { fn from(color: Color) -> Self { Self { color, angle: None, hint: 0.5, } } } impl From for AngularColorStop { fn from(color: Srgba) -> Self { Self { color: color.into(), angle: None, hint: 0.5, } } } impl Default for AngularColorStop { fn default() -> Self { Self { color: Color::WHITE, angle: None, hint: 0.5, } } } /// A linear gradient /// /// #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] pub struct LinearGradient { /// The direction of the gradient in radians. /// An angle of `0.` points upward, with the value increasing in the clockwise direction. pub angle: f32, /// The list of color stops pub stops: Vec, } impl LinearGradient { /// Angle of a linear gradient transitioning from bottom to top pub const TO_TOP: f32 = 0.; /// Angle of a linear gradient transitioning from bottom-left to top-right pub const TO_TOP_RIGHT: f32 = TAU / 8.; /// Angle of a linear gradient transitioning from left to right pub const TO_RIGHT: f32 = 2. * Self::TO_TOP_RIGHT; /// Angle of a linear gradient transitioning from top-left to bottom-right pub const TO_BOTTOM_RIGHT: f32 = 3. * Self::TO_TOP_RIGHT; /// Angle of a linear gradient transitioning from top to bottom pub const TO_BOTTOM: f32 = 4. * Self::TO_TOP_RIGHT; /// Angle of a linear gradient transitioning from top-right to bottom-left pub const TO_BOTTOM_LEFT: f32 = 5. * Self::TO_TOP_RIGHT; /// Angle of a linear gradient transitioning from right to left pub const TO_LEFT: f32 = 6. * Self::TO_TOP_RIGHT; /// Angle of a linear gradient transitioning from bottom-right to top-left pub const TO_TOP_LEFT: f32 = 7. * Self::TO_TOP_RIGHT; /// Create a new linear gradient pub fn new(angle: f32, stops: Vec) -> Self { Self { angle, stops } } /// A linear gradient transitioning from bottom to top pub fn to_top(stops: Vec) -> Self { Self { angle: Self::TO_TOP, stops, } } /// A linear gradient transitioning from bottom-left to top-right pub fn to_top_right(stops: Vec) -> Self { Self { angle: Self::TO_TOP_RIGHT, stops, } } /// A linear gradient transitioning from left to right pub fn to_right(stops: Vec) -> Self { Self { angle: Self::TO_RIGHT, stops, } } /// A linear gradient transitioning from top-left to bottom-right pub fn to_bottom_right(stops: Vec) -> Self { Self { angle: Self::TO_BOTTOM_RIGHT, stops, } } /// A linear gradient transitioning from top to bottom pub fn to_bottom(stops: Vec) -> Self { Self { angle: Self::TO_BOTTOM, stops, } } /// A linear gradient transitioning from top-right to bottom-left pub fn to_bottom_left(stops: Vec) -> Self { Self { angle: Self::TO_BOTTOM_LEFT, stops, } } /// A linear gradient transitioning from right to left pub fn to_left(stops: Vec) -> Self { Self { angle: Self::TO_LEFT, stops, } } /// A linear gradient transitioning from bottom-right to top-left pub fn to_top_left(stops: Vec) -> Self { Self { angle: Self::TO_TOP_LEFT, stops, } } /// A linear gradient with the given angle in degrees pub fn degrees(degrees: f32, stops: Vec) -> Self { Self { angle: degrees.to_radians(), stops, } } } /// A radial gradient /// /// #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] pub struct RadialGradient { /// The center of the radial gradient pub position: UiPosition, /// Defines the end shape of the radial gradient pub shape: RadialGradientShape, /// The list of color stops pub stops: Vec, } impl RadialGradient { /// Create a new radial gradient pub fn new(position: UiPosition, shape: RadialGradientShape, stops: Vec) -> Self { Self { position, shape, stops, } } } impl Default for RadialGradient { fn default() -> Self { Self { position: UiPosition::CENTER, shape: RadialGradientShape::ClosestCorner, stops: Vec::new(), } } } /// A conic gradient /// /// #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] pub struct ConicGradient { /// The starting angle of the gradient in radians pub start: f32, /// The center of the conic gradient pub position: UiPosition, /// The list of color stops pub stops: Vec, } impl ConicGradient { /// Create a new conic gradient pub fn new(position: UiPosition, stops: Vec) -> Self { Self { start: 0., position, stops, } } /// Sets the starting angle of the gradient in radians pub fn with_start(mut self, start: f32) -> Self { self.start = start; self } /// Sets the position of the gradient pub fn with_position(mut self, position: UiPosition) -> Self { self.position = position; self } } #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] pub enum Gradient { /// A linear gradient /// /// Linear(LinearGradient), /// A radial gradient /// /// Radial(RadialGradient), /// A conic gradient /// /// Conic(ConicGradient), } impl Gradient { /// Returns true if the gradient has no stops. pub fn is_empty(&self) -> bool { match self { Gradient::Linear(gradient) => gradient.stops.is_empty(), Gradient::Radial(gradient) => gradient.stops.is_empty(), Gradient::Conic(gradient) => gradient.stops.is_empty(), } } /// If the gradient has only a single color stop, `get_single` returns its color. pub fn get_single(&self) -> Option { match self { Gradient::Linear(gradient) => gradient .stops .first() .and_then(|stop| (gradient.stops.len() == 1).then_some(stop.color)), Gradient::Radial(gradient) => gradient .stops .first() .and_then(|stop| (gradient.stops.len() == 1).then_some(stop.color)), Gradient::Conic(gradient) => gradient .stops .first() .and_then(|stop| (gradient.stops.len() == 1).then_some(stop.color)), } } } impl From for Gradient { fn from(value: LinearGradient) -> Self { Self::Linear(value) } } impl From for Gradient { fn from(value: RadialGradient) -> Self { Self::Radial(value) } } impl From for Gradient { fn from(value: ConicGradient) -> Self { Self::Conic(value) } } #[derive(Component, Clone, PartialEq, Debug, Default, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] /// A UI node that displays a gradient pub struct BackgroundGradient(pub Vec); impl> From for BackgroundGradient { fn from(value: T) -> Self { Self(vec![value.into()]) } } #[derive(Component, Clone, PartialEq, Debug, Default, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] /// A UI node border that displays a gradient pub struct BorderGradient(pub Vec); impl> From for BorderGradient { fn from(value: T) -> Self { Self(vec![value.into()]) } } #[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq, Default)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] pub enum RadialGradientShape { /// A circle with radius equal to the distance from its center to the closest side ClosestSide, /// A circle with radius equal to the distance from its center to the farthest side FarthestSide, /// An ellipse with extents equal to the distance from its center to the nearest corner #[default] ClosestCorner, /// An ellipse with extents equal to the distance from its center to the farthest corner FarthestCorner, /// A circle Circle(Val), /// An ellipse Ellipse(Val, Val), } fn close_side(p: f32, h: f32) -> f32 { (-h - p).abs().min((h - p).abs()) } fn far_side(p: f32, h: f32) -> f32 { (-h - p).abs().max((h - p).abs()) } fn close_side2(p: Vec2, h: Vec2) -> f32 { close_side(p.x, h.x).min(close_side(p.y, h.y)) } fn far_side2(p: Vec2, h: Vec2) -> f32 { far_side(p.x, h.x).max(far_side(p.y, h.y)) } impl RadialGradientShape { /// Resolve the physical dimensions of the end shape of the radial gradient pub fn resolve( self, position: Vec2, scale_factor: f32, physical_size: Vec2, physical_target_size: Vec2, ) -> Vec2 { let half_size = 0.5 * physical_size; match self { RadialGradientShape::ClosestSide => Vec2::splat(close_side2(position, half_size)), RadialGradientShape::FarthestSide => Vec2::splat(far_side2(position, half_size)), RadialGradientShape::ClosestCorner => Vec2::new( close_side(position.x, half_size.x), close_side(position.y, half_size.y), ), RadialGradientShape::FarthestCorner => Vec2::new( far_side(position.x, half_size.x), far_side(position.y, half_size.y), ), RadialGradientShape::Circle(radius) => Vec2::splat( radius .resolve(scale_factor, physical_size.x, physical_target_size) .unwrap_or(0.), ), RadialGradientShape::Ellipse(x, y) => Vec2::new( x.resolve(scale_factor, physical_size.x, physical_target_size) .unwrap_or(0.), y.resolve(scale_factor, physical_size.y, physical_target_size) .unwrap_or(0.), ), } } }