1use 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 .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
292fn 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 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
319fn 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
350fn 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}