bevy_ui/
gradients.rs

1use crate::{UiPosition, Val};
2use bevy_color::{Color, Srgba};
3use bevy_ecs::{component::Component, reflect::ReflectComponent};
4use bevy_math::Vec2;
5use bevy_reflect::prelude::*;
6use bevy_utils::default;
7use core::{f32, f32::consts::TAU};
8
9/// A color stop for a gradient
10#[derive(Debug, Copy, Clone, PartialEq, Reflect)]
11#[reflect(Default, PartialEq, Debug)]
12#[cfg_attr(
13    feature = "serialize",
14    derive(serde::Serialize, serde::Deserialize),
15    reflect(Serialize, Deserialize)
16)]
17pub struct ColorStop {
18    /// Color
19    pub color: Color,
20    /// Logical position along the gradient line.
21    /// Stop positions are relative to the start of the gradient and not other stops.
22    pub point: Val,
23    /// Normalized position between this and the following stop of the interpolation midpoint.
24    pub hint: f32,
25}
26
27impl ColorStop {
28    /// Create a new color stop
29    pub fn new(color: impl Into<Color>, point: Val) -> Self {
30        Self {
31            color: color.into(),
32            point,
33            hint: 0.5,
34        }
35    }
36
37    /// An automatic color stop.
38    /// The positions of automatic stops are interpolated evenly between explicit stops.
39    pub fn auto(color: impl Into<Color>) -> Self {
40        Self {
41            color: color.into(),
42            point: Val::Auto,
43            hint: 0.5,
44        }
45    }
46
47    /// A color stop with its position in logical pixels.
48    pub fn px(color: impl Into<Color>, px: f32) -> Self {
49        Self {
50            color: color.into(),
51            point: Val::Px(px),
52            hint: 0.5,
53        }
54    }
55
56    /// A color stop with a percentage position.
57    pub fn percent(color: impl Into<Color>, percent: f32) -> Self {
58        Self {
59            color: color.into(),
60            point: Val::Percent(percent),
61            hint: 0.5,
62        }
63    }
64
65    // Set the interpolation midpoint between this and the following stop
66    pub const fn with_hint(mut self, hint: f32) -> Self {
67        self.hint = hint;
68        self
69    }
70}
71
72impl From<(Color, Val)> for ColorStop {
73    fn from((color, stop): (Color, Val)) -> Self {
74        Self {
75            color,
76            point: stop,
77            hint: 0.5,
78        }
79    }
80}
81
82impl From<Color> for ColorStop {
83    fn from(color: Color) -> Self {
84        Self {
85            color,
86            point: Val::Auto,
87            hint: 0.5,
88        }
89    }
90}
91
92impl From<Srgba> for ColorStop {
93    fn from(color: Srgba) -> Self {
94        Self {
95            color: color.into(),
96            point: Val::Auto,
97            hint: 0.5,
98        }
99    }
100}
101
102impl Default for ColorStop {
103    fn default() -> Self {
104        Self {
105            color: Color::WHITE,
106            point: Val::Auto,
107            hint: 0.5,
108        }
109    }
110}
111
112/// An angular color stop for a conic gradient
113#[derive(Debug, Copy, Clone, PartialEq, Reflect)]
114#[reflect(Default, PartialEq, Debug)]
115#[cfg_attr(
116    feature = "serialize",
117    derive(serde::Serialize, serde::Deserialize),
118    reflect(Serialize, Deserialize)
119)]
120pub struct AngularColorStop {
121    /// Color of the stop
122    pub color: Color,
123    /// The angle of the stop.
124    /// Angles are relative to the start of the gradient and not other stops.
125    /// 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.
126    /// Given angles are clamped to between `0.`, and [`TAU`].
127    /// This means that a list of stops:
128    /// ```
129    /// # use std::f32::consts::TAU;
130    /// # use bevy_ui::AngularColorStop;
131    /// # use bevy_color::{Color, palettes::css::{RED, BLUE}};
132    /// let stops = [
133    ///     AngularColorStop::new(Color::WHITE, 0.),
134    ///     AngularColorStop::new(Color::BLACK, -1.),
135    ///     AngularColorStop::new(RED, 2. * TAU),
136    ///     AngularColorStop::new(BLUE, TAU),
137    /// ];
138    /// ```
139    /// is equivalent to:
140    /// ```
141    /// # use std::f32::consts::TAU;
142    /// # use bevy_ui::AngularColorStop;
143    /// # use bevy_color::{Color, palettes::css::{RED, BLUE}};
144    /// let stops = [
145    ///     AngularColorStop::new(Color::WHITE, 0.),
146    ///     AngularColorStop::new(Color::BLACK, 0.),
147    ///     AngularColorStop::new(RED, TAU),
148    ///     AngularColorStop::new(BLUE, TAU),
149    /// ];
150    /// ```
151    /// Resulting in a black to red gradient, not white to blue.
152    pub angle: Option<f32>,
153    /// Normalized angle between this and the following stop of the interpolation midpoint.
154    pub hint: f32,
155}
156
157impl AngularColorStop {
158    // Create a new color stop
159    pub fn new(color: impl Into<Color>, angle: f32) -> Self {
160        Self {
161            color: color.into(),
162            angle: Some(angle),
163            hint: 0.5,
164        }
165    }
166
167    /// An angular stop without an explicit angle. The angles of automatic stops
168    /// are interpolated evenly between explicit stops.
169    pub fn auto(color: impl Into<Color>) -> Self {
170        Self {
171            color: color.into(),
172            angle: None,
173            hint: 0.5,
174        }
175    }
176
177    // Set the interpolation midpoint between this and the following stop
178    pub const fn with_hint(mut self, hint: f32) -> Self {
179        self.hint = hint;
180        self
181    }
182}
183
184impl From<(Color, f32)> for AngularColorStop {
185    fn from((color, angle): (Color, f32)) -> Self {
186        Self {
187            color,
188            angle: Some(angle),
189            hint: 0.5,
190        }
191    }
192}
193
194impl From<Color> for AngularColorStop {
195    fn from(color: Color) -> Self {
196        Self {
197            color,
198            angle: None,
199            hint: 0.5,
200        }
201    }
202}
203
204impl From<Srgba> for AngularColorStop {
205    fn from(color: Srgba) -> Self {
206        Self {
207            color: color.into(),
208            angle: None,
209            hint: 0.5,
210        }
211    }
212}
213
214impl Default for AngularColorStop {
215    fn default() -> Self {
216        Self {
217            color: Color::WHITE,
218            angle: None,
219            hint: 0.5,
220        }
221    }
222}
223
224/// A linear gradient
225///
226/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
227#[derive(Default, Clone, PartialEq, Debug, Reflect)]
228#[reflect(PartialEq)]
229#[cfg_attr(
230    feature = "serialize",
231    derive(serde::Serialize, serde::Deserialize),
232    reflect(Serialize, Deserialize)
233)]
234pub struct LinearGradient {
235    /// The color space used for interpolation.
236    pub color_space: InterpolationColorSpace,
237    /// The direction of the gradient in radians.
238    /// An angle of `0.` points upward, with the value increasing in the clockwise direction.
239    pub angle: f32,
240    /// The list of color stops
241    pub stops: Vec<ColorStop>,
242}
243
244impl LinearGradient {
245    /// Angle of a linear gradient transitioning from bottom to top
246    pub const TO_TOP: f32 = 0.;
247    /// Angle of a linear gradient transitioning from bottom-left to top-right
248    pub const TO_TOP_RIGHT: f32 = TAU / 8.;
249    /// Angle of a linear gradient transitioning from left to right
250    pub const TO_RIGHT: f32 = 2. * Self::TO_TOP_RIGHT;
251    /// Angle of a linear gradient transitioning from top-left to bottom-right
252    pub const TO_BOTTOM_RIGHT: f32 = 3. * Self::TO_TOP_RIGHT;
253    /// Angle of a linear gradient transitioning from top to bottom
254    pub const TO_BOTTOM: f32 = 4. * Self::TO_TOP_RIGHT;
255    /// Angle of a linear gradient transitioning from top-right to bottom-left
256    pub const TO_BOTTOM_LEFT: f32 = 5. * Self::TO_TOP_RIGHT;
257    /// Angle of a linear gradient transitioning from right to left
258    pub const TO_LEFT: f32 = 6. * Self::TO_TOP_RIGHT;
259    /// Angle of a linear gradient transitioning from bottom-right to top-left
260    pub const TO_TOP_LEFT: f32 = 7. * Self::TO_TOP_RIGHT;
261
262    /// Create a new linear gradient
263    pub fn new(angle: f32, stops: Vec<ColorStop>) -> Self {
264        Self {
265            angle,
266            stops,
267            color_space: InterpolationColorSpace::default(),
268        }
269    }
270
271    /// A linear gradient transitioning from bottom to top
272    pub fn to_top(stops: Vec<ColorStop>) -> Self {
273        Self {
274            angle: Self::TO_TOP,
275            stops,
276            color_space: InterpolationColorSpace::default(),
277        }
278    }
279
280    /// A linear gradient transitioning from bottom-left to top-right
281    pub fn to_top_right(stops: Vec<ColorStop>) -> Self {
282        Self {
283            angle: Self::TO_TOP_RIGHT,
284            stops,
285            color_space: InterpolationColorSpace::default(),
286        }
287    }
288
289    /// A linear gradient transitioning from left to right
290    pub fn to_right(stops: Vec<ColorStop>) -> Self {
291        Self {
292            angle: Self::TO_RIGHT,
293            stops,
294            color_space: InterpolationColorSpace::default(),
295        }
296    }
297
298    /// A linear gradient transitioning from top-left to bottom-right
299    pub fn to_bottom_right(stops: Vec<ColorStop>) -> Self {
300        Self {
301            angle: Self::TO_BOTTOM_RIGHT,
302            stops,
303            color_space: InterpolationColorSpace::default(),
304        }
305    }
306
307    /// A linear gradient transitioning from top to bottom
308    pub fn to_bottom(stops: Vec<ColorStop>) -> Self {
309        Self {
310            angle: Self::TO_BOTTOM,
311            stops,
312            color_space: InterpolationColorSpace::default(),
313        }
314    }
315
316    /// A linear gradient transitioning from top-right to bottom-left
317    pub fn to_bottom_left(stops: Vec<ColorStop>) -> Self {
318        Self {
319            angle: Self::TO_BOTTOM_LEFT,
320            stops,
321            color_space: InterpolationColorSpace::default(),
322        }
323    }
324
325    /// A linear gradient transitioning from right to left
326    pub fn to_left(stops: Vec<ColorStop>) -> Self {
327        Self {
328            angle: Self::TO_LEFT,
329            stops,
330            color_space: InterpolationColorSpace::default(),
331        }
332    }
333
334    /// A linear gradient transitioning from bottom-right to top-left
335    pub fn to_top_left(stops: Vec<ColorStop>) -> Self {
336        Self {
337            angle: Self::TO_TOP_LEFT,
338            stops,
339            color_space: InterpolationColorSpace::default(),
340        }
341    }
342
343    /// A linear gradient with the given angle in degrees
344    pub fn degrees(degrees: f32, stops: Vec<ColorStop>) -> Self {
345        Self {
346            angle: degrees.to_radians(),
347            stops,
348            color_space: InterpolationColorSpace::default(),
349        }
350    }
351
352    pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
353        self.color_space = color_space;
354        self
355    }
356}
357
358/// A radial gradient
359///
360/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/radial-gradient>
361#[derive(Clone, PartialEq, Debug, Reflect)]
362#[reflect(PartialEq)]
363#[cfg_attr(
364    feature = "serialize",
365    derive(serde::Serialize, serde::Deserialize),
366    reflect(Serialize, Deserialize)
367)]
368pub struct RadialGradient {
369    /// The color space used for interpolation.
370    pub color_space: InterpolationColorSpace,
371    /// The center of the radial gradient
372    pub position: UiPosition,
373    /// Defines the end shape of the radial gradient
374    pub shape: RadialGradientShape,
375    /// The list of color stops
376    pub stops: Vec<ColorStop>,
377}
378
379impl RadialGradient {
380    /// Create a new radial gradient
381    pub fn new(position: UiPosition, shape: RadialGradientShape, stops: Vec<ColorStop>) -> Self {
382        Self {
383            color_space: default(),
384            position,
385            shape,
386            stops,
387        }
388    }
389
390    pub const fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
391        self.color_space = color_space;
392        self
393    }
394}
395
396impl Default for RadialGradient {
397    fn default() -> Self {
398        Self {
399            position: UiPosition::CENTER,
400            shape: RadialGradientShape::ClosestCorner,
401            stops: Vec::new(),
402            color_space: default(),
403        }
404    }
405}
406
407/// A conic gradient
408///
409/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient>
410#[derive(Default, Clone, PartialEq, Debug, Reflect)]
411#[reflect(PartialEq)]
412#[cfg_attr(
413    feature = "serialize",
414    derive(serde::Serialize, serde::Deserialize),
415    reflect(Serialize, Deserialize)
416)]
417pub struct ConicGradient {
418    /// The color space used for interpolation.
419    pub color_space: InterpolationColorSpace,
420    /// The starting angle of the gradient in radians
421    pub start: f32,
422    /// The center of the conic gradient
423    pub position: UiPosition,
424    /// The list of color stops
425    pub stops: Vec<AngularColorStop>,
426}
427
428impl ConicGradient {
429    /// Create a new conic gradient
430    pub fn new(position: UiPosition, stops: Vec<AngularColorStop>) -> Self {
431        Self {
432            color_space: default(),
433            start: 0.,
434            position,
435            stops,
436        }
437    }
438
439    /// Sets the starting angle of the gradient in radians
440    pub const fn with_start(mut self, start: f32) -> Self {
441        self.start = start;
442        self
443    }
444
445    /// Sets the position of the gradient
446    pub const fn with_position(mut self, position: UiPosition) -> Self {
447        self.position = position;
448        self
449    }
450
451    pub const fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
452        self.color_space = color_space;
453        self
454    }
455}
456
457#[derive(Clone, PartialEq, Debug, Reflect)]
458#[reflect(PartialEq)]
459#[cfg_attr(
460    feature = "serialize",
461    derive(serde::Serialize, serde::Deserialize),
462    reflect(Serialize, Deserialize)
463)]
464pub enum Gradient {
465    /// A linear gradient
466    ///
467    /// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
468    Linear(LinearGradient),
469    /// A radial gradient
470    ///
471    /// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/radial-gradient>
472    Radial(RadialGradient),
473    /// A conic gradient
474    ///
475    /// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient>
476    Conic(ConicGradient),
477}
478
479impl Gradient {
480    /// Returns true if the gradient has no stops.
481    pub const fn is_empty(&self) -> bool {
482        match self {
483            Gradient::Linear(gradient) => gradient.stops.is_empty(),
484            Gradient::Radial(gradient) => gradient.stops.is_empty(),
485            Gradient::Conic(gradient) => gradient.stops.is_empty(),
486        }
487    }
488
489    /// If the gradient has only a single color stop, `get_single` returns its color.
490    pub fn get_single(&self) -> Option<Color> {
491        match self {
492            Gradient::Linear(gradient) => gradient
493                .stops
494                .first()
495                .and_then(|stop| (gradient.stops.len() == 1).then_some(stop.color)),
496            Gradient::Radial(gradient) => gradient
497                .stops
498                .first()
499                .and_then(|stop| (gradient.stops.len() == 1).then_some(stop.color)),
500            Gradient::Conic(gradient) => gradient
501                .stops
502                .first()
503                .and_then(|stop| (gradient.stops.len() == 1).then_some(stop.color)),
504        }
505    }
506}
507
508impl From<LinearGradient> for Gradient {
509    fn from(value: LinearGradient) -> Self {
510        Self::Linear(value)
511    }
512}
513
514impl From<RadialGradient> for Gradient {
515    fn from(value: RadialGradient) -> Self {
516        Self::Radial(value)
517    }
518}
519
520impl From<ConicGradient> for Gradient {
521    fn from(value: ConicGradient) -> Self {
522        Self::Conic(value)
523    }
524}
525
526#[derive(Component, Clone, PartialEq, Debug, Default, Reflect)]
527#[reflect(Component, Default, PartialEq, Debug, Clone)]
528#[cfg_attr(
529    feature = "serialize",
530    derive(serde::Serialize, serde::Deserialize),
531    reflect(Serialize, Deserialize)
532)]
533/// A UI node that displays a gradient
534pub struct BackgroundGradient(pub Vec<Gradient>);
535
536impl<T: Into<Gradient>> From<T> for BackgroundGradient {
537    fn from(value: T) -> Self {
538        Self(vec![value.into()])
539    }
540}
541
542#[derive(Component, Clone, PartialEq, Debug, Default, Reflect)]
543#[reflect(Component, Default, PartialEq, Debug, Clone)]
544#[cfg_attr(
545    feature = "serialize",
546    derive(serde::Serialize, serde::Deserialize),
547    reflect(Serialize, Deserialize)
548)]
549/// A UI node border that displays a gradient
550pub struct BorderGradient(pub Vec<Gradient>);
551
552impl<T: Into<Gradient>> From<T> for BorderGradient {
553    fn from(value: T) -> Self {
554        Self(vec![value.into()])
555    }
556}
557
558#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)]
559#[reflect(PartialEq, Default)]
560#[cfg_attr(
561    feature = "serialize",
562    derive(serde::Serialize, serde::Deserialize),
563    reflect(Serialize, Deserialize)
564)]
565pub enum RadialGradientShape {
566    /// A circle with radius equal to the distance from its center to the closest side
567    ClosestSide,
568    /// A circle with radius equal to the distance from its center to the farthest side
569    FarthestSide,
570    /// An ellipse with extents equal to the distance from its center to the nearest corner
571    #[default]
572    ClosestCorner,
573    /// An ellipse with extents equal to the distance from its center to the farthest corner
574    FarthestCorner,
575    /// A circle
576    Circle(Val),
577    /// An ellipse
578    Ellipse(Val, Val),
579}
580
581const fn close_side(p: f32, h: f32) -> f32 {
582    (-h - p).abs().min((h - p).abs())
583}
584
585const fn far_side(p: f32, h: f32) -> f32 {
586    (-h - p).abs().max((h - p).abs())
587}
588
589const fn close_side2(p: Vec2, h: Vec2) -> f32 {
590    close_side(p.x, h.x).min(close_side(p.y, h.y))
591}
592
593const fn far_side2(p: Vec2, h: Vec2) -> f32 {
594    far_side(p.x, h.x).max(far_side(p.y, h.y))
595}
596
597impl RadialGradientShape {
598    /// Resolve the physical dimensions of the end shape of the radial gradient
599    pub fn resolve(
600        self,
601        position: Vec2,
602        scale_factor: f32,
603        physical_size: Vec2,
604        physical_target_size: Vec2,
605    ) -> Vec2 {
606        let half_size = 0.5 * physical_size;
607        match self {
608            RadialGradientShape::ClosestSide => Vec2::splat(close_side2(position, half_size)),
609            RadialGradientShape::FarthestSide => Vec2::splat(far_side2(position, half_size)),
610            RadialGradientShape::ClosestCorner => Vec2::new(
611                close_side(position.x, half_size.x),
612                close_side(position.y, half_size.y),
613            ),
614            RadialGradientShape::FarthestCorner => Vec2::new(
615                far_side(position.x, half_size.x),
616                far_side(position.y, half_size.y),
617            ),
618            RadialGradientShape::Circle(radius) => Vec2::splat(
619                radius
620                    .resolve(scale_factor, physical_size.x, physical_target_size)
621                    .unwrap_or(0.),
622            ),
623            RadialGradientShape::Ellipse(x, y) => Vec2::new(
624                x.resolve(scale_factor, physical_size.x, physical_target_size)
625                    .unwrap_or(0.),
626                y.resolve(scale_factor, physical_size.y, physical_target_size)
627                    .unwrap_or(0.),
628            ),
629        }
630    }
631}
632
633/// The color space used for interpolation.
634#[derive(Default, Copy, Clone, Hash, Debug, PartialEq, Eq, Reflect)]
635#[cfg_attr(
636    feature = "serialize",
637    derive(serde::Serialize, serde::Deserialize),
638    reflect(Serialize, Deserialize)
639)]
640pub enum InterpolationColorSpace {
641    /// Interpolates in OKLABA space.
642    #[default]
643    Oklaba,
644    /// Interpolates in OKLCHA space, taking the shortest hue path.
645    Oklcha,
646    /// Interpolates in OKLCHA space, taking the longest hue path.
647    OklchaLong,
648    /// Interpolates in sRGBA space.
649    Srgba,
650    /// Interpolates in linear sRGBA space.
651    LinearRgba,
652    /// Interpolates in HSLA space, taking the shortest hue path.
653    Hsla,
654    /// Interpolates in HSLA space, taking the longest hue path.
655    HslaLong,
656    /// Interpolates in HSVA space, taking the shortest hue path.
657    Hsva,
658    /// Interpolates in HSVA space, taking the longest hue path.
659    HsvaLong,
660}
661
662/// Set the color space used for interpolation.
663pub trait InColorSpace: Sized {
664    /// Interpolate in the given `color_space`.
665    fn in_color_space(self, color_space: InterpolationColorSpace) -> Self;
666
667    /// Interpolate in `OKLab` space.
668    fn in_oklaba(self) -> Self {
669        self.in_color_space(InterpolationColorSpace::Oklaba)
670    }
671
672    /// Interpolate in OKLCH space (short hue path).
673    fn in_oklch(self) -> Self {
674        self.in_color_space(InterpolationColorSpace::Oklcha)
675    }
676
677    /// Interpolate in OKLCH space (long hue path).
678    fn in_oklch_long(self) -> Self {
679        self.in_color_space(InterpolationColorSpace::OklchaLong)
680    }
681
682    /// Interpolate in sRGB space.
683    fn in_srgb(self) -> Self {
684        self.in_color_space(InterpolationColorSpace::Srgba)
685    }
686
687    /// Interpolate in linear sRGB space.
688    fn in_linear_rgb(self) -> Self {
689        self.in_color_space(InterpolationColorSpace::LinearRgba)
690    }
691}
692
693impl InColorSpace for LinearGradient {
694    /// Interpolate in the given `color_space`.
695    fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
696        self.color_space = color_space;
697        self
698    }
699}
700
701impl InColorSpace for RadialGradient {
702    /// Interpolate in the given `color_space`.
703    fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
704        self.color_space = color_space;
705        self
706    }
707}
708
709impl InColorSpace for ConicGradient {
710    /// Interpolate in the given `color_space`.
711    fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
712        self.color_space = color_space;
713        self
714    }
715}