//! 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 Box + 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| { info!("Menu item 4 clicked!"); }) -- @FeathersMenuItem { @caption: bsn! { @caption("MenuItem 5") } } on(|_: On| { info!("Menu item 5 clicked!"); }) -- @FeathersMenuItem { @caption: bsn! { @caption("MenuItem 6") } } on(|_: On| { 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| { info!("Normal button clicked!"); }) AutoFocus -- @FeathersButton { @caption: bsn! { @caption("Disabled") }, } Node { flex_grow: 1.0, } AccessibleLabel("Disabled") InteractionDisabled DemoDisabledButton on(|_activate: On| { info!("Disabled button clicked!"); }) -- @FeathersButton { @caption: bsn! { @caption("Primary") }, @variant: ButtonVariant::Primary, } AccessibleLabel("Primary") Node { flex_grow: 1.0, } on(|_activate: On| { 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| { info!("Menu item 1 clicked!"); }) -- @FeathersMenuItem { @caption: bsn! { @caption("MenuItem 2") } } on(|_: On| { info!("Menu item 2 clicked!"); }) -- @FeathersMenuDivider -- @FeathersMenuItem { @caption: bsn! { @caption("MenuItem 3") } } on(|_: On| { 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>, 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 { 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::>(), ) as Box }, @max_visible: 6, } Node { flex_grow: 1.0, } on(|change: On>, 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| { info!("Left button clicked!"); }) -- @FeathersButton { @caption: bsn! { @caption("Center") }, @corners: RoundedCorners::None, } Node { flex_grow: 1.0, } AccessibleLabel("Center") on(|_activate: On| { 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| { 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, mut ovr: ResMut| { 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>, query: Query>, mut commands: Commands| { info!("Checkbox clicked!"); let mut button = commands.entity(query.single().unwrap()); if change.value { button.insert(InteractionDisabled); } else { button.remove::(); } let mut checkbox = commands.entity(change.source); if change.value { checkbox.insert(Checked); } else { checkbox.remove::(); } }) -- @FeathersCheckbox { @caption: bsn! { @caption("Fast Click Checkbox") } } ActivateOnPress AccessibleLabel("Fast Click Checkbox Example") on(|change: On>, mut commands: Commands| { info!("Checkbox clicked!"); let mut checkbox = commands.entity(change.source); if change.value { checkbox.insert(Checked); } else { checkbox.remove::(); } }) -- @FeathersCheckbox { @caption: bsn! { @caption("Disabled") }, } InteractionDisabled AccessibleLabel("Disabled Checkbox Example") on(|_change: On>| { warn!("Disabled checkbox clicked!"); }) -- @FeathersCheckbox { @caption: bsn! { @caption("Checked+Disabled") } } InteractionDisabled Checked AccessibleLabel("Disabled and Checked Checkbox Example") on(|_change: On>| { 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>, mut color: ResMut| { 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>, mut color: ResMut| { color.rgb_color.red = change.value; }) -- @FeathersColorSlider { @value: 0.5, @channel: ColorChannel::Green } AccessibleLabel("Green Channel") on(|change: On>, mut color: ResMut| { color.rgb_color.green = change.value; }) -- @FeathersColorSlider { @value: 0.5, @channel: ColorChannel::Blue } AccessibleLabel("Blue Channel") on(|change: On>, mut color: ResMut| { color.rgb_color.blue = change.value; }) -- @FeathersColorSlider { @value: 0.5, @channel: ColorChannel::Alpha } AccessibleLabel("Alpha Channel") on(|change: On>, mut color: ResMut| { 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>, mut color: ResMut| { color.hsl_color.hue = change.value; }) -- @FeathersColorSlider { @value: 0.5, @channel: ColorChannel::HslSaturation } AccessibleLabel("Saturation Channel") on(|change: On>, mut color: ResMut| { color.hsl_color.saturation = change.value; }) -- @FeathersColorSlider { @value: 0.5, @channel: ColorChannel::HslLightness } AccessibleLabel("Lightness Channel") on(|change: On>, mut color: ResMut| { 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>, mut color: ResMut| { 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>, mut color: ResMut| { color.okhsl_color.hue = change.value; }) -- @FeathersColorSlider { @value: 0.5, @channel: ColorChannel::OkhslSaturation } AccessibleLabel("Okhsl Saturation Channel") on(|change: On>, mut color: ResMut| { color.okhsl_color.saturation = change.value; }) -- @FeathersColorSlider { @value: 0.5, @channel: ColorChannel::OkhslLightness } AccessibleLabel("Okhsl Lightness Channel") on(|change: On>, mut color: ResMut| { 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>, mut states: ResMut| { 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>, mut states: ResMut| { 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>, mut states: ResMut| { 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>, mut states: ResMut| { 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>, mut states: ResMut| { 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, mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>, swatches: Query<(Entity, &SwatchType), With>, mut color_planes: Query<(&mut ColorPlaneValue, &FeathersColorPlane)>, q_text_input: Single<(Entity, &mut EditableText), With>, q_scalar_input: Query>, q_vec3_input: Query<(Entity, &DemoVec3Field)>, mut commands: Commands, focus: Res, ) { 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, q_text_input: Single<&EditableText, With>, mut colors: ResMut, ) { 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, mut commands: Commands) { commands .entity(activate.event_target()) .queue_spawn_related_scenes::(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, 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, mut exit: MessageWriter| { exit.write(AppExit::Success); }) ] } } on(|close: On, mut commands: Commands| { commands.entity(close.event_target()).despawn(); }) }); } fn toggle_demo_dialog( change: On>, mut commands: Commands, dialogs: Query>, ) { 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, mut commands: Commands, toggles: Query>| { for toggle in toggles.iter() { commands.entity(toggle).remove::(); } }) }); } else { commands.entity(toggle).remove::(); for dialog in dialogs.iter() { commands.entity(dialog).despawn(); } } }