feathers/
feathers.rs

1//! This example shows off the various Bevy Feathers widgets.
2
3use bevy::{
4    color::palettes,
5    feathers::{
6        controls::{
7            button, checkbox, color_slider, color_swatch, radio, slider, toggle_switch,
8            ButtonProps, ButtonVariant, ColorChannel, ColorSlider, ColorSliderProps, ColorSwatch,
9            SliderBaseColor, SliderProps,
10        },
11        dark_theme::create_dark_theme,
12        rounded_corners::RoundedCorners,
13        theme::{ThemeBackgroundColor, ThemedText, UiTheme},
14        tokens, FeathersPlugins,
15    },
16    input_focus::tab_navigation::TabGroup,
17    prelude::*,
18    ui::{Checked, InteractionDisabled},
19    ui_widgets::{
20        checkbox_self_update, observe, slider_self_update, Activate, RadioButton, RadioGroup,
21        SliderPrecision, SliderStep, SliderValue, ValueChange,
22    },
23};
24
25/// A struct to hold the state of various widgets shown in the demo.
26#[derive(Resource)]
27struct DemoWidgetStates {
28    rgb_color: Srgba,
29    hsl_color: Hsla,
30}
31
32#[derive(Component, Clone, Copy, PartialEq)]
33enum SwatchType {
34    Rgb,
35    Hsl,
36}
37
38#[derive(Component, Clone, Copy)]
39struct DemoDisabledButton;
40
41fn main() {
42    App::new()
43        .add_plugins((DefaultPlugins, FeathersPlugins))
44        .insert_resource(UiTheme(create_dark_theme()))
45        .insert_resource(DemoWidgetStates {
46            rgb_color: palettes::tailwind::EMERALD_800.with_alpha(0.7),
47            hsl_color: palettes::tailwind::AMBER_800.into(),
48        })
49        .add_systems(Startup, setup)
50        .add_systems(Update, update_colors)
51        .run();
52}
53
54fn setup(mut commands: Commands) {
55    // ui camera
56    commands.spawn(Camera2d);
57    commands.spawn(demo_root());
58}
59
60fn demo_root() -> impl Bundle {
61    (
62        Node {
63            width: percent(100),
64            height: percent(100),
65            align_items: AlignItems::Start,
66            justify_content: JustifyContent::Start,
67            display: Display::Flex,
68            flex_direction: FlexDirection::Column,
69            row_gap: px(10),
70            ..default()
71        },
72        TabGroup::default(),
73        ThemeBackgroundColor(tokens::WINDOW_BG),
74        children![(
75            Node {
76                display: Display::Flex,
77                flex_direction: FlexDirection::Column,
78                align_items: AlignItems::Stretch,
79                justify_content: JustifyContent::Start,
80                padding: UiRect::all(px(8)),
81                row_gap: px(8),
82                width: percent(30),
83                min_width: px(200),
84                ..default()
85            },
86            children![
87                (
88                    Node {
89                        display: Display::Flex,
90                        flex_direction: FlexDirection::Row,
91                        align_items: AlignItems::Center,
92                        justify_content: JustifyContent::Start,
93                        column_gap: px(8),
94                        ..default()
95                    },
96                    children![
97                        (
98                            button(
99                                ButtonProps::default(),
100                                (),
101                                Spawn((Text::new("Normal"), ThemedText))
102                            ),
103                            observe(|_activate: On<Activate>| {
104                                info!("Normal button clicked!");
105                            })
106                        ),
107                        (
108                            button(
109                                ButtonProps::default(),
110                                (InteractionDisabled, DemoDisabledButton),
111                                Spawn((Text::new("Disabled"), ThemedText))
112                            ),
113                            observe(|_activate: On<Activate>| {
114                                info!("Disabled button clicked!");
115                            })
116                        ),
117                        (
118                            button(
119                                ButtonProps {
120                                    variant: ButtonVariant::Primary,
121                                    ..default()
122                                },
123                                (),
124                                Spawn((Text::new("Primary"), ThemedText))
125                            ),
126                            observe(|_activate: On<Activate>| {
127                                info!("Disabled button clicked!");
128                            })
129                        ),
130                    ]
131                ),
132                (
133                    Node {
134                        display: Display::Flex,
135                        flex_direction: FlexDirection::Row,
136                        align_items: AlignItems::Center,
137                        justify_content: JustifyContent::Start,
138                        column_gap: px(1),
139                        ..default()
140                    },
141                    children![
142                        (
143                            button(
144                                ButtonProps {
145                                    corners: RoundedCorners::Left,
146                                    ..default()
147                                },
148                                (),
149                                Spawn((Text::new("Left"), ThemedText))
150                            ),
151                            observe(|_activate: On<Activate>| {
152                                info!("Left button clicked!");
153                            })
154                        ),
155                        (
156                            button(
157                                ButtonProps {
158                                    corners: RoundedCorners::None,
159                                    ..default()
160                                },
161                                (),
162                                Spawn((Text::new("Center"), ThemedText))
163                            ),
164                            observe(|_activate: On<Activate>| {
165                                info!("Center button clicked!");
166                            })
167                        ),
168                        (
169                            button(
170                                ButtonProps {
171                                    variant: ButtonVariant::Primary,
172                                    corners: RoundedCorners::Right,
173                                },
174                                (),
175                                Spawn((Text::new("Right"), ThemedText))
176                            ),
177                            observe(|_activate: On<Activate>| {
178                                info!("Right button clicked!");
179                            })
180                        ),
181                    ]
182                ),
183                (
184                    button(
185                        ButtonProps::default(),
186                        (),
187                        Spawn((Text::new("Button"), ThemedText))
188                    ),
189                    observe(|_activate: On<Activate>| {
190                        info!("Wide button clicked!");
191                    })
192                ),
193                (
194                    checkbox(Checked, Spawn((Text::new("Checkbox"), ThemedText))),
195                    observe(
196                        |change: On<ValueChange<bool>>,
197                         query: Query<Entity, With<DemoDisabledButton>>,
198                         mut commands: Commands| {
199                            info!("Checkbox clicked!");
200                            let mut button = commands.entity(query.single().unwrap());
201                            if change.value {
202                                button.insert(InteractionDisabled);
203                            } else {
204                                button.remove::<InteractionDisabled>();
205                            }
206                            let mut checkbox = commands.entity(change.source);
207                            if change.value {
208                                checkbox.insert(Checked);
209                            } else {
210                                checkbox.remove::<Checked>();
211                            }
212                        }
213                    )
214                ),
215                (
216                    checkbox(
217                        InteractionDisabled,
218                        Spawn((Text::new("Disabled"), ThemedText))
219                    ),
220                    observe(|_change: On<ValueChange<bool>>| {
221                        warn!("Disabled checkbox clicked!");
222                    })
223                ),
224                (
225                    checkbox(
226                        (InteractionDisabled, Checked),
227                        Spawn((Text::new("Disabled+Checked"), ThemedText))
228                    ),
229                    observe(|_change: On<ValueChange<bool>>| {
230                        warn!("Disabled checkbox clicked!");
231                    })
232                ),
233                (
234                    Node {
235                        display: Display::Flex,
236                        flex_direction: FlexDirection::Column,
237                        row_gap: px(4),
238                        ..default()
239                    },
240                    RadioGroup,
241                    observe(
242                        |value_change: On<ValueChange<Entity>>,
243                         q_radio: Query<Entity, With<RadioButton>>,
244                         mut commands: Commands| {
245                            for radio in q_radio.iter() {
246                                if radio == value_change.value {
247                                    commands.entity(radio).insert(Checked);
248                                } else {
249                                    commands.entity(radio).remove::<Checked>();
250                                }
251                            }
252                        }
253                    ),
254                    children![
255                        radio(Checked, Spawn((Text::new("One"), ThemedText))),
256                        radio((), Spawn((Text::new("Two"), ThemedText))),
257                        radio((), Spawn((Text::new("Three"), ThemedText))),
258                        radio(
259                            InteractionDisabled,
260                            Spawn((Text::new("Disabled"), ThemedText))
261                        ),
262                    ]
263                ),
264                (
265                    Node {
266                        display: Display::Flex,
267                        flex_direction: FlexDirection::Row,
268                        align_items: AlignItems::Center,
269                        justify_content: JustifyContent::Start,
270                        column_gap: px(8),
271                        ..default()
272                    },
273                    children![
274                        (toggle_switch((),), observe(checkbox_self_update)),
275                        (
276                            toggle_switch(InteractionDisabled,),
277                            observe(checkbox_self_update)
278                        ),
279                        (
280                            toggle_switch((InteractionDisabled, Checked),),
281                            observe(checkbox_self_update)
282                        ),
283                    ]
284                ),
285                (
286                    slider(
287                        SliderProps {
288                            max: 100.0,
289                            value: 20.0,
290                            ..default()
291                        },
292                        (SliderStep(10.), SliderPrecision(2)),
293                    ),
294                    observe(slider_self_update)
295                ),
296                (
297                    Node {
298                        display: Display::Flex,
299                        flex_direction: FlexDirection::Row,
300                        justify_content: JustifyContent::SpaceBetween,
301                        ..default()
302                    },
303                    children![Text("Srgba".to_owned()), color_swatch(SwatchType::Rgb),]
304                ),
305                (
306                    color_slider(
307                        ColorSliderProps {
308                            value: 0.5,
309                            channel: ColorChannel::Red
310                        },
311                        ()
312                    ),
313                    observe(
314                        |change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
315                            color.rgb_color.red = change.value;
316                        }
317                    )
318                ),
319                (
320                    color_slider(
321                        ColorSliderProps {
322                            value: 0.5,
323                            channel: ColorChannel::Green
324                        },
325                        ()
326                    ),
327                    observe(
328                        |change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
329                            color.rgb_color.green = change.value;
330                        },
331                    )
332                ),
333                (
334                    color_slider(
335                        ColorSliderProps {
336                            value: 0.5,
337                            channel: ColorChannel::Blue
338                        },
339                        ()
340                    ),
341                    observe(
342                        |change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
343                            color.rgb_color.blue = change.value;
344                        },
345                    )
346                ),
347                (
348                    color_slider(
349                        ColorSliderProps {
350                            value: 0.5,
351                            channel: ColorChannel::Alpha
352                        },
353                        ()
354                    ),
355                    observe(
356                        |change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
357                            color.rgb_color.alpha = change.value;
358                        },
359                    )
360                ),
361                (
362                    Node {
363                        display: Display::Flex,
364                        flex_direction: FlexDirection::Row,
365                        justify_content: JustifyContent::SpaceBetween,
366                        ..default()
367                    },
368                    children![Text("Hsl".to_owned()), color_swatch(SwatchType::Hsl),]
369                ),
370                (
371                    color_slider(
372                        ColorSliderProps {
373                            value: 0.5,
374                            channel: ColorChannel::HslHue
375                        },
376                        ()
377                    ),
378                    observe(
379                        |change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
380                            color.hsl_color.hue = change.value;
381                        },
382                    )
383                ),
384                (
385                    color_slider(
386                        ColorSliderProps {
387                            value: 0.5,
388                            channel: ColorChannel::HslSaturation
389                        },
390                        ()
391                    ),
392                    observe(
393                        |change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
394                            color.hsl_color.saturation = change.value;
395                        },
396                    )
397                ),
398                (
399                    color_slider(
400                        ColorSliderProps {
401                            value: 0.5,
402                            channel: ColorChannel::HslLightness
403                        },
404                        ()
405                    ),
406                    observe(
407                        |change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
408                            color.hsl_color.lightness = change.value;
409                        },
410                    )
411                )
412            ]
413        ),],
414    )
415}
416
417fn update_colors(
418    colors: Res<DemoWidgetStates>,
419    mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>,
420    swatches: Query<(&SwatchType, &Children), With<ColorSwatch>>,
421    mut commands: Commands,
422) {
423    if colors.is_changed() {
424        for (slider_ent, slider, mut base) in sliders.iter_mut() {
425            match slider.channel {
426                ColorChannel::Red => {
427                    base.0 = colors.rgb_color.into();
428                    commands
429                        .entity(slider_ent)
430                        .insert(SliderValue(colors.rgb_color.red));
431                }
432                ColorChannel::Green => {
433                    base.0 = colors.rgb_color.into();
434                    commands
435                        .entity(slider_ent)
436                        .insert(SliderValue(colors.rgb_color.green));
437                }
438                ColorChannel::Blue => {
439                    base.0 = colors.rgb_color.into();
440                    commands
441                        .entity(slider_ent)
442                        .insert(SliderValue(colors.rgb_color.blue));
443                }
444                ColorChannel::HslHue => {
445                    base.0 = colors.hsl_color.into();
446                    commands
447                        .entity(slider_ent)
448                        .insert(SliderValue(colors.hsl_color.hue));
449                }
450                ColorChannel::HslSaturation => {
451                    base.0 = colors.hsl_color.into();
452                    commands
453                        .entity(slider_ent)
454                        .insert(SliderValue(colors.hsl_color.saturation));
455                }
456                ColorChannel::HslLightness => {
457                    base.0 = colors.hsl_color.into();
458                    commands
459                        .entity(slider_ent)
460                        .insert(SliderValue(colors.hsl_color.lightness));
461                }
462                ColorChannel::Alpha => {
463                    base.0 = colors.rgb_color.into();
464                    commands
465                        .entity(slider_ent)
466                        .insert(SliderValue(colors.rgb_color.alpha));
467                }
468            }
469        }
470
471        for (swatch_type, children) in swatches.iter() {
472            commands
473                .entity(children[0])
474                .insert(BackgroundColor(match swatch_type {
475                    SwatchType::Rgb => colors.rgb_color.into(),
476                    SwatchType::Hsl => colors.hsl_color.into(),
477                }));
478        }
479    }
480}