bevy_ui_gradients/
lib.rs

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