Skip to main content

multiple_text_inputs/
multiple_text_inputs.rs

1//! Demonstrates multiple text inputs
2//!
3//! This example arranges three text inputs in a 3x3 grid layout.  The first column of each row is an [`EditableText`] text input node, the second column is a `Text` node
4//! that is kept synchronized with the [`EditableText`]'s contents by the [`synchronize_output_text`] system, and the third column is updated
5//! by the [`submit_text`] system when the user submits the [`EditableText`]'s text by pressing `Enter`.
6
7use bevy::color::palettes::tailwind::SLATE_300;
8use bevy::input::keyboard::Key;
9use bevy::input_focus::tab_navigation::NavAction;
10use bevy::input_focus::{tab_navigation::TabNavigation, AutoFocus, FocusCause};
11use bevy::input_focus::{
12    tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
13    InputFocus,
14};
15use bevy::prelude::*;
16use bevy::text::{EditableText, TextCursorStyle};
17
18fn main() {
19    App::new()
20        // `EditableTextInputPlugin` is part of `DefaultPlugins`
21        .add_plugins((DefaultPlugins, TabNavigationPlugin))
22        .add_systems(Startup, setup)
23        .add_systems(
24            Update,
25            (
26                synchronize_output_text,
27                submit_text,
28                update_row_border_colors,
29            ),
30        )
31        .run();
32}
33
34#[derive(Component)]
35struct TextOutput;
36
37#[derive(Component)]
38struct SubmitOutput;
39
40#[derive(Component)]
41struct TextInputRow(usize);
42
43fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
44    commands.spawn(Camera2d);
45
46    let font = TextFont {
47        font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
48        font_size: FontSize::Px(24.),
49        ..default()
50    };
51
52    commands
53        .spawn((
54            Node {
55                width: percent(100.),
56                height: percent(100.),
57                display: Display::Grid,
58                justify_content: JustifyContent::Center,
59                align_content: AlignContent::Center,
60                grid_template_columns: RepeatedGridTrack::px(3, 320.),
61                grid_template_rows: RepeatedGridTrack::auto(6),
62                row_gap: px(8.),
63                column_gap: px(8.),
64                ..default()
65            },
66            TabGroup::default(),
67        ))
68        .with_children(|parent| {
69            parent.spawn((
70                Text::new("Multiple Text Inputs Example"),
71                Node {
72                    grid_column: GridPlacement::span(3),
73                    justify_self: JustifySelf::Center,
74                    margin: px(16).bottom(),
75                    ..default()
76                },
77                TextColor::WHITE,
78                font.clone(),
79            ));
80
81            let label_font = font.clone().with_font_size(14.);
82            for label in ["EditableText", "value", "submission"] {
83                parent.spawn((
84                    Text::new(label),
85                    label_font.clone(),
86                    Node {
87                        justify_self: JustifySelf::Center,
88                        margin: px(-4).bottom(),
89                        ..default()
90                    },
91                ));
92            }
93
94            for row in 0..3 {
95                let mut input = parent.spawn((
96                    Node {
97                        border: px(4.).all(),
98                        padding: px(4.).all(),
99                        ..default()
100                    },
101                    EditableText::new(format!("Initial text {row}")),
102                    TextCursorStyle::default(),
103                    font.clone(),
104                    BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()),
105                    TextInputRow(row),
106                    TextLayout::no_wrap(),
107                    TabIndex(row as i32),
108                    BorderColor::all(SLATE_300),
109                ));
110                if row == 0 {
111                    input.insert(AutoFocus);
112                }
113
114                parent.spawn((
115                    Node {
116                        border: px(4.).all(),
117                        padding: px(4.).all(),
118                        overflow: Overflow::clip_x(),
119                        overflow_clip_margin: OverflowClipMargin {
120                            visual_box: VisualBox::ContentBox,
121                            ..default()
122                        },
123                        ..default()
124                    },
125                    BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()),
126                    BorderColor::all(Color::WHITE),
127                    children![(
128                        Text::default(),
129                        TextLayout::no_wrap(),
130                        font.clone(),
131                        BackgroundColor(bevy::color::palettes::css::DARK_SLATE_GRAY.into()),
132                        BorderColor::all(Color::WHITE),
133                        TextInputRow(row),
134                        TextOutput,
135                    )],
136                ));
137
138                parent.spawn((
139                    Node {
140                        border: px(4.).all(),
141                        padding: px(4.).all(),
142                        overflow: Overflow::clip_x(),
143                        overflow_clip_margin: OverflowClipMargin {
144                            visual_box: VisualBox::ContentBox,
145                            ..default()
146                        },
147
148                        ..default()
149                    },
150                    BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()),
151                    BorderColor::all(Color::WHITE),
152                    children![(
153                        Text::default(),
154                        TextLayout::no_wrap(),
155                        font.clone(),
156                        TextInputRow(row),
157                        SubmitOutput,
158                    )],
159                ));
160            }
161
162            parent.spawn((
163                Text::new("Press Enter to submit"),
164                Node {
165                    grid_column: GridPlacement::span(3),
166                    justify_self: JustifySelf::Center,
167                    margin: px(16).top(),
168                    ..default()
169                },
170                font.clone(),
171            ));
172        });
173}
174
175/// This system keeps the text of the [`TextOutput`] [`Text`] nodes synchronized with the text
176/// of the [`EditableText`] node on the same row.
177fn synchronize_output_text(
178    changed_inputs: Query<(&EditableText, &TextInputRow), Changed<EditableText>>,
179    mut outputs: Query<(&mut Text, &TextInputRow), With<TextOutput>>,
180) {
181    for (editable_text, input_row) in &changed_inputs {
182        for (mut text, output_row) in &mut outputs {
183            if output_row.0 == input_row.0 {
184                // `EditableText::value()` returns a `SplitString` because Parley may keep IME preedit text
185                // in a contiguous range of the editor’s internal `String` buffer during composition.
186                // The returned `SplitString` omits that preedit range, exposing only the text before and after it.
187                //
188                // To avoid allocating a new `String`, we reserve the total length of the `SplitString`'s slices,
189                // then append them to the output `Text`.
190                text.0.clear();
191                text.0
192                    .reserve(editable_text.value().into_iter().map(str::len).sum());
193                for sub_str in editable_text.value() {
194                    text.0.push_str(sub_str);
195                }
196            }
197        }
198    }
199}
200
201// Submit the focused input's text when Enter is pressed.
202fn submit_text(
203    mut input_focus: ResMut<InputFocus>,
204    keyboard_input: Res<ButtonInput<Key>>,
205    mut text_input: Query<(&mut EditableText, &TextInputRow)>,
206    mut text_output: Query<(&mut Text, &TextInputRow), With<SubmitOutput>>,
207    tab_navigation: TabNavigation,
208) {
209    if keyboard_input.just_pressed(Key::Enter)
210        && let Some(focused_entity) = input_focus.get()
211        && let Ok((mut editable_text, input_row)) = text_input.get_mut(focused_entity)
212    {
213        for (mut text, output_row) in &mut text_output {
214            if input_row.0 == output_row.0 {
215                text.0.clear();
216                text.0
217                    .reserve(editable_text.value().into_iter().map(str::len).sum());
218                for sub_str in editable_text.value() {
219                    text.0.push_str(sub_str);
220                }
221                break;
222            }
223        }
224        editable_text.clear();
225
226        if let Ok(next) = tab_navigation.navigate(&input_focus, NavAction::Next) {
227            input_focus.set(next, FocusCause::Navigated);
228        }
229    }
230}
231
232/// Dim a row's border colors when its [`EditableText`] does not have input focus.
233fn update_row_border_colors(
234    input_focus: Res<InputFocus>,
235    input_rows: Query<&TextInputRow, With<EditableText>>,
236    mut row_borders: Query<(&TextInputRow, &mut BorderColor, Has<EditableText>)>,
237) {
238    if !input_focus.is_changed() {
239        return;
240    }
241
242    let focused_row = input_focus
243        .get()
244        .and_then(|focused_entity| input_rows.get(focused_entity).ok())
245        .map(|row| row.0);
246
247    for (row, mut border_color, is_input) in &mut row_borders {
248        let mut color = if is_input {
249            SLATE_300.into()
250        } else {
251            Color::WHITE
252        };
253        if Some(row.0) != focused_row {
254            color = color.darker(0.75);
255        }
256        border_color.set_all(color);
257    }
258}