Skip to main content

multiple_text_inputs/
multiple_text_inputs.rs

1//! Demonstrates multiple text inputs
2//!
3//! This example arranges text inputs in a four-column grid layout. The first column shows the text justification, the second column is an
4//! [`TextInput`] text input node, the third column is a `Text` node that is kept synchronized with the [`TextInput`]'s contents by the
5//! [`synchronize_output_text`] system, and the fourth column is updated by the [`submit_text`] system when the user submits the
6//! [`TextInput`]'s text by pressing `Enter`.
7
8use bevy::color::palettes::tailwind::SLATE_300;
9use bevy::input::keyboard::Key;
10use bevy::input_focus::tab_navigation::NavAction;
11use bevy::input_focus::{tab_navigation::TabNavigation, AutoFocus, FocusCause};
12use bevy::input_focus::{
13    tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
14    InputFocus,
15};
16use bevy::prelude::*;
17use bevy::text::{EditableText, TextCursorStyle, TextEditChange, TextReadWriteMode};
18use bevy::ui_widgets::TextInput;
19
20fn main() {
21    App::new()
22        // `TextInputPlugin` is part of `DefaultPlugins`
23        .add_plugins((DefaultPlugins, TabNavigationPlugin))
24        .add_systems(Startup, setup)
25        .add_systems(Update, (submit_text, update_row_border_colors))
26        .add_observer(synchronize_output_text)
27        .run();
28}
29
30#[derive(Component)]
31struct TextOutput;
32
33#[derive(Component)]
34struct SubmitOutput;
35
36#[derive(Component)]
37struct TextInputRow(usize);
38
39fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
40    commands.spawn(Camera2d);
41
42    let font = TextFont {
43        font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
44        font_size: FontSize::Px(24.),
45        ..default()
46    };
47
48    commands
49        .spawn((
50            Node {
51                width: percent(100.),
52                height: percent(100.),
53                display: Display::Grid,
54                justify_content: JustifyContent::Center,
55                align_content: AlignContent::Center,
56                grid_template_columns: vec![GridTrack::px(160.), RepeatedGridTrack::px(3, 320.)],
57                row_gap: px(8.),
58                column_gap: px(8.),
59                ..default()
60            },
61            TabGroup::default(),
62        ))
63        .with_children(|parent| {
64            parent.spawn((
65                Text::new("Multiple Text Inputs Example"),
66                Node {
67                    grid_column: GridPlacement::span(4),
68                    justify_self: JustifySelf::Center,
69                    margin: px(16).bottom(),
70                    ..default()
71                },
72                TextColor::WHITE,
73                font.clone(),
74            ));
75
76            let label_font = font.clone().with_font_size(14.);
77            for label in ["Justify", "EditableText", "value", "submission"] {
78                parent.spawn((
79                    Text::new(label),
80                    label_font.clone(),
81                    Node {
82                        justify_self: JustifySelf::Center,
83                        margin: px(-4).bottom(),
84                        ..default()
85                    },
86                ));
87            }
88
89            for (row, justify) in [
90                Justify::Left,
91                Justify::Center,
92                Justify::Right,
93                Justify::Justified,
94                Justify::Start,
95                Justify::End,
96            ]
97            .into_iter()
98            .enumerate()
99            {
100                parent.spawn((
101                    Node {
102                        border: px(4).all(),
103                        justify_content: JustifyContent::Center,
104                        align_items: AlignItems::Center,
105                        ..default()
106                    },
107                    BorderColor::all(Color::WHITE),
108                    children![(Text::new(format!("{justify:?}")), font.clone(),)],
109                ));
110
111                let mut input = parent.spawn((
112                    Node {
113                        border: px(4.).all(),
114                        padding: px(4.).all(),
115                        ..default()
116                    },
117                    TextInput,
118                    EditableText::new(format!("Initial text {row}")),
119                    TextCursorStyle::default(),
120                    font.clone(),
121                    BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()),
122                    TextInputRow(row),
123                    TextLayout::no_wrap().with_justify(justify),
124                    TabIndex(row as i32),
125                    BorderColor::all(SLATE_300),
126                ));
127                if row == 0 {
128                    input.insert(AutoFocus);
129                }
130
131                parent.spawn((
132                    Node {
133                        border: px(4.).all(),
134                        padding: px(4.).all(),
135                        overflow: Overflow::clip_x(),
136                        overflow_clip_margin: OverflowClipMargin {
137                            visual_box: VisualBox::ContentBox,
138                            ..default()
139                        },
140                        ..default()
141                    },
142                    BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()),
143                    BorderColor::all(Color::WHITE),
144                    children![(
145                        Text::default(),
146                        TextLayout::no_wrap(),
147                        font.clone(),
148                        BackgroundColor(bevy::color::palettes::css::DARK_SLATE_GRAY.into()),
149                        BorderColor::all(Color::WHITE),
150                        TextInputRow(row),
151                        TextOutput,
152                    )],
153                ));
154
155                parent.spawn((
156                    Node {
157                        border: px(4.).all(),
158                        padding: px(4.).all(),
159                        overflow: Overflow::clip_x(),
160                        overflow_clip_margin: OverflowClipMargin {
161                            visual_box: VisualBox::ContentBox,
162                            ..default()
163                        },
164
165                        ..default()
166                    },
167                    BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()),
168                    BorderColor::all(Color::WHITE),
169                    children![(
170                        Text::default(),
171                        TextLayout::no_wrap(),
172                        font.clone(),
173                        TextInputRow(row),
174                        SubmitOutput,
175                    )],
176                ));
177            }
178
179            let label_font = font.clone().with_font_size(14.);
180            for label in ["ReadWrite", "EditableText", "value", "submission"] {
181                parent.spawn((
182                    Text::new(label),
183                    label_font.clone(),
184                    Node {
185                        justify_self: JustifySelf::Center,
186                        margin: px(-4).bottom(),
187                        ..default()
188                    },
189                ));
190            }
191
192            for (row, rwmode) in [
193                TextReadWriteMode::Editable,
194                TextReadWriteMode::ReadOnly,
195                TextReadWriteMode::Static,
196            ]
197            .into_iter()
198            .enumerate()
199            {
200                parent.spawn((
201                    Node {
202                        border: px(4).all(),
203                        justify_content: JustifyContent::Center,
204                        align_items: AlignItems::Center,
205                        ..default()
206                    },
207                    BorderColor::all(Color::WHITE),
208                    children![(Text::new(format!("{rwmode:?}")), font.clone(),)],
209                ));
210
211                let mut input = parent.spawn((
212                    Node {
213                        border: px(4.).all(),
214                        padding: px(4.).all(),
215                        ..default()
216                    },
217                    TextInput,
218                    EditableText::new(format!("Initial text {row}")),
219                    TextCursorStyle::default(),
220                    font.clone(),
221                    BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()),
222                    TextInputRow(row + 100),
223                    rwmode,
224                    TabIndex(row as i32 + 100),
225                    BorderColor::all(SLATE_300),
226                ));
227                if row == 0 {
228                    input.insert(AutoFocus);
229                }
230
231                parent.spawn((
232                    Node {
233                        border: px(4.).all(),
234                        padding: px(4.).all(),
235                        overflow: Overflow::clip_x(),
236                        overflow_clip_margin: OverflowClipMargin {
237                            visual_box: VisualBox::ContentBox,
238                            ..default()
239                        },
240                        ..default()
241                    },
242                    BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()),
243                    BorderColor::all(Color::WHITE),
244                    children![(
245                        Text::default(),
246                        TextLayout::no_wrap(),
247                        font.clone(),
248                        BackgroundColor(bevy::color::palettes::css::DARK_SLATE_GRAY.into()),
249                        BorderColor::all(Color::WHITE),
250                        TextInputRow(row + 100),
251                        TextOutput,
252                    )],
253                ));
254
255                parent.spawn((
256                    Node {
257                        border: px(4.).all(),
258                        padding: px(4.).all(),
259                        overflow: Overflow::clip_x(),
260                        overflow_clip_margin: OverflowClipMargin {
261                            visual_box: VisualBox::ContentBox,
262                            ..default()
263                        },
264
265                        ..default()
266                    },
267                    BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()),
268                    BorderColor::all(Color::WHITE),
269                    children![(
270                        Text::default(),
271                        TextLayout::no_wrap(),
272                        font.clone(),
273                        TextInputRow(row + 100),
274                        SubmitOutput,
275                    )],
276                ));
277            }
278
279            parent.spawn((
280                Text::new("Press Enter to submit"),
281                Node {
282                    grid_column: GridPlacement::span(4),
283                    justify_self: JustifySelf::Center,
284                    margin: px(16).top(),
285                    ..default()
286                },
287                font.clone(),
288            ));
289        });
290}
291
292/// This system keeps the text of the [`TextOutput`] [`Text`] nodes synchronized with the text
293/// of the [`EditableText`] node on the same row.
294fn synchronize_output_text(
295    on: On<TextEditChange>,
296    inputs: Query<(&EditableText, &TextInputRow)>,
297    mut outputs: Query<(&mut Text, &TextInputRow), With<TextOutput>>,
298) {
299    if let Ok((editable_text, input_row)) = &inputs.get(on.event_target()) {
300        for (mut text, output_row) in &mut outputs {
301            if output_row.0 == input_row.0 {
302                // `EditableText::value()` returns a `SplitString` because Parley may keep IME preedit text
303                // in a contiguous range of the editor’s internal `String` buffer during composition.
304                // The returned `SplitString` omits that preedit range, exposing only the text before and after it.
305                //
306                // To avoid allocating a new `String`, we reserve the total length of the `SplitString`'s slices,
307                // then append them to the output `Text`.
308                text.0.clear();
309                text.0
310                    .reserve(editable_text.value().into_iter().map(str::len).sum());
311                for sub_str in editable_text.value() {
312                    text.0.push_str(sub_str);
313                }
314            }
315        }
316    }
317}
318
319// Submit the focused input's text when Enter is pressed.
320fn submit_text(
321    mut input_focus: ResMut<InputFocus>,
322    keyboard_input: Res<ButtonInput<Key>>,
323    mut text_input: Query<(&mut EditableText, &TextInputRow)>,
324    mut text_output: Query<(&mut Text, &TextInputRow), With<SubmitOutput>>,
325    tab_navigation: TabNavigation,
326) {
327    if keyboard_input.just_pressed(Key::Enter)
328        && let Some(focused_entity) = input_focus.get()
329        && let Ok((mut editable_text, input_row)) = text_input.get_mut(focused_entity)
330    {
331        for (mut text, output_row) in &mut text_output {
332            if input_row.0 == output_row.0 {
333                text.0.clear();
334                text.0
335                    .reserve(editable_text.value().into_iter().map(str::len).sum());
336                for sub_str in editable_text.value() {
337                    text.0.push_str(sub_str);
338                }
339                break;
340            }
341        }
342        editable_text.clear();
343
344        if let Ok(next) = tab_navigation.navigate(&input_focus, NavAction::Next) {
345            input_focus.set(next, FocusCause::Navigated);
346        }
347    }
348}
349
350/// Dim a row's border colors when its [`EditableText`] does not have input focus.
351fn update_row_border_colors(
352    input_focus: Res<InputFocus>,
353    input_rows: Query<&TextInputRow, With<EditableText>>,
354    mut row_borders: Query<(&TextInputRow, &mut BorderColor, Has<EditableText>)>,
355) {
356    if !input_focus.is_changed() {
357        return;
358    }
359
360    let focused_row = input_focus
361        .get()
362        .and_then(|focused_entity| input_rows.get(focused_entity).ok())
363        .map(|row| row.0);
364
365    for (row, mut border_color, is_input) in &mut row_borders {
366        let mut color = if is_input {
367            SLATE_300.into()
368        } else {
369            Color::WHITE
370        };
371        if Some(row.0) != focused_row {
372            color = color.darker(0.75);
373        }
374        border_color.set_all(color);
375    }
376}