UI (User Interface) / Feathers Widgets

Back to examples View in GitHub

This example shows off the various Bevy Feathers widgets.

use bevy::{
    color::palettes,
    feathers::{
        constants::{fonts, icons},
        containers::*,
        controls::*,
        dark_theme::create_dark_theme,
        display::{caption, icon, label, label_dim, label_small},
        font_styles::InheritableFont,
        palette,
        rounded_corners::RoundedCorners,
        theme::{ThemeBackgroundColor, UiTheme},
        tokens, FeathersPlugins,
    },
    input_focus::{tab_navigation::TabGroup, AutoFocus, InputFocus},
    picking::cursor::{EntityCursor, OverrideCursor},
    prelude::*,
    text::{EditableText, TextEdit, TextEditChange},
    ui::{Checked, InteractionDisabled, Selected},
    ui_widgets::{
        checkbox_self_update, listbox_update_selection,
        popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide},
        radio_self_update, slider_self_update, Activate, ActivateOnPress, RadioGroup, RequestClose,
        SliderPrecision, SliderStep, SliderValue, ValueChange,
    },
    window::SystemCursorIcon,
};
use std::sync::Arc;

/// A struct to hold the state of various widgets shown in the demo.
#[derive(Resource)]
struct DemoWidgetStates {
    rgb_color: Srgba,
    hsl_color: Hsla,
    okhsl_color: Okhsla,
    scalar_prop: f32,
    vec3_prop: Vec3,
}

#[derive(Component, Clone, Copy, PartialEq, Default)]
enum SwatchType {
    #[default]
    Rgb,
    Hsl,
    Okhsl,
}

#[derive(Component, Clone, Copy, Default)]
struct HexColorInput;

#[derive(Component, Clone, Copy, Default)]
struct DemoDisabledButton;

#[derive(Component, Clone, Copy, Default)]
struct DemoDialogToggle;

#[derive(Component, Clone, Copy, Default)]
struct DemoScalarField;

#[derive(Component, Clone, Copy, Default)]
enum DemoVec3Field {
    #[default]
    X,
    Y,
    Z,
}

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, FeathersPlugins))
        .insert_resource(UiTheme(create_dark_theme()))
        .insert_resource(DemoWidgetStates {
            rgb_color: palettes::tailwind::EMERALD_800.with_alpha(0.7),
            hsl_color: palettes::tailwind::AMBER_800.into(),
            okhsl_color: palettes::tailwind::AMBER_800.into(),
            scalar_prop: 7.0,
            vec3_prop: Vec3::new(10.1, 7.124, 100.0),
        })
        .add_systems(Startup, scene.spawn())
        .add_systems(Update, update_colors)
        .run();
}

fn scene() -> impl SceneList {
    bsn_list! {
        Camera2d
        --
        @demo_root()
    }
}

fn demo_root() -> impl Scene {
    bsn! {
        Node {
            width: percent(100),
            height: percent(100),
            align_items: AlignItems::Start,
            justify_content: JustifyContent::Start,
            display: Display::Flex,
            flex_direction: FlexDirection::Row,
            column_gap: px(8),
        }
        TabGroup
        ThemeBackgroundColor(tokens::WINDOW_BG)
        Children [
            @demo_column_1()
            --
            @demo_column_2()
            --
            @demo_column_3()
        ]
    }
}

#[derive(Component, Debug, Clone, Default, PartialEq)]
enum Months {
    #[default]
    Jan,
    Feb,
    Mar,
    Apr,
    May,
    Jun,
    Jul,
    Aug,
    Sep,
    Oct,
    Nov,
    Dec,
}

impl Months {
    const ALL: [Months; 12] = [
        Months::Jan,
        Months::Feb,
        Months::Mar,
        Months::Apr,
        Months::May,
        Months::Jun,
        Months::Jul,
        Months::Aug,
        Months::Sep,
        Months::Oct,
        Months::Nov,
        Months::Dec,
    ];

    fn to_str(&self) -> &'static str {
        match self {
            Months::Jan => "January",
            Months::Feb => "February",
            Months::Mar => "March",
            Months::Apr => "April",
            Months::May => "May",
            Months::Jun => "June",
            Months::Jul => "July",
            Months::Aug => "August",
            Months::Sep => "September",
            Months::Oct => "October",
            Months::Nov => "November",
            Months::Dec => "December",
        }
    }
}

