//! Demonstrates multiple text inputs //! //! This example arranges text inputs in a four-column grid layout. The first column shows the text justification, the second column is an //! [`EditableText`] text input node, the third column is a `Text` node that is kept synchronized with the [`EditableText`]'s contents by the //! [`synchronize_output_text`] system, and the fourth column is updated by the [`submit_text`] system when the user submits the //! [`EditableText`]'s text by pressing `Enter`. use bevy::color::palettes::tailwind::SLATE_300; use bevy::input::keyboard::Key; use bevy::input_focus::tab_navigation::NavAction; use bevy::input_focus::{tab_navigation::TabNavigation, AutoFocus, FocusCause}; use bevy::input_focus::{ tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, InputFocus, }; use bevy::prelude::*; use bevy::text::{EditableText, TextCursorStyle}; fn main() { App::new() // `EditableTextInputPlugin` is part of `DefaultPlugins` .add_plugins((DefaultPlugins, TabNavigationPlugin)) .add_systems(Startup, setup) .add_systems( Update, ( synchronize_output_text, submit_text, update_row_border_colors, ), ) .run(); } #[derive(Component)] struct TextOutput; #[derive(Component)] struct SubmitOutput; #[derive(Component)] struct TextInputRow(usize); fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); let font = TextFont { font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), font_size: FontSize::Px(24.), ..default() }; commands .spawn(( Node { width: percent(100.), height: percent(100.), display: Display::Grid, justify_content: JustifyContent::Center, align_content: AlignContent::Center, grid_template_columns: vec![GridTrack::px(160.), RepeatedGridTrack::px(3, 320.)], row_gap: px(8.), column_gap: px(8.), ..default() }, TabGroup::default(), )) .with_children(|parent| { parent.spawn(( Text::new("Multiple Text Inputs Example"), Node { grid_column: GridPlacement::span(4), justify_self: JustifySelf::Center, margin: px(16).bottom(), ..default() }, TextColor::WHITE, font.clone(), )); let label_font = font.clone().with_font_size(14.); for label in ["Justify", "EditableText", "value", "submission"] { parent.spawn(( Text::new(label), label_font.clone(), Node { justify_self: JustifySelf::Center, margin: px(-4).bottom(), ..default() }, )); } for (row, justify) in [ Justify::Left, Justify::Center, Justify::Right, Justify::Justified, Justify::Start, Justify::End, ] .into_iter() .enumerate() { parent.spawn(( Node { border: px(4).all(), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }, BorderColor::all(Color::WHITE), children![(Text::new(format!("{justify:?}")), font.clone(),)], )); let mut input = parent.spawn(( Node { border: px(4.).all(), padding: px(4.).all(), ..default() }, EditableText::new(format!("Initial text {row}")), TextCursorStyle::default(), font.clone(), BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()), TextInputRow(row), TextLayout::no_wrap().with_justify(justify), TabIndex(row as i32), BorderColor::all(SLATE_300), )); if row == 0 { input.insert(AutoFocus); } parent.spawn(( Node { border: px(4.).all(), padding: px(4.).all(), overflow: Overflow::clip_x(), overflow_clip_margin: OverflowClipMargin { visual_box: VisualBox::ContentBox, ..default() }, ..default() }, BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()), BorderColor::all(Color::WHITE), children![( Text::default(), TextLayout::no_wrap(), font.clone(), BackgroundColor(bevy::color::palettes::css::DARK_SLATE_GRAY.into()), BorderColor::all(Color::WHITE), TextInputRow(row), TextOutput, )], )); parent.spawn(( Node { border: px(4.).all(), padding: px(4.).all(), overflow: Overflow::clip_x(), overflow_clip_margin: OverflowClipMargin { visual_box: VisualBox::ContentBox, ..default() }, ..default() }, BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()), BorderColor::all(Color::WHITE), children![( Text::default(), TextLayout::no_wrap(), font.clone(), TextInputRow(row), SubmitOutput, )], )); } parent.spawn(( Text::new("Press Enter to submit"), Node { grid_column: GridPlacement::span(4), justify_self: JustifySelf::Center, margin: px(16).top(), ..default() }, font.clone(), )); }); } /// This system keeps the text of the [`TextOutput`] [`Text`] nodes synchronized with the text /// of the [`EditableText`] node on the same row. fn synchronize_output_text( changed_inputs: Query<(&EditableText, &TextInputRow), Changed>, mut outputs: Query<(&mut Text, &TextInputRow), With>, ) { for (editable_text, input_row) in &changed_inputs { for (mut text, output_row) in &mut outputs { if output_row.0 == input_row.0 { // `EditableText::value()` returns a `SplitString` because Parley may keep IME preedit text // in a contiguous range of the editor’s internal `String` buffer during composition. // The returned `SplitString` omits that preedit range, exposing only the text before and after it. // // To avoid allocating a new `String`, we reserve the total length of the `SplitString`'s slices, // then append them to the output `Text`. text.0.clear(); text.0 .reserve(editable_text.value().into_iter().map(str::len).sum()); for sub_str in editable_text.value() { text.0.push_str(sub_str); } } } } } // Submit the focused input's text when Enter is pressed. fn submit_text( mut input_focus: ResMut, keyboard_input: Res>, mut text_input: Query<(&mut EditableText, &TextInputRow)>, mut text_output: Query<(&mut Text, &TextInputRow), With>, tab_navigation: TabNavigation, ) { if keyboard_input.just_pressed(Key::Enter) && let Some(focused_entity) = input_focus.get() && let Ok((mut editable_text, input_row)) = text_input.get_mut(focused_entity) { for (mut text, output_row) in &mut text_output { if input_row.0 == output_row.0 { text.0.clear(); text.0 .reserve(editable_text.value().into_iter().map(str::len).sum()); for sub_str in editable_text.value() { text.0.push_str(sub_str); } break; } } editable_text.clear(); if let Ok(next) = tab_navigation.navigate(&input_focus, NavAction::Next) { input_focus.set(next, FocusCause::Navigated); } } } /// Dim a row's border colors when its [`EditableText`] does not have input focus. fn update_row_border_colors( input_focus: Res, input_rows: Query<&TextInputRow, With>, mut row_borders: Query<(&TextInputRow, &mut BorderColor, Has)>, ) { if !input_focus.is_changed() { return; } let focused_row = input_focus .get() .and_then(|focused_entity| input_rows.get(focused_entity).ok()) .map(|row| row.0); for (row, mut border_color, is_input) in &mut row_borders { let mut color = if is_input { SLATE_300.into() } else { Color::WHITE }; if Some(row.0) != focused_row { color = color.darker(0.75); } border_color.set_all(color); } }