multiline_text_input/multiline_text_input.rs
1//! Demonstrates a single, minimal multiline [`EditableText`] widget.
2
3use bevy::color::palettes::css::DARK_SLATE_GRAY;
4use bevy::color::palettes::tailwind::SLATE_300;
5use bevy::input::keyboard::{Key, KeyboardInput};
6use bevy::input_focus::tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin};
7use bevy::input_focus::{AutoFocus, FocusedInput};
8use bevy::prelude::*;
9use bevy::text::{EditableText, EditableTextFilter, TextCursorStyle};
10use bevy::ui_widgets::SelectAllOnFocus;
11
12fn main() {
13 App::new()
14 .add_plugins((DefaultPlugins, TabNavigationPlugin))
15 .add_systems(Startup, setup)
16 .run();
17}
18
19#[derive(Component)]
20struct MultilineInput;
21
22#[derive(Component)]
23struct VisibleLinesInput;
24
25#[derive(Component)]
26struct FontSizeInput;
27
28fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
29 commands.spawn(Camera2d);
30
31 commands
32 .spawn(Node {
33 width: percent(100.),
34 height: percent(100.),
35 justify_content: JustifyContent::Center,
36 align_items: AlignItems::Center,
37 ..default()
38 })
39 .with_children(|parent| {
40 parent
41 .spawn((
42 Node {
43 flex_direction: FlexDirection::Column,
44 align_items: AlignItems::End,
45 row_gap: px(10.),
46 ..default()
47 },
48 TabGroup::default(),
49 ))
50 .with_children(|parent| {
51 parent
52 .spawn((
53 Node {
54 width: px(450.),
55 border: px(2.).all(),
56 padding: px(8.).all(),
57 ..default()
58 },
59 EditableText {
60 visible_lines: Some(8.),
61 allow_newlines: true,
62 ..default()
63 },
64 TextLayout {
65 linebreak: LineBreak::WordOrCharacter,
66 ..default()
67 },
68 TextCursorStyle {
69 color: Color::WHITE,
70 selected_text_color: Some(Color::BLACK),
71 ..default()
72 },
73 TextFont {
74 font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
75 font_size: FontSize::Px(30.),
76 ..default()
77 },
78 BackgroundColor(DARK_SLATE_GRAY.into()),
79 BorderColor::all(SLATE_300),
80 MultilineInput,
81 TabIndex(0),
82 AutoFocus,
83 ))
84 .observe(
85 |on: On<FocusedInput<KeyboardInput>>,
86 keys: Res<ButtonInput<Key>>,
87 input_query: Query<&EditableText, With<MultilineInput>>| {
88 if !(on.input.state.is_pressed()
89 && on.input.logical_key == Key::Enter
90 && keys.pressed(Key::Control))
91 {
92 return;
93 }
94 let Ok(input) = input_query.get(on.focused_entity) else {
95 return;
96 };
97
98 let mut output = String::new();
99 output.reserve(input.value().into_iter().map(str::len).sum());
100 for sub_str in input.value() {
101 output.push_str(sub_str);
102 }
103
104 info!("{output}" );
105 },
106 );
107
108 parent
109 .spawn((
110 Node {
111 flex_direction: FlexDirection::Row,
112 column_gap: px(10.),
113 ..default()
114 },
115 children![
116 (
117 Text::new("visible lines:"),
118 TextFont {
119 font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
120 font_size: FontSize::Px(30.),
121 ..default()
122 },
123 ),
124 (
125 Node {
126 width: px(100.),
127 border: px(2.).all(),
128 ..default()
129 },
130 TextFont {
131 font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
132 font_size: FontSize::Px(30.),
133 ..default()
134 },
135 TextLayout {
136 justify: Justify::End,
137 ..default()
138 },
139 BackgroundColor(DARK_SLATE_GRAY.into()),
140 BorderColor::all(SLATE_300),
141 EditableText::new("8"),
142 EditableTextFilter::new(|c| c.is_ascii_digit() || c == '.'),
143 TextCursorStyle {
144 color: Color::WHITE,
145 selected_text_color: Some(Color::BLACK),
146 unfocused_selection_color: Color::NONE,
147 ..default()
148 },
149 SelectAllOnFocus,
150 VisibleLinesInput,
151 TabIndex(1),
152 )
153 ],
154 ))
155 .observe(
156 |on: On<FocusedInput<KeyboardInput>>,
157 mut query_set: ParamSet<(
158 Query<&EditableText, With<VisibleLinesInput>>,
159 Query<&mut EditableText, With<MultilineInput>>,
160 )>| {
161 if !(on.input.state.is_pressed()
162 && on.input.logical_key == Key::Enter)
163 {
164 return;
165 }
166
167 let visible_lines_query = query_set.p0();
168 let Ok(input) = visible_lines_query.get(on.original_event_target())
169 else {
170 return;
171 };
172
173 let mut output = String::new();
174 output.reserve(input.value().into_iter().map(str::len).sum());
175 for sub_str in input.value() {
176 output.push_str(sub_str);
177 }
178
179 let Ok(lines) = output.parse::<f32>() else {
180 return;
181 };
182
183 let mut multiline_query = query_set.p1();
184 let Ok(mut multiline_input) = multiline_query.single_mut() else {
185 return;
186 };
187
188 multiline_input.visible_lines = Some(lines.clamp(1., 10.));
189 },
190 );
191
192 parent
193 .spawn((
194 Node {
195 flex_direction: FlexDirection::Row,
196 column_gap: px(10.),
197 ..default()
198 },
199 children![
200 (
201 Text::new("font size:"),
202 TextFont {
203 font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
204 font_size: FontSize::Px(30.),
205 ..default()
206 },
207 ),
208 (
209 Node {
210 width: px(100.),
211 border: px(2.).all(),
212 ..default()
213 },
214 TextFont {
215 font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
216 font_size: FontSize::Px(30.),
217 ..default()
218 },
219 TextLayout {
220 justify: Justify::End,
221 ..default()
222 },
223 BackgroundColor(DARK_SLATE_GRAY.into()),
224 BorderColor::all(SLATE_300),
225 EditableText::new("30"),
226 EditableTextFilter::new(|c| c.is_ascii_digit()),
227 TextCursorStyle {
228 color: Color::WHITE,
229 selected_text_color: Some(Color::BLACK),
230 unfocused_selection_color: Color::NONE,
231 ..default()
232 },
233 SelectAllOnFocus,
234 FontSizeInput,
235 TabIndex(2),
236 )
237 ],
238 ))
239 .observe(
240 |on: On<FocusedInput<KeyboardInput>>,
241 font_size_input_query: Query<&EditableText, With<FontSizeInput>>,
242 mut multiline_input_font: Single<
243 &mut TextFont,
244 With<MultilineInput>,
245 >| {
246 if !(on.input.state.is_pressed()
247 && on.input.logical_key == Key::Enter)
248 {
249 return;
250 }
251
252 let Ok(input) =
253 font_size_input_query.get(on.original_event_target())
254 else {
255 return;
256 };
257
258 let mut output = String::new();
259 output.reserve(input.value().into_iter().map(str::len).sum());
260 for sub_str in input.value() {
261 output.push_str(sub_str);
262 }
263
264 let Ok(font_size) = output.parse::<f32>() else {
265 return;
266 };
267
268 multiline_input_font.font_size =
269 FontSize::Px(font_size.clamp(5., 50.));
270 },
271 );
272 });
273 });
274}