Skip to main content

feathers_gallery/
feathers_gallery.rs

1//! This example shows off the various Bevy Feathers widgets.
2
3use bevy::{
4    color::palettes,
5    feathers::{
6        constants::{fonts, icons},
7        containers::*,
8        controls::*,
9        dark_theme::create_dark_theme,
10        display::{caption, icon, label, label_dim, label_small},
11        font_styles::InheritableFont,
12        palette,
13        rounded_corners::RoundedCorners,
14        theme::{ThemeBackgroundColor, UiTheme},
15        tokens, FeathersPlugins,
16    },
17    input_focus::{tab_navigation::TabGroup, AutoFocus, InputFocus},
18    picking::cursor::{EntityCursor, OverrideCursor},
19    prelude::*,
20    text::{EditableText, TextEdit, TextEditChange},
21    ui::{Checked, InteractionDisabled, Selected},
22    ui_widgets::{
23        checkbox_self_update, listbox_update_selection,
24        popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide},
25        radio_self_update, slider_self_update, Activate, ActivateOnPress, RadioGroup, RequestClose,
26        SliderPrecision, SliderStep, SliderValue, ValueChange,
27    },
28    window::SystemCursorIcon,
29};
30use std::sync::Arc;
31
32/// A struct to hold the state of various widgets shown in the demo.
33#[derive(Resource)]
34struct DemoWidgetStates {
35    rgb_color: Srgba,
36    hsl_color: Hsla,
37    okhsl_color: Okhsla,
38    scalar_prop: f32,
39    vec3_prop: Vec3,
40}
41
42#[derive(Component, Clone, Copy, PartialEq, Default)]
43enum SwatchType {
44    #[default]
45    Rgb,
46    Hsl,
47    Okhsl,
48}
49
50#[derive(Component, Clone, Copy, Default)]
51struct HexColorInput;
52
53#[derive(Component, Clone, Copy, Default)]
54struct DemoDisabledButton;
55
56#[derive(Component, Clone, Copy, Default)]
57struct DemoDialogToggle;
58
59#[derive(Component, Clone, Copy, Default)]
60struct DemoScalarField;
61
62#[derive(Component, Clone, Copy, Default)]
63enum DemoVec3Field {
64    #[default]
65    X,
66    Y,
67    Z,
68}
69
70fn main() {
71    App::new()
72        .add_plugins((DefaultPlugins, FeathersPlugins))
73        .insert_resource(UiTheme(create_dark_theme()))
74        .insert_resource(DemoWidgetStates {
75            rgb_color: palettes::tailwind::EMERALD_800.with_alpha(0.7),
76            hsl_color: palettes::tailwind::AMBER_800.into(),
77            okhsl_color: palettes::tailwind::AMBER_800.into(),
78            scalar_prop: 7.0,
79            vec3_prop: Vec3::new(10.1, 7.124, 100.0),
80        })
81        .add_systems(Startup, scene.spawn())
82        .add_systems(Update, update_colors)
83        .run();
84}
85
86fn scene() -> impl SceneList {
87    bsn_list! {
88        Camera2d
89        --
90        @demo_root()
91    }
92}
93
94fn demo_root() -> impl Scene {
95    bsn! {
96        Node {
97            width: percent(100),
98            height: percent(100),
99            align_items: AlignItems::Start,
100            justify_content: JustifyContent::Start,
101            display: Display::Flex,
102            flex_direction: FlexDirection::Row,
103            column_gap: px(8),
104        }
105        TabGroup
106        ThemeBackgroundColor(tokens::WINDOW_BG)
107        Children [
108            @demo_column_1()
109            --
110            @demo_column_2()
111            --
112            @demo_column_3()
113        ]
114    }
115}
116
117#[derive(Component, Debug, Clone, Default, PartialEq)]
118enum Months {
119    #[default]
120    Jan,
121    Feb,
122    Mar,
123    Apr,
124    May,
125    Jun,
126    Jul,
127    Aug,
128    Sep,
129    Oct,
130    Nov,
131    Dec,
132}
133
134impl Months {
135    const ALL: [Months; 12] = [
136        Months::Jan,
137        Months::Feb,
138        Months::Mar,
139        Months::Apr,
140        Months::May,
141        Months::Jun,
142        Months::Jul,
143        Months::Aug,
144        Months::Sep,
145        Months::Oct,
146        Months::Nov,
147        Months::Dec,
148    ];
149
150    fn to_str(&self) -> &'static str {
151        match self {
152            Months::Jan => "January",
153            Months::Feb => "February",
154            Months::Mar => "March",
155            Months::Apr => "April",
156            Months::May => "May",
157            Months::Jun => "June",
158            Months::Jul => "July",
159            Months::Aug => "August",
160            Months::Sep => "September",
161            Months::Oct => "October",
162            Months::Nov => "November",
163            Months::Dec => "December",
164        }
165    }
166}
167
168fn demo_column_1() -> impl Scene {
169    // Lazily-constructed menu popup
170    let popup: Arc<dyn Fn() -> Box<dyn Scene> + Sync + Send> = Arc::new(|| {
171        Box::new(bsn! {
172            @FeathersMenuPopup
173            // Override popover placement to right-align the popup
174            Popover {
175                positions: vec![
176                    PopoverPlacement {
177                        side: PopoverSide::Bottom,
178                        align: PopoverAlign::End,
179                        gap: 2.0,
180                    },
181                    PopoverPlacement {
182                        side: PopoverSide::Top,
183                        align: PopoverAlign::End,
184                        gap: 2.0,
185                    },
186                ],
187                window_margin: 10.0,
188            }
189            Children [
190                @FeathersMenuItem {
191                    @caption: bsn! { @caption("MenuItem 4") }
192                }
193                on(|_: On<Activate>| {
194                    info!("Menu item 4 clicked!");
195                })
196                --
197                @FeathersMenuItem {
198                    @caption: bsn! { @caption("MenuItem 5") }
199                }
200                on(|_: On<Activate>| {
201                    info!("Menu item 5 clicked!");
202                })
203                --
204                @FeathersMenuItem {
205                    @caption: bsn! { @caption("MenuItem 6") }
206                }
207                on(|_: On<Activate>| {
208                    info!("Menu item 6 clicked!");
209                })
210            ]
211        })
212    });
213
214    bsn! {
215        Node {
216            display: Display::Flex,
217            flex_direction: FlexDirection::Column,
218            align_items: AlignItems::Stretch,
219            justify_content: JustifyContent::Start,
220            padding: px(8),
221            row_gap: px(8),
222            width: percent(30),
223            min_width: px(200),
224        }
225        Children [
226            Node {
227                display: Display::Flex,
228                flex_direction: FlexDirection::Row,
229                align_items: AlignItems::Center,
230                justify_content: JustifyContent::Start,
231                column_gap: px(8),
232            }
233            Children [
234                @FeathersButton {
235                    @caption: bsn! { @caption("Normal") }
236                }
237                Node {
238                    flex_grow: 1.0,
239                }
240                AccessibleLabel("Normal")
241                on(|_activate: On<Activate>| {
242                    info!("Normal button clicked!");
243                })
244                AutoFocus
245                --
246                @FeathersButton {
247                    @caption: bsn! { @caption("Disabled") },
248                }
249                Node {
250                    flex_grow: 1.0,
251                }
252                AccessibleLabel("Disabled")
253                InteractionDisabled
254                DemoDisabledButton
255                on(|_activate: On<Activate>| {
256                    info!("Disabled button clicked!");
257                })
258                --
259                @FeathersButton {
260                    @caption: bsn! { @caption("Primary") },
261                    @variant: ButtonVariant::Primary,
262                }
263                AccessibleLabel("Primary")
264                Node {
265                    flex_grow: 1.0,
266                }
267                on(|_activate: On<Activate>| {
268                    info!("Primary button clicked!");
269                })
270                --
271                @FeathersMenu
272                Children [
273                    @FeathersMenuButton {
274                        @caption: bsn! { @caption("Menu") }
275                    }
276                    AccessibleLabel("Menu Example")
277                    Node {
278                        flex_grow: 1.0,
279                    }
280                    --
281                    @FeathersMenuPopup
282                    Children [
283                        @FeathersMenuItem {
284                            @caption: bsn! { @caption("MenuItem 1") }
285                        }
286                        on(|_: On<Activate>| {
287                            info!("Menu item 1 clicked!");
288                        })
289                        --
290                        @FeathersMenuItem {
291                            @caption: bsn! { @caption("MenuItem 2") }
292                        }
293                        on(|_: On<Activate>| {
294                            info!("Menu item 2 clicked!");
295                        })
296                        --
297                        @FeathersMenuDivider
298                        --
299                        @FeathersMenuItem {
300                            @caption: bsn! { @caption("MenuItem 3") }
301                        }
302                        on(|_: On<Activate>| {
303                            info!("Menu item 3 clicked!");
304                        })
305                    ]
306                ]
307                --
308                @FeathersLazyMenu { popup }
309                Children [
310                    @FeathersMenuToolButton {
311                        @caption: bsn! { @caption("\u{0398}") }
312                    }
313                    AccessibleLabel("Menu Example")
314                    Node {
315                        flex_grow: 1.0,
316                    }
317                ]
318            ]
319            --
320            @FeathersSelect {
321                @options: list_rows_from_strings([
322                    "One",
323                    "Two",
324                    "Three",
325                ], Some(0)),
326            }
327            Node {
328                flex_grow: 1.0,
329            }
330            on(|change: On<ValueChange<Entity>>, q_options: Query<&OptionIndex>| {
331                let Ok(option) = q_options.get(change.value) else {
332                    info!("Select changed, not sure");
333                    return;
334                };
335                info!("Select changed to index {}", option.0);
336            })
337            --
338            @FeathersSelect {
339                @options: {
340                    Box::new(
341                        Months::ALL
342                            .into_iter()
343                            .map(|m| -> Box<dyn SceneList> {
344                                let label = m.to_str();
345                                if m == Months::default() {
346                                    bsn! { @FeathersListRow Selected m Children [ @caption(label) ] }.into()
347                                } else {
348                                    bsn! { @FeathersListRow m Children [ @caption(label) ] }.into()
349                                }
350                            })
351                            .collect::<Vec<_>>(),
352                    ) as Box<dyn SceneList>
353                },
354                @max_visible: 6,
355            }
356            Node {
357                flex_grow: 1.0,
358            }
359            on(|change: On<ValueChange<Entity>>, q_months: Query<&Months>| {
360                let Ok(month) = q_months.get(change.value) else {
361                    return;
362                };
363                info!("Select changed to {:?}", month);
364            })
365            --
366            Node {
367                display: Display::Flex,
368                flex_direction: FlexDirection::Row,
369                align_items: AlignItems::Center,
370                justify_content: JustifyContent::Start,
371                column_gap: px(1),
372            }
373            Children [
374                @FeathersButton {
375                    @caption: bsn! { @caption("Left") },
376                    @corners: RoundedCorners::Left,
377                }
378                Node {
379                    flex_grow: 1.0,
380                }
381                AccessibleLabel("Left")
382                on(|_activate: On<Activate>| {
383                    info!("Left button clicked!");
384                })
385                --
386                @FeathersButton {
387                    @caption: bsn! { @caption("Center") },
388                    @corners: RoundedCorners::None,
389                }
390                Node {
391                    flex_grow: 1.0,
392                }
393                AccessibleLabel("Center")
394                on(|_activate: On<Activate>| {
395                    info!("Center button clicked!");
396                })
397                --
398                @FeathersButton {
399                    @caption: bsn! { @caption("Right") },
400                    @variant: ButtonVariant::Primary,
401                    @corners: RoundedCorners::Right,
402                }
403                Node {
404                    flex_grow: 1.0,
405                }
406                AccessibleLabel("Right")
407                on(|_activate: On<Activate>| {
408                    info!("Right button clicked!");
409                })
410            ]
411            --
412            Node {
413                display: Display::Flex,
414                flex_direction: FlexDirection::Row,
415                align_items: AlignItems::Center,
416                justify_content: JustifyContent::Start,
417                column_gap: px(8),
418            }
419            Children [
420                @FeathersButton {
421                    @caption: bsn! { @caption("Toggle override") },
422                }
423                Node {
424                    flex_grow: 1.0,
425                }
426                on(|_activate: On<Activate>, mut ovr: ResMut<OverrideCursor>| {
427                    ovr.0 = if ovr.0.is_some() {
428                        None
429                    } else {
430                        Some(EntityCursor::System(SystemCursorIcon::Wait))
431                    };
432                    info!("Override cursor button clicked!");
433                })
434                --
435                @FeathersButton {
436                    @caption: bsn! { @caption("Quit\u{2026}") },
437                }
438                Node {
439                    flex_grow: 1.0,
440                }
441                on(spawn_quit_dialog)
442            ]
443            --
444            Node {
445                display: Display::Flex,
446                flex_direction: FlexDirection::Row,
447                align_items: AlignItems::Center,
448                justify_content: JustifyContent::Start,
449                column_gap: px(8),
450            }
451            Children [
452                @label("Dialog:")
453                --
454                @FeathersToggleSwitch
455                DemoDialogToggle
456                on(toggle_demo_dialog)
457            ]
458            --
459            @FeathersCheckbox {
460                @caption: bsn! { @caption("Checkbox") }
461            }
462            Checked
463            AccessibleLabel("Checkbox Example")
464            on(|change: On<ValueChange<bool>>, query: Query<Entity, With<DemoDisabledButton>>, mut commands: Commands| {
465                info!("Checkbox clicked!");
466                let mut button = commands.entity(query.single().unwrap());
467                if change.value {
468                    button.insert(InteractionDisabled);
469                } else {
470                    button.remove::<InteractionDisabled>();
471                }
472                let mut checkbox = commands.entity(change.source);
473                if change.value {
474                    checkbox.insert(Checked);
475                } else {
476                    checkbox.remove::<Checked>();
477                }
478            })
479            --
480            @FeathersCheckbox {
481                @caption: bsn! { @caption("Fast Click Checkbox") }
482            }
483            ActivateOnPress
484            AccessibleLabel("Fast Click Checkbox Example")
485            on(|change: On<ValueChange<bool>>, mut commands: Commands| {
486                info!("Checkbox clicked!");
487                let mut checkbox = commands.entity(change.source);
488                if change.value {
489                    checkbox.insert(Checked);
490                } else {
491                    checkbox.remove::<Checked>();
492                }
493            })
494            --
495            @FeathersCheckbox {
496                @caption: bsn! { @caption("Disabled") },
497            }
498            InteractionDisabled
499            AccessibleLabel("Disabled Checkbox Example")
500            on(|_change: On<ValueChange<bool>>| {
501                warn!("Disabled checkbox clicked!");
502            })
503            --
504            @FeathersCheckbox {
505                @caption: bsn! { @caption("Checked+Disabled") }
506            }
507            InteractionDisabled
508            Checked
509            AccessibleLabel("Disabled and Checked Checkbox Example")
510            on(|_change: On<ValueChange<bool>>| {
511                warn!("Disabled checkbox clicked!");
512            })
513            --
514            Node {
515                display: Display::Flex,
516                flex_direction: FlexDirection::Row,
517                align_items: AlignItems::Center,
518                justify_content: JustifyContent::Start,
519                column_gap: px(8),
520            }
521            Children [
522                Node {
523                    display: Display::Flex,
524                    flex_direction: FlexDirection::Column,
525                    row_gap: px(4),
526                }
527                RadioGroup
528                on(radio_self_update)
529                Children [
530                    @FeathersRadio {
531                        @caption: bsn! { @caption("One") }
532                    }
533                    Checked
534                    --
535                    @FeathersRadio {
536                        @caption: bsn! { @caption("Two") }
537                    }
538                    --
539                    @FeathersRadio {
540                        @caption: bsn! { @caption("Fast Click") }
541                    }
542                    ActivateOnPress
543                    --
544                    @FeathersRadio {
545                        @caption: bsn! { @caption("Disabled") }
546                    }
547                    InteractionDisabled
548                ]
549            ]
550            --
551            Node {
552                display: Display::Flex,
553                flex_direction: FlexDirection::Row,
554                align_items: AlignItems::Center,
555                justify_content: JustifyContent::Start,
556                column_gap: px(8),
557            }
558            Children [
559                @FeathersToggleSwitch on(checkbox_self_update)
560                --
561                @FeathersToggleSwitch ActivateOnPress on(checkbox_self_update)
562                --
563                @FeathersToggleSwitch InteractionDisabled on(checkbox_self_update)
564                --
565                @FeathersToggleSwitch InteractionDisabled Checked on(checkbox_self_update)
566                --
567                @FeathersDisclosureToggle on(checkbox_self_update)
568            ]
569            --
570            @FeathersSlider {
571                @max: 100.0,
572            }
573            SliderValue(20.0)
574            SliderStep(10.)
575            SliderPrecision(2)
576            on(slider_self_update)
577            --
578            Node {
579                display: Display::Flex,
580                flex_direction: FlexDirection::Row,
581                align_items: AlignItems::Center,
582                justify_content: JustifyContent::SpaceBetween,
583                column_gap: px(4),
584            }
585            Children [
586                @label("Srgba")
587                --
588                @flex_spacer()
589                --
590                @FeathersTextInputContainer
591                Node {
592                    flex_grow: 0.
593                    padding: { px(4).left() },
594                }
595                Children [
596                    @FeathersTextInput {
597                        @visible_width: 10f32,
598                        @max_characters: 9usize,
599                    }
600                    InheritableFont {
601                        font: fonts::MONO
602                    }
603                    HexColorInput
604                    on(handle_hex_color_change)
605                ]
606                --
607                @FeathersColorSwatch {
608                    @opaque_color_percentage: 30.0,
609                }
610                SwatchType::Rgb
611            ]
612            --
613            @FeathersColorPlane::RedBlue
614            on(|change: On<ValueChange<Vec2>>, mut color: ResMut<DemoWidgetStates>| {
615                color.rgb_color.red = change.value.x;
616                color.rgb_color.blue = change.value.y;
617            })
618            --
619            @FeathersColorSlider {
620                @value: 0.5,
621                @channel: ColorChannel::Red
622            }
623            AccessibleLabel("Red Channel")
624            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
625                color.rgb_color.red = change.value;
626            })
627            --
628            @FeathersColorSlider {
629                @value: 0.5,
630                @channel: ColorChannel::Green
631            }
632            AccessibleLabel("Green Channel")
633            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
634                color.rgb_color.green = change.value;
635            })
636            --
637            @FeathersColorSlider {
638                @value: 0.5,
639                @channel: ColorChannel::Blue
640            }
641            AccessibleLabel("Blue Channel")
642            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
643                color.rgb_color.blue = change.value;
644            })
645            --
646            @FeathersColorSlider {
647                @value: 0.5,
648                @channel: ColorChannel::Alpha
649            }
650            AccessibleLabel("Alpha Channel")
651            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
652                color.rgb_color.alpha = change.value;
653            })
654            --
655            Node {
656                display: Display::Flex,
657                align_items: AlignItems::Center,
658                flex_direction: FlexDirection::Row,
659                justify_content: JustifyContent::SpaceBetween,
660            }
661            Children [
662                @label("Hsl")
663                --
664                @FeathersColorSwatch SwatchType::Hsl
665            ]
666            --
667            @FeathersColorSlider {
668                @value: 0.5,
669                @channel: ColorChannel::HslHue
670            }
671            AccessibleLabel("Hue Channel")
672            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
673                color.hsl_color.hue = change.value;
674            })
675            --
676            @FeathersColorSlider {
677                @value: 0.5,
678                @channel: ColorChannel::HslSaturation
679            }
680            AccessibleLabel("Saturation Channel")
681            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
682                color.hsl_color.saturation = change.value;
683            })
684            --
685            @FeathersColorSlider {
686                @value: 0.5,
687                @channel: ColorChannel::HslLightness
688            }
689            AccessibleLabel("Lightness Channel")
690            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
691                color.hsl_color.lightness = change.value;
692            })
693            --
694            Node {
695                display: Display::Flex,
696                align_items: AlignItems::Center,
697                flex_direction: FlexDirection::Row,
698                justify_content: JustifyContent::SpaceBetween,
699            }
700            Children [
701                @label("Okhsl")
702                --
703                @FeathersColorSwatch SwatchType::Okhsl
704            ]
705            --
706            @FeathersColorPlane::OkhslHueLightness
707            on(|change: On<ValueChange<Vec2>>, mut color: ResMut<DemoWidgetStates>| {
708                color.okhsl_color.hue = change.value.x * 360.0;
709                color.okhsl_color.lightness = 1.0 - change.value.y;
710            })
711            --
712            @FeathersColorSlider {
713                @value: 0.5,
714                @channel: ColorChannel::OkhslHue
715            }
716            AccessibleLabel("Okhsl Hue Channel")
717            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
718                color.okhsl_color.hue = change.value;
719            })
720            --
721            @FeathersColorSlider {
722                @value: 0.5,
723                @channel: ColorChannel::OkhslSaturation
724            }
725            AccessibleLabel("Okhsl Saturation Channel")
726            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
727                color.okhsl_color.saturation = change.value;
728            })
729            --
730            @FeathersColorSlider {
731                @value: 0.5,
732                @channel: ColorChannel::OkhslLightness
733            }
734            AccessibleLabel("Okhsl Lightness Channel")
735            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
736                color.okhsl_color.lightness = change.value;
737            })
738        ]
739    }
740}
741
742fn demo_column_2() -> impl Scene {
743    bsn! {
744        Node {
745            display: Display::Flex,
746            flex_direction: FlexDirection::Column,
747            align_items: AlignItems::Stretch,
748            justify_content: JustifyContent::Start,
749            padding: px(8),
750            row_gap: px(8),
751            width: percent(30),
752            min_width: px(200),
753        }
754        Children [
755            @pane() Children [
756                @pane_header() Children [
757                    @FeathersToolButton {
758                        @variant: ButtonVariant::Primary,
759                        @caption: bsn! { @caption("\u{0398}") }
760                    }
761                    --
762                    @pane_header_divider()
763                    --
764                    @FeathersToolButton {
765                        @variant: ButtonVariant::Plain,
766                        @caption: bsn! { @caption("\u{00BC}") }
767                    }
768                    --
769                    @FeathersToolButton {
770                        @variant: ButtonVariant::Plain,
771                        @caption: bsn! { @caption("\u{00BD}") }
772                    }
773                    --
774                    @FeathersToolButton {
775                        @variant: ButtonVariant::Plain,
776                        @caption: bsn! { @caption("\u{00BE}") }
777                    }
778                    --
779                    @pane_header_divider()
780                    --
781                    @FeathersToolButton {
782                        @variant: ButtonVariant::Plain,
783                        @caption: bsn! { @icon(icons::CHEVRON_DOWN) }
784                    }
785                    --
786                    @flex_spacer()
787                    --
788                    @FeathersToolButton {
789                        @variant: ButtonVariant::Plain,
790                        @caption: bsn! { @icon(icons::X) }
791                    }
792                ]
793                --
794                @pane_body() Children [
795                    @label_dim("A standard editor pane")
796                    --
797                    @subpane() Children [
798                        @subpane_header() Children [
799                            @caption("Left")
800                            --
801                            @caption("Center")
802                            --
803                            @caption("Right")
804                        ]
805                        --
806                        @subpane_body() Children [
807                            @label_dim("A standard sub-pane")
808                            --
809                            @group() Children [
810                                @group_header() Children [
811                                    @caption("Group")
812                                ]
813                                --
814                                @group_body()
815                                Children [
816                                    @label("A standard group")
817                                    --
818                                    @label_small("Scalar property")
819                                    --
820                                    @FeathersNumberInput
821                                    DemoScalarField
822                                    NumberInputPrecision(2)
823                                    HardLimit::f32(0.0..=100.0)
824                                    Node {
825                                        flex_grow: 1.0,
826                                        max_width: px(100),
827                                    }
828                                    on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
829                                        states.scalar_prop = value_change.value;
830                                    })
831                                    --
832                                    @label_small("Scalar property (copy)")
833                                    --
834                                    @FeathersNumberInput
835                                    DemoScalarField
836                                    NumberInputPrecision(4)
837                                    Node {
838                                        flex_grow: 1.0,
839                                        max_width: px(100),
840                                    }
841                                    on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
842                                        states.scalar_prop = value_change.value;
843                                    })
844                                    --
845                                    @label_small("Vec3 property")
846                                    --
847                                    Node {
848                                        display: Display::Flex,
849                                        flex_direction: FlexDirection::Row,
850                                        column_gap: px(6),
851                                        align_items: AlignItems::Center,
852                                        justify_content: JustifyContent::SpaceBetween,
853                                    }
854                                    Children [
855                                        @FeathersNumberInput {
856                                            @sigil_color: tokens::TEXT_INPUT_X_AXIS,
857                                            @label_text: "X",
858                                        }
859                                        NumberInputPrecision(2)
860                                        DemoVec3Field::X
861                                        Node {
862                                            flex_grow: 1.0,
863                                        }
864                                        BorderColor::all(palette::X_AXIS)
865                                        on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
866                                            states.vec3_prop.x = value_change.value;
867                                        })
868                                        --
869                                        @FeathersNumberInput {
870                                            @sigil_color: tokens::TEXT_INPUT_Y_AXIS,
871                                            @label_text: "Y",
872                                        }
873                                        NumberInputPrecision(2)
874                                        DemoVec3Field::Y
875                                        Node {
876                                            flex_grow: 1.0,
877                                        }
878                                        on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
879                                            states.vec3_prop.y = value_change.value;
880                                        })
881                                        --
882                                        @FeathersNumberInput {
883                                            @sigil_color: tokens::TEXT_INPUT_Z_AXIS,
884                                            @label_text: "Z",
885                                        }
886                                        NumberInputPrecision(2)
887                                        DemoVec3Field::Z
888                                        Node {
889                                            flex_grow: 1.0,
890                                        }
891                                        on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
892                                            states.vec3_prop.z = value_change.value;
893                                        })
894                                    ]
895                                    --
896                                    @label_small("Color property")
897                                    --
898                                    Node {
899                                        display: Display::Flex,
900                                        flex_direction: FlexDirection::Row,
901                                        column_gap: px(6),
902                                        align_items: AlignItems::Center,
903                                        justify_content: JustifyContent::SpaceBetween,
904                                    }
905                                    Children [
906                                        @FeathersNumberInput {
907                                            @sigil_color: tokens::TEXT_INPUT_X_AXIS,
908                                            @label_text: "R",
909                                        }
910                                        InteractionDisabled
911                                        NumberInputPrecision(2)
912                                        HardLimit(NumberInputRange::F32(0.0..=1.0))
913                                        Node {
914                                            flex_grow: 1.0,
915                                        }
916                                        BorderColor::all(palette::X_AXIS)
917                                        --
918                                        @FeathersNumberInput {
919                                            @sigil_color: tokens::TEXT_INPUT_Y_AXIS,
920                                            @label_text: "G",
921                                        }
922                                        InteractionDisabled
923                                        NumberInputPrecision(2)
924                                        HardLimit(NumberInputRange::F32(0.0..=1.0))
925                                        Node {
926                                            flex_grow: 1.0,
927                                        }
928                                        --
929                                        @FeathersNumberInput {
930                                            @sigil_color: tokens::TEXT_INPUT_Z_AXIS,
931                                            @label_text: "B",
932                                        }
933                                        InteractionDisabled
934                                        NumberInputPrecision(2)
935                                        HardLimit(NumberInputRange::F32(0.0..=1.0))
936                                        Node {
937                                            flex_grow: 1.0,
938                                        }
939                                        --
940                                        @FeathersNumberInput {
941                                            @sigil_color: tokens::TEXT_INPUT_W_AXIS,
942                                            @label_text: "A",
943                                        }
944                                        InteractionDisabled
945                                        NumberInputPrecision(2)
946                                        HardLimit(NumberInputRange::F32(0.0..=1.0))
947                                        Node {
948                                            flex_grow: 1.0,
949                                        }
950                                    ]
951                                ]
952                            ]
953                        ]
954                    ]
955                ]
956            ]
957            --
958            @subpane() Children [
959                @subpane_header() Children [
960                    @caption("List")
961                ]
962                --
963                @subpane_body() Children [
964                    @FeathersListView {
965                        @rows: bsn_list! {
966                            @FeathersListRow Children [ @caption("First World") ]
967                            --
968                            @FeathersListRow Selected Children [ @caption("Second Nature") ]
969                            --
970                            @FeathersListRow Children [ @caption("Third Degree") ]
971                            --
972                            @FeathersListRow InteractionDisabled Children [ @caption("Fourth Wall") ]
973                            --
974                            @FeathersListRow Children [ @caption("Fifth Column") ]
975                            --
976                            @FeathersListRow Children [ @caption("Sixth Sense") ]
977                            --
978                            @FeathersListRow Children [ @caption("Seventh Heaven") ]
979                            --
980                            @FeathersListRow Children [ @caption("Eighth Wonder") ]
981                            --
982                            @FeathersListRow Children [ @caption("Ninth Inning") ]
983                            --
984                            @FeathersListRow Children [ @caption("Tenth Amendment") ]
985                            --
986                            @FeathersListRow Children [ @caption("Eleventh Hour") ]
987                            --
988                            @FeathersListRow Children [ @caption("Twelfth Night") ]
989                        }
990                    }
991                    Node {
992                        max_height: px(130)
993                    }
994                    on(listbox_update_selection)
995                ]
996            ]
997        ]
998    }
999}
1000
1001fn demo_column_3() -> impl Scene {
1002    bsn! {
1003        Node {
1004            display: Display::Flex,
1005            flex_direction: FlexDirection::Column,
1006            align_items: AlignItems::Stretch,
1007            justify_content: JustifyContent::Start,
1008            padding: px(8),
1009            row_gap: px(8),
1010            width: percent(15),
1011            min_width: px(100),
1012        }
1013        Children [
1014            Node {
1015                display: Display::Flex,
1016                flex_direction: FlexDirection::Row,
1017                align_items: AlignItems::Center,
1018                justify_content: JustifyContent::SpaceBetween,
1019            }
1020            Children [
1021                @label("Armor Tint")
1022                --
1023                @FeathersColorInput
1024                ColorInputValue(palettes::tailwind::AMBER_600)
1025                on(color_input_self_update)
1026            ]
1027            --
1028            Node {
1029                display: Display::Flex,
1030                flex_direction: FlexDirection::Row,
1031                align_items: AlignItems::Center,
1032                justify_content: JustifyContent::SpaceBetween,
1033            }
1034            Children [
1035                @label("Skin Color")
1036                --
1037                @FeathersColorInput
1038                ColorInputValue(palettes::tailwind::BLUE_800)
1039                on(color_input_self_update)
1040            ]
1041        ]
1042    }
1043}
1044
1045fn update_colors(
1046    states: Res<DemoWidgetStates>,
1047    mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>,
1048    swatches: Query<(Entity, &SwatchType), With<FeathersColorSwatch>>,
1049    mut color_planes: Query<(&mut ColorPlaneValue, &FeathersColorPlane)>,
1050    q_text_input: Single<(Entity, &mut EditableText), With<HexColorInput>>,
1051    q_scalar_input: Query<Entity, With<DemoScalarField>>,
1052    q_vec3_input: Query<(Entity, &DemoVec3Field)>,
1053    mut commands: Commands,
1054    focus: Res<InputFocus>,
1055) {
1056    if states.is_changed() {
1057        for (slider_ent, slider, mut base) in sliders.iter_mut() {
1058            match slider.channel {
1059                ColorChannel::Red => {
1060                    base.0 = states.rgb_color.into();
1061                    commands
1062                        .entity(slider_ent)
1063                        .insert(SliderValue(states.rgb_color.red));
1064                }
1065                ColorChannel::Green => {
1066                    base.0 = states.rgb_color.into();
1067                    commands
1068                        .entity(slider_ent)
1069                        .insert(SliderValue(states.rgb_color.green));
1070                }
1071                ColorChannel::Blue => {
1072                    base.0 = states.rgb_color.into();
1073                    commands
1074                        .entity(slider_ent)
1075                        .insert(SliderValue(states.rgb_color.blue));
1076                }
1077                ColorChannel::HslHue => {
1078                    base.0 = states.hsl_color.into();
1079                    commands
1080                        .entity(slider_ent)
1081                        .insert(SliderValue(states.hsl_color.hue));
1082                }
1083                ColorChannel::HslSaturation => {
1084                    base.0 = states.hsl_color.into();
1085                    commands
1086                        .entity(slider_ent)
1087                        .insert(SliderValue(states.hsl_color.saturation));
1088                }
1089                ColorChannel::HslLightness => {
1090                    base.0 = states.hsl_color.into();
1091                    commands
1092                        .entity(slider_ent)
1093                        .insert(SliderValue(states.hsl_color.lightness));
1094                }
1095                ColorChannel::OkhslHue => {
1096                    base.0 = states.okhsl_color.into();
1097                    commands
1098                        .entity(slider_ent)
1099                        .insert(SliderValue(states.okhsl_color.hue));
1100                }
1101                ColorChannel::OkhslSaturation => {
1102                    base.0 = states.okhsl_color.into();
1103                    commands
1104                        .entity(slider_ent)
1105                        .insert(SliderValue(states.okhsl_color.saturation));
1106                }
1107                ColorChannel::OkhslLightness => {
1108                    base.0 = states.okhsl_color.into();
1109                    commands
1110                        .entity(slider_ent)
1111                        .insert(SliderValue(states.okhsl_color.lightness));
1112                }
1113                ColorChannel::Alpha => {
1114                    base.0 = states.rgb_color.into();
1115                    commands
1116                        .entity(slider_ent)
1117                        .insert(SliderValue(states.rgb_color.alpha));
1118                }
1119            }
1120        }
1121
1122        for (swatch_ent, swatch_type) in swatches.iter() {
1123            commands
1124                .entity(swatch_ent)
1125                .insert(ColorSwatchValue(match swatch_type {
1126                    SwatchType::Rgb => states.rgb_color.into(),
1127                    SwatchType::Hsl => states.hsl_color.into(),
1128                    SwatchType::Okhsl => states.okhsl_color.into(),
1129                }));
1130        }
1131
1132        for (mut plane_value, plane_type) in color_planes.iter_mut() {
1133            match plane_type {
1134                FeathersColorPlane::OkhslHueLightness => {
1135                    plane_value.0.x = states.okhsl_color.hue / 360.0;
1136                    plane_value.0.y = states.okhsl_color.lightness;
1137                    plane_value.0.z = states.okhsl_color.saturation;
1138                }
1139                FeathersColorPlane::OkhslHueSaturation => {
1140                    plane_value.0.x = states.okhsl_color.hue / 360.0;
1141                    plane_value.0.y = states.okhsl_color.saturation;
1142                    plane_value.0.z = states.okhsl_color.lightness;
1143                }
1144                FeathersColorPlane::HueLightness => {
1145                    plane_value.0.x = states.hsl_color.hue / 360.0;
1146                    plane_value.0.y = states.hsl_color.lightness;
1147                    plane_value.0.z = states.hsl_color.saturation;
1148                }
1149                FeathersColorPlane::HueSaturation => {
1150                    plane_value.0.x = states.hsl_color.hue / 360.0;
1151                    plane_value.0.y = states.hsl_color.saturation;
1152                    plane_value.0.z = states.hsl_color.lightness;
1153                }
1154                FeathersColorPlane::RedBlue => {
1155                    plane_value.0.x = states.rgb_color.red;
1156                    plane_value.0.y = states.rgb_color.blue;
1157                    plane_value.0.z = states.rgb_color.green;
1158                }
1159                FeathersColorPlane::GreenBlue => {
1160                    plane_value.0.x = states.rgb_color.green;
1161                    plane_value.0.y = states.rgb_color.blue;
1162                    plane_value.0.z = states.rgb_color.red;
1163                }
1164                FeathersColorPlane::RedGreen => {
1165                    plane_value.0.x = states.rgb_color.red;
1166                    plane_value.0.y = states.rgb_color.green;
1167                    plane_value.0.z = states.rgb_color.blue;
1168                }
1169            }
1170        }
1171
1172        // Only update the hex input field when it's not focused, otherwise it interferes
1173        // with typing.
1174        let (input_ent, mut editable_text) = q_text_input.into_inner();
1175        if Some(input_ent) != focus.get() {
1176            editable_text.queue_edit(TextEdit::SelectAll);
1177            editable_text.queue_edit(TextEdit::Insert(states.rgb_color.to_hex().into()));
1178        }
1179
1180        for scalar_input_ent in q_scalar_input.iter() {
1181            commands
1182                .entity(scalar_input_ent)
1183                .insert(NumberInputValue::F32(states.scalar_prop));
1184        }
1185
1186        for (vec3_input_ent, axis) in q_vec3_input.iter() {
1187            let new_value = match axis {
1188                DemoVec3Field::X => states.vec3_prop.x,
1189                DemoVec3Field::Y => states.vec3_prop.y,
1190                DemoVec3Field::Z => states.vec3_prop.z,
1191            };
1192
1193            commands
1194                .entity(vec3_input_ent)
1195                .insert(NumberInputValue::F32(new_value));
1196        }
1197    }
1198}
1199
1200fn handle_hex_color_change(
1201    _change: On<TextEditChange>,
1202    q_text_input: Single<&EditableText, With<HexColorInput>>,
1203    mut colors: ResMut<DemoWidgetStates>,
1204) {
1205    let editable_text = *q_text_input;
1206    if let Ok(color) = Srgba::hex(editable_text.value().to_string())
1207        && color != colors.rgb_color
1208    {
1209        colors.rgb_color = color;
1210    }
1211}
1212
1213fn spawn_quit_dialog(activate: On<Activate>, mut commands: Commands) {
1214    commands
1215        .entity(activate.event_target())
1216        .queue_spawn_related_scenes::<Children>(bsn_list! {
1217            @FeathersDialog {
1218                @width: px(320),
1219                @contents: bsn_list! {
1220                    @FeathersDialogHeader Children [
1221                        @caption("Quit Feathers Gallery")
1222                        --
1223                        @FeathersDialogClose
1224                    ]
1225                    --
1226                    @FeathersDialogBody Children [
1227                        @caption("Are you really sure you want to quit? I mean, really, really sure?")
1228                    ]
1229                    --
1230                    @FeathersDialogFooter Children [
1231                        @FeathersButton {
1232                            @caption: bsn! { @caption("Cancel") },
1233                        }
1234                        AccessibleLabel("Cancel")
1235                        on(|activate: On<Activate>, mut commands: Commands| {
1236                            commands.trigger(RequestClose { source: activate.event_target() });
1237                        })
1238                        --
1239                        @FeathersButton {
1240                            @caption: bsn! { @caption("Exit Application") },
1241                            @variant: ButtonVariant::Primary,
1242                        }
1243                        AccessibleLabel("Exit Application")
1244                        on(|_activate: On<Activate>, mut exit: MessageWriter<AppExit>| {
1245                            exit.write(AppExit::Success);
1246                        })
1247                    ]
1248                }
1249            }
1250            on(|close: On<RequestClose>, mut commands: Commands| {
1251                commands.entity(close.event_target()).despawn();
1252            })
1253        });
1254}
1255
1256fn toggle_demo_dialog(
1257    change: On<ValueChange<bool>>,
1258    mut commands: Commands,
1259    dialogs: Query<Entity, With<FeathersFloatingDialog>>,
1260) {
1261    let toggle = change.source;
1262    if change.value {
1263        commands.entity(toggle).insert(Checked);
1264        // Spawn at the root rather than as a child of the toggle, so clicks on the
1265        // dialog don't bubble up to the toggle switch.
1266        commands.spawn_scene(bsn! {
1267            @FeathersFloatingDialog {
1268                @title: {"Hello".to_string()},
1269                @width: px(280),
1270                @contents: bsn_list! {
1271                    @caption("Close this dialog to unset the toggle.")
1272                }
1273            }
1274            // The dialog despawns itself on close; this just clears the toggle.
1275            on(|_close: On<RequestClose>, mut commands: Commands, toggles: Query<Entity, With<DemoDialogToggle>>| {
1276                for toggle in toggles.iter() {
1277                    commands.entity(toggle).remove::<Checked>();
1278                }
1279            })
1280        });
1281    } else {
1282        commands.entity(toggle).remove::<Checked>();
1283        for dialog in dialogs.iter() {
1284            commands.entity(dialog).despawn();
1285        }
1286    }
1287}