Skip to main content

gradients/
gradients.rs

1//! Simple example demonstrating linear gradients.
2
3use bevy::{
4    color::palettes::css::{BLUE, GREEN, INDIGO, LIME, ORANGE, RED, VIOLET, YELLOW},
5    prelude::*,
6    ui::ColorStop,
7    ui_widgets::{Activate, Button},
8};
9use std::f32::consts::TAU;
10
11const COLOR_SPACES: [InterpolationColorSpace; 11] = [
12    InterpolationColorSpace::Oklaba,
13    InterpolationColorSpace::Oklcha,
14    InterpolationColorSpace::OklchaLong,
15    InterpolationColorSpace::Srgba,
16    InterpolationColorSpace::LinearRgba,
17    InterpolationColorSpace::Hsla,
18    InterpolationColorSpace::HslaLong,
19    InterpolationColorSpace::Hsva,
20    InterpolationColorSpace::HsvaLong,
21    InterpolationColorSpace::Okhsla,
22    InterpolationColorSpace::OkhslaLong,
23];
24
25/// Marker component for the previous button
26#[derive(Component, Clone, Default)]
27struct PreviousButton;
28
29/// Marker component for the next button
30#[derive(Component, Clone, Default)]
31struct NextButton;
32
33/// Marker component for the current color space label
34#[derive(Component, Clone, Default)]
35struct CurrentColorSpaceLabel;
36
37/// Resource that holds the current settings for the app
38#[derive(Resource, Default)]
39struct AppSettings {
40    /// The currently shown color space as an index into `COLOR_SPACES`
41    color_space_current_index: usize,
42}
43
44fn main() {
45    App::new()
46        .add_plugins(DefaultPlugins)
47        .init_resource::<AppSettings>()
48        .add_systems(Startup, setup)
49        .add_systems(Update, update)
50        .add_observer(on_activate_change_space)
51        .run();
52}
53
54fn setup(mut commands: Commands) {
55    commands.spawn(Camera2d);
56
57    let buttons_id = commands.spawn_scene(buttons_scene()).id();
58
59    commands
60        .spawn(Node {
61            flex_direction: FlexDirection::Column,
62            row_gap: px(20),
63            margin: UiRect::all(px(20)),
64            ..Default::default()
65        })
66        .with_children(|commands| {
67            for (b, stops) in [
68                (
69                    4.,
70                    vec![
71                        ColorStop::new(Color::WHITE, percent(15)),
72                        ColorStop::new(Color::BLACK, percent(85)),
73                    ],
74                ),
75                (4., vec![RED.into(), BLUE.into(), LIME.into()]),
76                (
77                    0.,
78                    vec![
79                        RED.into(),
80                        ColorStop::new(RED, percent(100. / 7.)),
81                        ColorStop::new(ORANGE, percent(100. / 7.)),
82                        ColorStop::new(ORANGE, percent(200. / 7.)),
83                        ColorStop::new(YELLOW, percent(200. / 7.)),
84                        ColorStop::new(YELLOW, percent(300. / 7.)),
85                        ColorStop::new(GREEN, percent(300. / 7.)),
86                        ColorStop::new(GREEN, percent(400. / 7.)),
87                        ColorStop::new(BLUE, percent(400. / 7.)),
88                        ColorStop::new(BLUE, percent(500. / 7.)),
89                        ColorStop::new(INDIGO, percent(500. / 7.)),
90                        ColorStop::new(INDIGO, percent(600. / 7.)),
91                        ColorStop::new(VIOLET, percent(600. / 7.)),
92                        VIOLET.into(),
93                    ],
94                ),
95            ] {
96                commands.spawn(Node::default()).with_children(|commands| {
97                    commands
98                        .spawn(Node {
99                            flex_direction: FlexDirection::Column,
100                            row_gap: px(5),
101                            ..Default::default()
102                        })
103                        .with_children(|commands| {
104                            for (w, h) in [(70., 70.), (35., 70.), (70., 35.)] {
105                                commands
106                                    .spawn(Node {
107                                        column_gap: px(10),
108                                        ..Default::default()
109                                    })
110                                    .with_children(|commands| {
111                                        for angle in (0..8).map(|i| i as f32 * TAU / 8.) {
112                                            commands.spawn((
113                                                Node {
114                                                    width: px(w),
115                                                    height: px(h),
116                                                    border: UiRect::all(px(b)),
117                                                    border_radius: BorderRadius::all(px(20)),
118                                                    ..default()
119                                                },
120                                                BackgroundGradient::from(LinearGradient {
121                                                    angle,
122                                                    stops: stops.clone(),
123                                                    ..default()
124                                                }),
125                                                BorderGradient::from(LinearGradient {
126                                                    angle: 3. * TAU / 8.,
127                                                    stops: vec![
128                                                        YELLOW.into(),
129                                                        Color::WHITE.into(),
130                                                        ORANGE.into(),
131                                                    ],
132                                                    ..default()
133                                                }),
134                                            ));
135                                        }
136                                    });
137                            }
138                        });
139
140                    commands.spawn(Node::default()).with_children(|commands| {
141                        commands.spawn((
142                            Node {
143                                aspect_ratio: Some(1.),
144                                height: percent(100),
145                                border: UiRect::all(px(b)),
146                                margin: UiRect::left(px(20)),
147                                border_radius: BorderRadius::all(px(20)),
148                                ..default()
149                            },
150                            BackgroundGradient::from(LinearGradient {
151                                angle: 0.,
152                                stops: stops.clone(),
153                                ..default()
154                            }),
155                            BorderGradient::from(LinearGradient {
156                                angle: 3. * TAU / 8.,
157                                stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
158                                ..default()
159                            }),
160                            AnimateMarker,
161                        ));
162
163                        commands.spawn((
164                            Node {
165                                aspect_ratio: Some(1.),
166                                height: percent(100),
167                                border: UiRect::all(px(b)),
168                                margin: UiRect::left(px(20)),
169                                border_radius: BorderRadius::all(px(20)),
170                                ..default()
171                            },
172                            BackgroundGradient::from(RadialGradient {
173                                stops: stops.clone(),
174                                shape: RadialGradientShape::ClosestSide,
175                                position: UiPosition::CENTER,
176                                ..default()
177                            }),
178                            BorderGradient::from(LinearGradient {
179                                angle: 3. * TAU / 8.,
180                                stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
181                                ..default()
182                            }),
183                            AnimateMarker,
184                        ));
185                        commands.spawn((
186                            Node {
187                                aspect_ratio: Some(1.),
188                                height: percent(100),
189                                border: UiRect::all(px(b)),
190                                margin: UiRect::left(px(20)),
191                                border_radius: BorderRadius::all(px(20)),
192                                ..default()
193                            },
194                            BackgroundGradient::from(ConicGradient {
195                                start: 0.,
196                                stops: stops
197                                    .iter()
198                                    .map(|stop| AngularColorStop::auto(stop.color))
199                                    .collect(),
200                                position: UiPosition::CENTER,
201                                ..default()
202                            }),
203                            BorderGradient::from(LinearGradient {
204                                angle: 3. * TAU / 8.,
205                                stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
206                                ..default()
207                            }),
208                            AnimateMarker,
209                        ));
210                    });
211                });
212            }
213        })
214        .add_child(buttons_id);
215}
216
217/// Scene of the current color space, the previous button, and the next button.
218/// The user can cycle through the color spaces by clicking the buttons.
219fn buttons_scene() -> impl Scene {
220    bsn! {
221        Node {
222            flex_direction: FlexDirection::Row,
223            align_items: AlignItems::Center,
224            justify_content: JustifyContent::Center,
225            column_gap: px(20),
226        }
227        Children [
228            Node {
229                align_items: AlignItems::Center,
230                justify_content: JustifyContent::Center,
231            }
232            Children [
233                CurrentColorSpaceLabel
234                template(|ctx| {
235                    let current_index = ctx.resource::<AppSettings>().color_space_current_index;
236                    Ok(Text(format!("Current Space\n{:?}", COLOR_SPACES[current_index])))
237                })
238            ]
239            --
240            PreviousButton
241            @button_node_scene("Previous")
242            --
243            NextButton
244            @button_node_scene("Next")
245        ]
246    }
247}
248
249/// Base Button node scene
250fn button_node_scene(caption: &'static str) -> impl Scene {
251    bsn! {
252        Button
253        Node {
254            border: UiRect::all(px(2)),
255            padding: UiRect::axes(px(8), px(4)),
256            // horizontally center child text
257            justify_content: JustifyContent::Center,
258            // vertically center child text
259            align_items: AlignItems::Center,
260            border_radius: BorderRadius::MAX,
261        }
262        BorderColor::all(Color::WHITE)
263        BackgroundColor(Color::BLACK)
264        on(|event: On<PointerOver>, mut border_query: Query<&mut BorderColor, With<Button>>| {
265            *border_query.get_mut(event.entity).unwrap() = BorderColor::all(RED);
266        })
267        on(|event: On<PointerOut>, mut border_query: Query<&mut BorderColor, With<Button>>| {
268            *border_query.get_mut(event.entity).unwrap() = BorderColor::all(Color::WHITE);
269        })
270        Children [
271            Text(caption)
272        ]
273    }
274}
275
276/// Observer that handles a button press from the previous/next buttons.
277fn on_activate_change_space(
278    event: On<Activate>,
279    mut app_settings: ResMut<AppSettings>,
280    button_type_q: Query<(Has<PreviousButton>, Has<NextButton>), With<Button>>,
281    mut gradients_query: Query<&mut BackgroundGradient>,
282    mut label_q: Query<&mut Text, With<CurrentColorSpaceLabel>>,
283) {
284    let Ok((has_previous, has_next)) = button_type_q.get(event.entity) else {
285        return;
286    };
287    let next_index = match (has_previous, has_next) {
288        (true, true) | (false, false) => return,
289        (true, false) => {
290            if app_settings.color_space_current_index == 0 {
291                COLOR_SPACES.len() - 1
292            } else {
293                app_settings.color_space_current_index - 1
294            }
295        }
296        (false, true) => (app_settings.color_space_current_index + 1) % COLOR_SPACES.len(),
297    };
298    app_settings.color_space_current_index = next_index;
299
300    // Set the current space label and update the visuals.
301    let next_space = COLOR_SPACES[app_settings.color_space_current_index];
302    for mut label in label_q.iter_mut() {
303        label.0 = format!("Current Space\n{next_space:?}");
304    }
305    for mut gradients in gradients_query.iter_mut() {
306        for gradient in gradients.0.iter_mut() {
307            let space = match gradient {
308                Gradient::Linear(linear_gradient) => &mut linear_gradient.color_space,
309                Gradient::Radial(radial_gradient) => &mut radial_gradient.color_space,
310                Gradient::Conic(conic_gradient) => &mut conic_gradient.color_space,
311            };
312            *space = next_space;
313        }
314    }
315}
316
317#[derive(Component)]
318struct AnimateMarker;
319
320fn update(time: Res<Time>, mut query: Query<&mut BackgroundGradient, With<AnimateMarker>>) {
321    for mut gradients in query.iter_mut() {
322        for gradient in gradients.0.iter_mut() {
323            if let Gradient::Linear(LinearGradient { angle, .. }) = gradient {
324                *angle += 0.5 * time.delta_secs();
325            }
326        }
327    }
328}