Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/text/multiple_text_inputs.rs
30635 views
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
7
use bevy::color::palettes::tailwind::SLATE_300;
8
use bevy::input::keyboard::Key;
9
use bevy::input_focus::tab_navigation::NavAction;
10
use bevy::input_focus::{tab_navigation::TabNavigation, AutoFocus, FocusCause};
11
use bevy::input_focus::{
12
tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
13
InputFocus,
14
};
15
use bevy::prelude::*;
16
use bevy::text::{EditableText, TextCursorStyle};
17
18
fn 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)]
35
struct TextOutput;
36
37
#[derive(Component)]
38
struct SubmitOutput;
39
40
#[derive(Component)]
41
struct TextInputRow(usize);
42
43
fn 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.
177
fn 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.
202
fn 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.
233
fn 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
}
259
260