1use 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 .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
175fn 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 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
201fn 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
232fn 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}