fn demo_column_1() -> impl Scene {
    // Lazily-constructed menu popup
    let popup: Arc<dyn Fn() -> Box<dyn Scene> + Sync + Send> = Arc::new(|| {
        Box::new(bsn! {
            @FeathersMenuPopup
            // Override popover placement to right-align the popup
            Popover {
                positions: vec![
                    PopoverPlacement {
                        side: PopoverSide::Bottom,
                        align: PopoverAlign::End,
                        gap: 2.0,
                    },
                    PopoverPlacement {
                        side: PopoverSide::Top,
                        align: PopoverAlign::End,
                        gap: 2.0,
                    },
                ],
                window_margin: 10.0,
            }
            Children [
                @FeathersMenuItem {
                    @caption: bsn! { @caption("MenuItem 4") }
                }
                on(|_: On<Activate>| {
                    info!("Menu item 4 clicked!");
                })
                --
                @FeathersMenuItem {
                    @caption: bsn! { @caption("MenuItem 5") }
                }
                on(|_: On<Activate>| {
                    info!("Menu item 5 clicked!");
                })
                --
                @FeathersMenuItem {
                    @caption: bsn! { @caption("MenuItem 6") }
                }
                on(|_: On<Activate>| {
                    info!("Menu item 6 clicked!");
                })
            ]
        })
    });

    bsn! {
        Node {
            display: Display::Flex,
            flex_direction: FlexDirection::Column,
            align_items: AlignItems::Stretch,
            justify_content: JustifyContent::Start,
            padding: px(8),
            row_gap: px(8),
            width: percent(30),
            min_width: px(200),
        }
        Children [
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Start,
                column_gap: px(8),
            }
            Children [
                @FeathersButton {
                    @caption: bsn! { @caption("Normal") }
                }
                Node {
                    flex_grow: 1.0,
                }
                AccessibleLabel("Normal")
                on(|_activate: On<Activate>| {
                    info!("Normal button clicked!");
                })
                AutoFocus
                --
                @FeathersButton {
                    @caption: bsn! { @caption("Disabled") },
                }
                Node {
                    flex_grow: 1.0,
                }
                AccessibleLabel("Disabled")
                InteractionDisabled
                DemoDisabledButton
                on(|_activate: On<Activate>| {
                    info!("Disabled button clicked!");
                })
                --
                @FeathersButton {
                    @caption: bsn! { @caption("Primary") },
                    @variant: ButtonVariant::Primary,
                }
                AccessibleLabel("Primary")
                Node {
                    flex_grow: 1.0,
                }
                on(|_activate: On<Activate>| {
                    info!("Primary button clicked!");
                })
                --
                @FeathersMenu
                Children [
                    @FeathersMenuButton {
                        @caption: bsn! { @caption("Menu") }
                    }
                    AccessibleLabel("Menu Example")
                    Node {
                        flex_grow: 1.0,
                    }
                    --
                    @FeathersMenuPopup
                    Children [
                        @FeathersMenuItem {
                            @caption: bsn! { @caption("MenuItem 1") }
                        }
                        on(|_: On<Activate>| {
                            info!("Menu item 1 clicked!");
                        })
                        --
                        @FeathersMenuItem {
                            @caption: bsn! { @caption("MenuItem 2") }
                        }
                        on(|_: On<Activate>| {
                            info!("Menu item 2 clicked!");
                        })
                        --
                        @FeathersMenuDivider
                        --
                        @FeathersMenuItem {
                            @caption: bsn! { @caption("MenuItem 3") }
                        }
                        on(|_: On<Activate>| {
                            info!("Menu item 3 clicked!");
                        })
                    ]
                ]
                --
                @FeathersLazyMenu { popup }
                Children [
                    @FeathersMenuToolButton {
                        @caption: bsn! { @caption("\u{0398}") }
                    }
                    AccessibleLabel("Menu Example")
                    Node {
                        flex_grow: 1.0,
                    }
                ]
            ]
            --
            @FeathersSelect {
                @options: list_rows_from_strings([
                    "One",
                    "Two",
                    "Three",
                ], Some(0)),
            }
            Node {
                flex_grow: 1.0,
            }
            on(|change: On<ValueChange<Entity>>, q_options: Query<&OptionIndex>| {
                let Ok(option) = q_options.get(change.value) else {
                    info!("Select changed, not sure");
                    return;
                };
                info!("Select changed to index {}", option.0);
            })
            --
            @FeathersSelect {
                @options: {
                    Box::new(
                        Months::ALL
                            .into_iter()
                            .map(|m| -> Box<dyn SceneList> {
                                let label = m.to_str();
                                if m == Months::default() {
                                    bsn! { @FeathersListRow Selected m Children [ @caption(label) ] }.into()
                                } else {
                                    bsn! { @FeathersListRow m Children [ @caption(label) ] }.into()
                                }
                            })
                            .collect::<Vec<_>>(),
                    ) as Box<dyn SceneList>
                },
                @max_visible: 6,
            }
            Node {
                flex_grow: 1.0,
            }
            on(|change: On<ValueChange<Entity>>, q_months: Query<&Months>| {
                let Ok(month) = q_months.get(change.value) else {
                    return;
                };
                info!("Select changed to {:?}", month);
            })
            --
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Start,
                column_gap: px(1),
            }
            Children [
                @FeathersButton {
                    @caption: bsn! { @caption("Left") },
                    @corners: RoundedCorners::Left,
                }
                Node {
                    flex_grow: 1.0,
                }
                AccessibleLabel("Left")
                on(|_activate: On<Activate>| {
                    info!("Left button clicked!");
                })
                --
                @FeathersButton {
                    @caption: bsn! { @caption("Center") },
                    @corners: RoundedCorners::None,
                }
                Node {
                    flex_grow: 1.0,
                }
                AccessibleLabel("Center")
                on(|_activate: On<Activate>| {
                    info!("Center button clicked!");
                })
                --
                @FeathersButton {
                    @caption: bsn! { @caption("Right") },
                    @variant: ButtonVariant::Primary,
                    @corners: RoundedCorners::Right,
                }
                Node {
                    flex_grow: 1.0,
                }
                AccessibleLabel("Right")
                on(|_activate: On<Activate>| {
                    info!("Right button clicked!");
                })
            ]
            --
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Start,
                column_gap: px(8),
            }
            Children [
                @FeathersButton {
                    @caption: bsn! { @caption("Toggle override") },
                }
                Node {
                    flex_grow: 1.0,
                }
                on(|_activate: On<Activate>, mut ovr: ResMut<OverrideCursor>| {
                    ovr.0 = if ovr.0.is_some() {
                        None
                    } else {
                        Some(EntityCursor::System(SystemCursorIcon::Wait))
                    };
                    info!("Override cursor button clicked!");
                })
                --
                @FeathersButton {
                    @caption: bsn! { @caption("Quit\u{2026}") },
                }
                Node {
                    flex_grow: 1.0,
                }
                on(spawn_quit_dialog)
            ]
            --
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Start,
                column_gap: px(8),
            }
            Children [
                @label("Dialog:")
                --
                @FeathersToggleSwitch
                DemoDialogToggle
                on(toggle_demo_dialog)
            ]
            --
            @FeathersCheckbox {
                @caption: bsn! { @caption("Checkbox") }
            }
            Checked
            AccessibleLabel("Checkbox Example")
            on(|change: On<ValueChange<bool>>, query: Query<Entity, With<DemoDisabledButton>>, mut commands: Commands| {
                info!("Checkbox clicked!");
                let mut button = commands.entity(query.single().unwrap());
                if change.value {
                    button.insert(InteractionDisabled);
                } else {
                    button.remove::<InteractionDisabled>();
                }
                let mut checkbox = commands.entity(change.source);
                if change.value {
                    checkbox.insert(Checked);
                } else {
                    checkbox.remove::<Checked>();
                }
            })
            --
            @FeathersCheckbox {
                @caption: bsn! { @caption("Fast Click Checkbox") }
            }
            ActivateOnPress
            AccessibleLabel("Fast Click Checkbox Example")
            on(|change: On<ValueChange<bool>>, mut commands: Commands| {
                info!("Checkbox clicked!");
                let mut checkbox = commands.entity(change.source);
                if change.value {
                    checkbox.insert(Checked);
                } else {
                    checkbox.remove::<Checked>();
                }
            })
            --
            @FeathersCheckbox {
                @caption: bsn! { @caption("Disabled") },
            }
            InteractionDisabled
            AccessibleLabel("Disabled Checkbox Example")
            on(|_change: On<ValueChange<bool>>| {
                warn!("Disabled checkbox clicked!");
            })
            --
            @FeathersCheckbox {
                @caption: bsn! { @caption("Checked+Disabled") }
            }
            InteractionDisabled
            Checked
            AccessibleLabel("Disabled and Checked Checkbox Example")
            on(|_change: On<ValueChange<bool>>| {
                warn!("Disabled checkbox clicked!");
            })
            --
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Start,
                column_gap: px(8),
            }
            Children [
                Node {
                    display: Display::Flex,
                    flex_direction: FlexDirection::Column,
                    row_gap: px(4),
                }
                RadioGroup
                on(radio_self_update)
                Children [
                    @FeathersRadio {
                        @caption: bsn! { @caption("One") }
                    }
                    Checked
                    --
                    @FeathersRadio {
                        @caption: bsn! { @caption("Two") }
                    }
                    --
                    @FeathersRadio {
                        @caption: bsn! { @caption("Fast Click") }
                    }
                    ActivateOnPress
                    --
                    @FeathersRadio {
                        @caption: bsn! { @caption("Disabled") }
                    }
                    InteractionDisabled
                ]
            ]
            --
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Start,
                column_gap: px(8),
            }
            Children [
                @FeathersToggleSwitch on(checkbox_self_update)
                --
                @FeathersToggleSwitch ActivateOnPress on(checkbox_self_update)
                --
                @FeathersToggleSwitch InteractionDisabled on(checkbox_self_update)
                --
                @FeathersToggleSwitch InteractionDisabled Checked on(checkbox_self_update)
                --
                @FeathersDisclosureToggle on(checkbox_self_update)
            ]
            --
            @FeathersSlider {
                @max: 100.0,
            }
            SliderValue(20.0)
            SliderStep(10.)
            SliderPrecision(2)
            on(slider_self_update)
            --
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::SpaceBetween,
                column_gap: px(4),
            }
            Children [
                @label("Srgba")
                --
                @flex_spacer()
                --
                @FeathersTextInputContainer
                Node {
                    flex_grow: 0.
                    padding: { px(4).left() },
                }
                Children [
                    @FeathersTextInput {
                        @visible_width: 10f32,
                        @max_characters: 9usize,
                    }
                    InheritableFont {
                        font: fonts::MONO
                    }
                    HexColorInput
                    on(handle_hex_color_change)
                ]
                --
                @FeathersColorSwatch {
                    @opaque_color_percentage: 30.0,
                }
                SwatchType::Rgb
            ]
            --
            @FeathersColorPlane::RedBlue
            on(|change: On<ValueChange<Vec2>>, mut color: ResMut<DemoWidgetStates>| {
                color.rgb_color.red = change.value.x;
                color.rgb_color.blue = change.value.y;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::Red
            }
            AccessibleLabel("Red Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.rgb_color.red = change.value;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::Green
            }
            AccessibleLabel("Green Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.rgb_color.green = change.value;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::Blue
            }
            AccessibleLabel("Blue Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.rgb_color.blue = change.value;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::Alpha
            }
            AccessibleLabel("Alpha Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.rgb_color.alpha = change.value;
            })
            --
            Node {
                display: Display::Flex,
                align_items: AlignItems::Center,
                flex_direction: FlexDirection::Row,
                justify_content: JustifyContent::SpaceBetween,
            }
            Children [
                @label("Hsl")
                --
                @FeathersColorSwatch SwatchType::Hsl
            ]
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::HslHue
            }
            AccessibleLabel("Hue Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.hsl_color.hue = change.value;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::HslSaturation
            }
            AccessibleLabel("Saturation Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.hsl_color.saturation = change.value;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::HslLightness
            }
            AccessibleLabel("Lightness Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.hsl_color.lightness = change.value;
            })
            --
            Node {
                display: Display::Flex,
                align_items: AlignItems::Center,
                flex_direction: FlexDirection::Row,
                justify_content: JustifyContent::SpaceBetween,
            }
            Children [
                @label("Okhsl")
                --
                @FeathersColorSwatch SwatchType::Okhsl
            ]
            --
            @FeathersColorPlane::OkhslHueLightness
            on(|change: On<ValueChange<Vec2>>, mut color: ResMut<DemoWidgetStates>| {
                color.okhsl_color.hue = change.value.x * 360.0;
                color.okhsl_color.lightness = 1.0 - change.value.y;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::OkhslHue
            }
            AccessibleLabel("Okhsl Hue Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.okhsl_color.hue = change.value;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::OkhslSaturation
            }
            AccessibleLabel("Okhsl Saturation Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.okhsl_color.saturation = change.value;
            })
            --
            @FeathersColorSlider {
                @value: 0.5,
                @channel: ColorChannel::OkhslLightness
            }
            AccessibleLabel("Okhsl Lightness Channel")
            on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
                color.okhsl_color.lightness = change.value;
            })
        ]
    }
}

