diff --git a/examples/keybinding_menu.rs b/examples/keybinding_menu.rs index 1fefd18e..649f0d15 100644 --- a/examples/keybinding_menu.rs +++ b/examples/keybinding_menu.rs @@ -2,6 +2,10 @@ use core::{error::Error, fmt::Write}; use std::fs; use bevy::{ + ecs::{ + relationship::RelatedSpawner, + spawn::{SpawnWith, SpawnableList}, + }, input::{ButtonState, common_conditions::*, keyboard::KeyboardInput, mouse::MouseButtonInput}, prelude::*, ui::FocusPolicy, @@ -59,41 +63,41 @@ fn setup(mut commands: Commands) { commands.spawn(Camera2d); // We use separate root node to let dialogs cover the whole UI. - commands - .spawn(Node { + commands.spawn(( + Node { width: Val::Percent(100.0), height: Val::Percent(100.0), ..Default::default() - }) - .with_children(|parent| { - parent - .spawn(Node { - flex_direction: FlexDirection::Column, - width: Val::Percent(100.0), - height: Val::Percent(100.0), - padding: PADDING, - row_gap: GAP, - ..Default::default() - }) - .with_children(|parent| { - setup_actions(parent, &settings); - - parent - .spawn(Node { - align_items: AlignItems::End, - width: Val::Percent(100.0), - height: Val::Percent(100.0), - justify_content: JustifyContent::End, - ..Default::default() - }) - .with_children(|parent| { - parent - .spawn(SettingsButton) - .with_child(Text::new("Apply")) - .observe(apply); - }); - }); - }); + }, + children![( + Node { + flex_direction: FlexDirection::Column, + width: Val::Percent(100.0), + height: Val::Percent(100.0), + padding: PADDING, + row_gap: GAP, + ..Default::default() + }, + children![ + actions_grid(settings.clone()), + ( + Node { + align_items: AlignItems::End, + width: Val::Percent(100.0), + height: Val::Percent(100.0), + justify_content: JustifyContent::End, + ..Default::default() + }, + children![( + SettingsButton, + Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<_>| { + parent.spawn(Text::new("Apply")).observe(apply); + })) + )], + ) + ] + )], + )); commands.insert_resource(settings); } @@ -122,85 +126,70 @@ struct SettingsField(&'static str); /// Number of input columns. const INPUTS_PER_ACTION: usize = 3; -fn setup_actions(parent: &mut ChildSpawnerCommands, settings: &KeyboardSettings) -> Entity { - parent - .spawn(Node { +fn actions_grid(settings: KeyboardSettings) -> impl Bundle { + ( + Node { display: Display::Grid, column_gap: GAP, row_gap: GAP, grid_template_columns: vec![GridTrack::auto(); INPUTS_PER_ACTION + 1], ..Default::default() - }) - .with_children(|parent| { - // We could utilzie reflection to iterate over fields, - // but in real application you most likely want to have a nice and translatable text on buttons. - setup_action_row( - parent, + }, + // We could utilzie reflection to iterate over fields, + // but in real application you most likely want to have a nice and translatable text on buttons. + Children::spawn(( + action_row( "Forward", - &settings.forward, settings_field!(settings.forward), - ); - setup_action_row( - parent, - "Left", - &settings.left, - settings_field!(settings.left), - ); - setup_action_row( - parent, + settings.forward, + ), + action_row("Left", settings_field!(settings.left), settings.left), + action_row( "Backward", - &settings.backward, settings_field!(settings.backward), - ); - setup_action_row( - parent, - "Right", - &settings.right, - settings_field!(settings.right), - ); - setup_action_row( - parent, - "Jump", - &settings.jump, - settings_field!(settings.jump), - ); - setup_action_row(parent, "Run", &settings.run, settings_field!(settings.run)); - }) - .id() + settings.backward, + ), + action_row("Right", settings_field!(settings.right), settings.right), + action_row("Jump", settings_field!(settings.jump), settings.jump), + action_row("Run", settings_field!(settings.run), settings.run), + )), + ) } -fn setup_action_row( - parent: &mut ChildSpawnerCommands, +fn action_row( name: &'static str, - inputs: &[Input], field: SettingsField, -) { - parent.spawn((Text::new(name), DARK_TEXT)); - for index in 0..INPUTS_PER_ACTION { - parent - .spawn(Node { - column_gap: GAP, - align_items: AlignItems::Center, - ..Default::default() - }) - .with_children(|parent| { - let button_entity = parent - .spawn(( - field, - Name::new(name), - InputButton { - input: inputs.get(index).copied(), - }, - )) - .with_child(Text::default()) // Will be updated automatically on `InputButton` insertion - .observe(show_binding_dialog) - .id(); - parent - .spawn(DeleteButton { button_entity }) - .with_child(Text::new("X")) - .observe(delete_binding); - }); - } + inputs: Vec, +) -> impl SpawnableList { + ( + Spawn((Text::new(name), DARK_TEXT)), + SpawnWith(move |parent: &mut RelatedSpawner<_>| { + for index in 0..INPUTS_PER_ACTION { + let input = inputs.get(index).copied(); + parent.spawn(( + Node { + column_gap: GAP, + align_items: AlignItems::Center, + ..Default::default() + }, + Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<_>| { + let button_entity = parent + .spawn(( + field, + Name::new(name), + InputButton { input }, + children![Text::default()], // Will be updated automatically on `InputButton` insertion + )) + .observe(show_binding_dialog) + .id(); + parent + .spawn((DeleteButton { button_entity }, children![Text::new("X")])) + .observe(delete_binding); + })), + )); + } + }), + ) } fn delete_binding( @@ -225,36 +214,30 @@ fn show_binding_dialog( let name = names.get(trigger.target()).unwrap(); info!("starting binding for '{name}'"); - commands.entity(*root_entity).with_children(|parent| { - parent - .spawn(BindingDialog { - button_entity: trigger.target(), - }) - .with_children(|parent| { - parent - .spawn(( - Node { - flex_direction: FlexDirection::Column, - padding: PADDING, - row_gap: GAP, - ..Default::default() - }, - PANEL_BACKGROUND, - )) - .with_children(|parent| { - parent.spawn(( - TextLayout { - justify: JustifyText::Center, - ..Default::default() - }, - DARK_TEXT, - Text::new(format!( - "Binding \"{name}\", \npress any key or Esc to cancel", - )), - )); - }); - }); - }); + commands.entity(*root_entity).with_child(( + BindingDialog { + button_entity: trigger.target(), + }, + children![( + Node { + flex_direction: FlexDirection::Column, + padding: PADDING, + row_gap: GAP, + ..Default::default() + }, + PANEL_BACKGROUND, + children![( + TextLayout { + justify: JustifyText::Center, + ..Default::default() + }, + DARK_TEXT, + Text::new(format!( + "Binding \"{name}\", \npress any key or Esc to cancel", + )), + )] + )], + )); } fn bind( @@ -286,47 +269,42 @@ fn bind( { info!("found conflict with '{name}' for '{input}'"); - commands.entity(*root_entity).with_children(|parent| { - parent - .spawn(ConflictDialog { - button_entity: dialog.button_entity, - conflict_entity, - }) - .with_children(|parent| { - parent - .spawn(( - Node { - flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, - padding: PADDING, - row_gap: GAP, - ..Default::default() - }, - PANEL_BACKGROUND, - )) - .with_children(|parent| { - parent.spawn(( - DARK_TEXT, - Text::new(format!("\"{input}\" is already used by \"{name}\"",)), - )); + commands.entity(*root_entity).with_child(( + ConflictDialog { + button_entity: dialog.button_entity, + conflict_entity, + }, + children![( + Node { + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + padding: PADDING, + row_gap: GAP, + ..Default::default() + }, + PANEL_BACKGROUND, + children![ + ( + DARK_TEXT, + Text::new(format!("\"{input}\" is already used by \"{name}\"",)), + ), + ( + Node { + column_gap: GAP, + ..Default::default() + }, + Children::spawn(SpawnWith(|parent: &mut RelatedSpawner<_>| { parent - .spawn(Node { - column_gap: GAP, - ..Default::default() - }) - .with_children(|parent| { - parent - .spawn(SettingsButton) - .with_child(Text::new("Replace")) - .observe(replace_binding); - parent - .spawn(SettingsButton) - .with_child(Text::new("Cancel")) - .observe(cancel_replace_binding); - }); - }); - }); - }); + .spawn((SettingsButton, children![Text::new("Replace")])) + .observe(replace_binding); + parent + .spawn((SettingsButton, children![Text::new("Cancel")])) + .observe(cancel_replace_binding); + })) + ) + ] + )], + )); } else { let (_, name, mut button) = buttons .get_mut(dialog.button_entity)