fn demo_column_2() -> impl Scene {
    bsn! {
        Node {
            display: Display::Flex,
            flex_direction: FlexDirection::Column,
            align_items: AlignItems::Stretch,
            justify_content: JustifyContent::Start,
            padding: px(8),
            row_gap: px(8),
            width: percent(30),
            min_width: px(200),
        }
        Children [
            @pane() Children [
                @pane_header() Children [
                    @FeathersToolButton {
                        @variant: ButtonVariant::Primary,
                        @caption: bsn! { @caption("\u{0398}") }
                    }
                    --
                    @pane_header_divider()
                    --
                    @FeathersToolButton {
                        @variant: ButtonVariant::Plain,
                        @caption: bsn! { @caption("\u{00BC}") }
                    }
                    --
                    @FeathersToolButton {
                        @variant: ButtonVariant::Plain,
                        @caption: bsn! { @caption("\u{00BD}") }
                    }
                    --
                    @FeathersToolButton {
                        @variant: ButtonVariant::Plain,
                        @caption: bsn! { @caption("\u{00BE}") }
                    }
                    --
                    @pane_header_divider()
                    --
                    @FeathersToolButton {
                        @variant: ButtonVariant::Plain,
                        @caption: bsn! { @icon(icons::CHEVRON_DOWN) }
                    }
                    --
                    @flex_spacer()
                    --
                    @FeathersToolButton {
                        @variant: ButtonVariant::Plain,
                        @caption: bsn! { @icon(icons::X) }
                    }
                ]
                --
                @pane_body() Children [
                    @label_dim("A standard editor pane")
                    --
                    @subpane() Children [
                        @subpane_header() Children [
                            @caption("Left")
                            --
                            @caption("Center")
                            --
                            @caption("Right")
                        ]
                        --
                        @subpane_body() Children [
                            @label_dim("A standard sub-pane")
                            --
                            @group() Children [
                                @group_header() Children [
                                    @caption("Group")
                                ]
                                --
                                @group_body()
                                Children [
                                    @label("A standard group")
                                    --
                                    @label_small("Scalar property")
                                    --
                                    @FeathersNumberInput
                                    DemoScalarField
                                    NumberInputPrecision(2)
                                    HardLimit::f32(0.0..=100.0)
                                    Node {
                                        flex_grow: 1.0,
                                        max_width: px(100),
                                    }
                                    on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
                                        states.scalar_prop = value_change.value;
                                    })
                                    --
                                    @label_small("Scalar property (copy)")
                                    --
                                    @FeathersNumberInput
                                    DemoScalarField
                                    NumberInputPrecision(4)
                                    Node {
                                        flex_grow: 1.0,
                                        max_width: px(100),
                                    }
                                    on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
                                        states.scalar_prop = value_change.value;
                                    })
                                    --
                                    @label_small("Vec3 property")
                                    --
                                    Node {
                                        display: Display::Flex,
                                        flex_direction: FlexDirection::Row,
                                        column_gap: px(6),
                                        align_items: AlignItems::Center,
                                        justify_content: JustifyContent::SpaceBetween,
                                    }
                                    Children [
                                        @FeathersNumberInput {
                                            @sigil_color: tokens::TEXT_INPUT_X_AXIS,
                                            @label_text: "X",
                                        }
                                        NumberInputPrecision(2)
                                        DemoVec3Field::X
                                        Node {
                                            flex_grow: 1.0,
                                        }
                                        BorderColor::all(palette::X_AXIS)
                                        on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
                                            states.vec3_prop.x = value_change.value;
                                        })
                                        --
                                        @FeathersNumberInput {
                                            @sigil_color: tokens::TEXT_INPUT_Y_AXIS,
                                            @label_text: "Y",
                                        }
                                        NumberInputPrecision(2)
                                        DemoVec3Field::Y
                                        Node {
                                            flex_grow: 1.0,
                                        }
                                        on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
                                            states.vec3_prop.y = value_change.value;
                                        })
                                        --
                                        @FeathersNumberInput {
                                            @sigil_color: tokens::TEXT_INPUT_Z_AXIS,
                                            @label_text: "Z",
                                        }
                                        NumberInputPrecision(2)
                                        DemoVec3Field::Z
                                        Node {
                                            flex_grow: 1.0,
                                        }
                                        on(|value_change: On<ValueChange<f32>>, mut states: ResMut<DemoWidgetStates>| {
                                            states.vec3_prop.z = value_change.value;
                                        })
                                    ]
                                    --
                                    @label_small("Color property")
                                    --
                                    Node {
                                        display: Display::Flex,
                                        flex_direction: FlexDirection::Row,
                                        column_gap: px(6),
                                        align_items: AlignItems::Center,
                                        justify_content: JustifyContent::SpaceBetween,
                                    }
                                    Children [
                                        @FeathersNumberInput {
                                            @sigil_color: tokens::TEXT_INPUT_X_AXIS,
                                            @label_text: "R",
                                        }
                                        InteractionDisabled
                                        NumberInputPrecision(2)
                                        HardLimit(NumberInputRange::F32(0.0..=1.0))
                                        Node {
                                            flex_grow: 1.0,
                                        }
                                        BorderColor::all(palette::X_AXIS)
                                        --
                                        @FeathersNumberInput {
                                            @sigil_color: tokens::TEXT_INPUT_Y_AXIS,
                                            @label_text: "G",
                                        }
                                        InteractionDisabled
                                        NumberInputPrecision(2)
                                        HardLimit(NumberInputRange::F32(0.0..=1.0))
                                        Node {
                                            flex_grow: 1.0,
                                        }
                                        --
                                        @FeathersNumberInput {
                                            @sigil_color: tokens::TEXT_INPUT_Z_AXIS,
                                            @label_text: "B",
                                        }
                                        InteractionDisabled
                                        NumberInputPrecision(2)
                                        HardLimit(NumberInputRange::F32(0.0..=1.0))
                                        Node {
                                            flex_grow: 1.0,
                                        }
                                        --
                                        @FeathersNumberInput {
                                            @sigil_color: tokens::TEXT_INPUT_W_AXIS,
                                            @label_text: "A",
                                        }
                                        InteractionDisabled
                                        NumberInputPrecision(2)
                                        HardLimit(NumberInputRange::F32(0.0..=1.0))
                                        Node {
                                            flex_grow: 1.0,
                                        }
                                    ]
                                ]
                            ]
                        ]
                    ]
                ]
            ]
            --
            @subpane() Children [
                @subpane_header() Children [
                    @caption("List")
                ]
                --
                @subpane_body() Children [
                    @FeathersListView {
                        @rows: bsn_list! {
                            @FeathersListRow Children [ @caption("First World") ]
                            --
                            @FeathersListRow Selected Children [ @caption("Second Nature") ]
                            --
                            @FeathersListRow Children [ @caption("Third Degree") ]
                            --
                            @FeathersListRow InteractionDisabled Children [ @caption("Fourth Wall") ]
                            --
                            @FeathersListRow Children [ @caption("Fifth Column") ]
                            --
                            @FeathersListRow Children [ @caption("Sixth Sense") ]
                            --
                            @FeathersListRow Children [ @caption("Seventh Heaven") ]
                            --
                            @FeathersListRow Children [ @caption("Eighth Wonder") ]
                            --
                            @FeathersListRow Children [ @caption("Ninth Inning") ]
                            --
                            @FeathersListRow Children [ @caption("Tenth Amendment") ]
                            --
                            @FeathersListRow Children [ @caption("Eleventh Hour") ]
                            --
                            @FeathersListRow Children [ @caption("Twelfth Night") ]
                        }
                    }
                    Node {
                        max_height: px(130)
                    }
                    on(listbox_update_selection)
                ]
            ]
        ]
    }
}

fn demo_column_3() -> impl Scene {
    bsn! {
        Node {
            display: Display::Flex,
            flex_direction: FlexDirection::Column,
            align_items: AlignItems::Stretch,
            justify_content: JustifyContent::Start,
            padding: px(8),
            row_gap: px(8),
            width: percent(15),
            min_width: px(100),
        }
        Children [
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::SpaceBetween,
            }
            Children [
                @label("Armor Tint")
                --
                @FeathersColorInput
                ColorInputValue(palettes::tailwind::AMBER_600)
                on(color_input_self_update)
            ]
            --
            Node {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::SpaceBetween,
            }
            Children [
                @label("Skin Color")
                --
                @FeathersColorInput
                ColorInputValue(palettes::tailwind::BLUE_800)
                on(color_input_self_update)
            ]
        ]
    }
}

fn update_colors(
    states: Res<DemoWidgetStates>,
    mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>,
    swatches: Query<(Entity, &SwatchType), With<FeathersColorSwatch>>,
    mut color_planes: Query<(&mut ColorPlaneValue, &FeathersColorPlane)>,
    q_text_input: Single<(Entity, &mut EditableText), With<HexColorInput>>,
    q_scalar_input: Query<Entity, With<DemoScalarField>>,
    q_vec3_input: Query<(Entity, &DemoVec3Field)>,
    mut commands: Commands,
    focus: Res<InputFocus>,
) {
    if states.is_changed() {
        for (slider_ent, slider, mut base) in sliders.iter_mut() {
            match slider.channel {
                ColorChannel::Red => {
                    base.0 = states.rgb_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.rgb_color.red));
                }
                ColorChannel::Green => {
                    base.0 = states.rgb_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.rgb_color.green));
                }
                ColorChannel::Blue => {
                    base.0 = states.rgb_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.rgb_color.blue));
                }
                ColorChannel::HslHue => {
                    base.0 = states.hsl_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.hsl_color.hue));
                }
                ColorChannel::HslSaturation => {
                    base.0 = states.hsl_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.hsl_color.saturation));
                }
                ColorChannel::HslLightness => {
                    base.0 = states.hsl_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.hsl_color.lightness));
                }
                ColorChannel::OkhslHue => {
                    base.0 = states.okhsl_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.okhsl_color.hue));
                }
                ColorChannel::OkhslSaturation => {
                    base.0 = states.okhsl_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.okhsl_color.saturation));
                }
                ColorChannel::OkhslLightness => {
                    base.0 = states.okhsl_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.okhsl_color.lightness));
                }
                ColorChannel::Alpha => {
                    base.0 = states.rgb_color.into();
                    commands
                        .entity(slider_ent)
                        .insert(SliderValue(states.rgb_color.alpha));
                }
            }
        }

        for (swatch_ent, swatch_type) in swatches.iter() {
            commands
                .entity(swatch_ent)
                .insert(ColorSwatchValue(match swatch_type {
                    SwatchType::Rgb => states.rgb_color.into(),
                    SwatchType::Hsl => states.hsl_color.into(),
                    SwatchType::Okhsl => states.okhsl_color.into(),
                }));
        }

        for (mut plane_value, plane_type) in color_planes.iter_mut() {
            match plane_type {
                FeathersColorPlane::OkhslHueLightness => {
                    plane_value.0.x = states.okhsl_color.hue / 360.0;
                    plane_value.0.y = states.okhsl_color.lightness;
                    plane_value.0.z = states.okhsl_color.saturation;
                }
                FeathersColorPlane::OkhslHueSaturation => {
                    plane_value.0.x = states.okhsl_color.hue / 360.0;
                    plane_value.0.y = states.okhsl_color.saturation;
                    plane_value.0.z = states.okhsl_color.lightness;
                }
                FeathersColorPlane::HueLightness => {
                    plane_value.0.x = states.hsl_color.hue / 360.0;
                    plane_value.0.y = states.hsl_color.lightness;
                    plane_value.0.z = states.hsl_color.saturation;
                }
                FeathersColorPlane::HueSaturation => {
                    plane_value.0.x = states.hsl_color.hue / 360.0;
                    plane_value.0.y = states.hsl_color.saturation;
                    plane_value.0.z = states.hsl_color.lightness;
                }
                FeathersColorPlane::RedBlue => {
                    plane_value.0.x = states.rgb_color.red;
                    plane_value.0.y = states.rgb_color.blue;
                    plane_value.0.z = states.rgb_color.green;
                }
                FeathersColorPlane::GreenBlue => {
                    plane_value.0.x = states.rgb_color.green;
                    plane_value.0.y = states.rgb_color.blue;
                    plane_value.0.z = states.rgb_color.red;
                }
                FeathersColorPlane::RedGreen => {
                    plane_value.0.x = states.rgb_color.red;
                    plane_value.0.y = states.rgb_color.green;
                    plane_value.0.z = states.rgb_color.blue;
                }
            }
        }

        // Only update the hex input field when it's not focused, otherwise it interferes
        // with typing.
        let (input_ent, mut editable_text) = q_text_input.into_inner();
        if Some(input_ent) != focus.get() {
            editable_text.queue_edit(TextEdit::SelectAll);
            editable_text.queue_edit(TextEdit::Insert(states.rgb_color.to_hex().into()));
        }

        for scalar_input_ent in q_scalar_input.iter() {
            commands
                .entity(scalar_input_ent)
                .insert(NumberInputValue::F32(states.scalar_prop));
        }

        for (vec3_input_ent, axis) in q_vec3_input.iter() {
            let new_value = match axis {
                DemoVec3Field::X => states.vec3_prop.x,
                DemoVec3Field::Y => states.vec3_prop.y,
                DemoVec3Field::Z => states.vec3_prop.z,
            };

            commands
                .entity(vec3_input_ent)
                .insert(NumberInputValue::F32(new_value));
        }
    }
}

fn handle_hex_color_change(
    _change: On<TextEditChange>,
    q_text_input: Single<&EditableText, With<HexColorInput>>,
    mut colors: ResMut<DemoWidgetStates>,
) {
    let editable_text = *q_text_input;
    if let Ok(color) = Srgba::hex(editable_text.value().to_string())
        && color != colors.rgb_color
    {
        colors.rgb_color = color;
    }
}

fn spawn_quit_dialog(activate: On<Activate>, mut commands: Commands) {
    commands
        .entity(activate.event_target())
        .queue_spawn_related_scenes::<Children>(bsn_list! {
            @FeathersDialog {
                @width: px(320),
                @contents: bsn_list! {
                    @FeathersDialogHeader Children [
                        @caption("Quit Feathers Gallery")
                        --
                        @FeathersDialogClose
                    ]
                    --
                    @FeathersDialogBody Children [
                        @caption("Are you really sure you want to quit? I mean, really, really sure?")
                    ]
                    --
                    @FeathersDialogFooter Children [
                        @FeathersButton {
                            @caption: bsn! { @caption("Cancel") },
                        }
                        AccessibleLabel("Cancel")
                        on(|activate: On<Activate>, mut commands: Commands| {
                            commands.trigger(RequestClose { source: activate.event_target() });
                        })
                        --
                        @FeathersButton {
                            @caption: bsn! { @caption("Exit Application") },
                            @variant: ButtonVariant::Primary,
                        }
                        AccessibleLabel("Exit Application")
                        on(|_activate: On<Activate>, mut exit: MessageWriter<AppExit>| {
                            exit.write(AppExit::Success);
                        })
                    ]
                }
            }
            on(|close: On<RequestClose>, mut commands: Commands| {
                commands.entity(close.event_target()).despawn();
            })
        });
}

fn toggle_demo_dialog(
    change: On<ValueChange<bool>>,
    mut commands: Commands,
    dialogs: Query<Entity, With<FeathersFloatingDialog>>,
) {
    let toggle = change.source;
    if change.value {
        commands.entity(toggle).insert(Checked);
        // Spawn at the root rather than as a child of the toggle, so clicks on the
        // dialog don't bubble up to the toggle switch.
        commands.spawn_scene(bsn! {
            @FeathersFloatingDialog {
                @title: {"Hello".to_string()},
                @width: px(280),
                @contents: bsn_list! {
                    @caption("Close this dialog to unset the toggle.")
                }
            }
            // The dialog despawns itself on close; this just clears the toggle.
            on(|_close: On<RequestClose>, mut commands: Commands, toggles: Query<Entity, With<DemoDialogToggle>>| {
                for toggle in toggles.iter() {
                    commands.entity(toggle).remove::<Checked>();
                }
            })
        });
    } else {
        commands.entity(toggle).remove::<Checked>();
        for dialog in dialogs.iter() {
            commands.entity(dialog).despawn();
        }
    }
}