Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_feathers/src/controls/number_input.rs
30636 views
1
use bevy_app::PropagateOver;
2
use bevy_ecs::{
3
component::Component,
4
entity::Entity,
5
event::EntityEvent,
6
hierarchy::{ChildOf, Children},
7
observer::On,
8
query::With,
9
relationship::Relationship,
10
system::{Commands, Query, Res},
11
};
12
use bevy_input::keyboard::{KeyCode, KeyboardInput};
13
use bevy_input_focus::{FocusLost, FocusedInput, InputFocus};
14
use bevy_log::warn;
15
use bevy_scene::prelude::*;
16
use bevy_text::{
17
EditableText, EditableTextFilter, FontSourceTemplate, FontWeight, TextEdit, TextEditChange,
18
TextFont,
19
};
20
use bevy_ui::{px, widget::Text, AlignItems, AlignSelf, Display, JustifyContent, Node, UiRect};
21
use bevy_ui_widgets::{SelectAllOnFocus, ValueChange};
22
23
use crate::{
24
constants::{fonts, size},
25
controls::{FeathersTextInput, FeathersTextInputContainer},
26
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeTextColor, ThemeToken},
27
tokens,
28
};
29
30
/// Widget that permits text entry of floating-point numbers. This widget implements two-way
31
/// synchronization:
32
/// * when the widget has focus, it emits values (via a [`ValueChange<T>`]) event as the user types.
33
/// The type of ``T`` will be ``f32``, ``f64``, ``i32``, or ``i64`` depending on the
34
/// ``number_format`` parameter.
35
/// * when the widget does not have focus, it listens for [`UpdateNumberInput`] events, and replaces
36
/// the contents of the text buffer based on the value in that event.
37
///
38
/// This is spawnable by inheriting it as a "scene component" with optional [`FeathersNumberInputProps`].
39
///
40
/// To avoid excessive updating, you should only update the number value when there is an actual
41
/// change, that is, when the new value is different from the current value.
42
///
43
/// In most cases, the actual source of truth for the numeric value will be external, that is,
44
/// some property in an app-specific data structure. It's the responsibility of the app to
45
/// synchronize this value with the [`FeathersNumberInput`] widget in both directions:
46
/// * When a [`ValueChange`] event is received, update the app-specific property.
47
/// * When the app-specific property changes - either in response to a [`ValueChange`] event, or
48
/// because of some other action, trigger an [`UpdateNumberInput`] entity event to update the
49
/// displayed value.
50
// TODO: Add text_input field validation when it becomes available.
51
#[derive(SceneComponent, Default, Clone)]
52
#[scene(FeathersNumberInputProps)]
53
pub struct FeathersNumberInput;
54
55
/// Props used to construct a [`FeathersNumberInput`] scene.
56
pub struct FeathersNumberInputProps {
57
/// The "sigil" is a colored strip along the left edge of the input, which is used to
58
/// distinguish between different axes. The default is transparent (no sigil).
59
pub sigil_color: ThemeToken,
60
/// A caption to be placed on the left side of the input, next to the colored stripe.
61
/// Usually one of "X", "Y" or "Z".
62
pub label_text: Option<&'static str>,
63
/// Indicate what size numbers we are editing.
64
pub number_format: NumberFormat,
65
}
66
67
impl Default for FeathersNumberInputProps {
68
fn default() -> Self {
69
Self {
70
sigil_color: tokens::TEXT_INPUT_BG,
71
label_text: None,
72
number_format: NumberFormat::F32,
73
}
74
}
75
}
76
77
impl FeathersNumberInput {
78
fn scene(props: FeathersNumberInputProps) -> impl Scene {
79
bsn! {
80
:@FeathersTextInputContainer
81
ThemeBorderColor({props.sigil_color})
82
FeathersNumberInput
83
template_value(props.number_format)
84
on(number_input_on_update)
85
Children [
86
{
87
match props.label_text {
88
Some(text) => Box::new(bsn_list!(
89
Node {
90
display: Display::Flex,
91
align_items: AlignItems::Center,
92
align_self: AlignSelf::Stretch,
93
justify_content: JustifyContent::Center,
94
padding: UiRect::axes(px(6), px(0)),
95
}
96
ThemeBackgroundColor(tokens::TEXT_INPUT_LABEL_BG)
97
Children [
98
Text(text)
99
TextFont {
100
font: FontSourceTemplate::Handle(fonts::REGULAR),
101
font_size: size::COMPACT_FONT,
102
weight: FontWeight::NORMAL,
103
}
104
PropagateOver<TextFont>
105
ThemeTextColor(tokens::TEXT_INPUT_TEXT)
106
]
107
)) as Box<dyn SceneList>,
108
None => Box::new(bsn_list!()) as Box<dyn SceneList>
109
}
110
}
111
@FeathersTextInput {
112
@max_characters: 20usize,
113
}
114
SelectAllOnFocus
115
on(number_input_on_text_change)
116
on(number_input_on_enter_key)
117
on(number_input_on_focus_loss)
118
EditableTextFilter::new(|c| {
119
c.is_ascii_digit() || matches!(c, '.' | '-' | '+' | 'e' | 'E')
120
}),
121
]
122
}
123
}
124
}
125
126
/// Used to indicate what format of numbers we are editing. This primarily affects the type
127
/// of [`ValueChange`] event that is emitted.
128
#[derive(Component, Default, Clone, Copy)]
129
pub enum NumberFormat {
130
/// A 32-bit float
131
#[default]
132
F32,
133
/// A 64-bit float
134
F64,
135
/// A 32-bit integer
136
I32,
137
/// A 64-bit integer
138
I64,
139
}
140
141
/// Represents numbers in different formats.
142
#[derive(Debug, PartialEq, Clone, Copy)]
143
pub enum NumberInputValue {
144
/// An f32 value
145
F32(f32),
146
/// An f64 value
147
F64(f64),
148
/// An i32 value
149
I32(i32),
150
/// An i64 value
151
I64(i64),
152
}
153
154
impl core::fmt::Display for NumberInputValue {
155
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
156
match self {
157
NumberInputValue::F32(v) => write!(f, "{}", v),
158
NumberInputValue::F64(v) => write!(f, "{}", v),
159
NumberInputValue::I32(v) => write!(f, "{}", v),
160
NumberInputValue::I64(v) => write!(f, "{}", v),
161
}
162
}
163
}
164
165
/// Event which can be sent to the number input widget to update the displayed value.
166
#[derive(Clone, EntityEvent)]
167
pub struct UpdateNumberInput {
168
/// Target widget
169
pub entity: Entity,
170
171
/// Value to change to
172
pub value: NumberInputValue,
173
}
174
175
fn number_input_on_text_change(
176
change: On<TextEditChange>,
177
q_parent: Query<&ChildOf>,
178
q_number_input: Query<&NumberFormat, With<FeathersNumberInput>>,
179
q_text_input: Query<&EditableText>,
180
mut commands: Commands,
181
) {
182
let Ok(parent) = q_parent.get(change.event_target()) else {
183
return;
184
};
185
186
let Ok(number_format) = q_number_input.get(parent.get()) else {
187
return;
188
};
189
190
let Ok(editable_text) = q_text_input.get(change.event_target()) else {
191
return;
192
};
193
194
let text_value = editable_text.value().to_string();
195
emit_value_change(text_value, *number_format, parent.0, &mut commands, false);
196
}
197
198
fn number_input_on_update(
199
update: On<UpdateNumberInput>,
200
q_children: Query<&Children>,
201
q_number_input: Query<(), With<FeathersNumberInput>>,
202
mut q_text_input: Query<&mut EditableText>,
203
focus: Res<InputFocus>,
204
) {
205
if !q_number_input.contains(update.event_target()) {
206
return;
207
};
208
209
let Ok(children) = q_children.get(update.event_target()) else {
210
return;
211
};
212
213
for child_id in children.iter() {
214
if focus.get() != Some(*child_id)
215
&& let Ok(mut editable_text) = q_text_input.get_mut(*child_id)
216
{
217
let new_digits = update.value.to_string();
218
let old_digits = editable_text.value().to_string();
219
if old_digits != new_digits {
220
editable_text.queue_edit(TextEdit::SelectAll);
221
editable_text.queue_edit(TextEdit::Insert(new_digits.into()));
222
}
223
break;
224
}
225
}
226
}
227
228
fn number_input_on_enter_key(
229
key_input: On<FocusedInput<KeyboardInput>>,
230
q_parent: Query<&ChildOf>,
231
q_number_input: Query<&NumberFormat, With<FeathersNumberInput>>,
232
q_text_input: Query<&EditableText>,
233
mut commands: Commands,
234
) {
235
if key_input.input.key_code != KeyCode::Enter {
236
return;
237
}
238
239
let Ok(parent) = q_parent.get(key_input.event_target()) else {
240
return;
241
};
242
243
let Ok(number_format) = q_number_input.get(parent.get()) else {
244
return;
245
};
246
247
let Ok(editable_text) = q_text_input.get(key_input.event_target()) else {
248
return;
249
};
250
251
let text_value = editable_text.value().to_string();
252
emit_value_change(text_value, *number_format, parent.0, &mut commands, true);
253
}
254
255
fn number_input_on_focus_loss(
256
focus_lost: On<FocusLost>,
257
q_parent: Query<&ChildOf>,
258
q_number_input: Query<&NumberFormat, With<FeathersNumberInput>>,
259
mut q_text_input: Query<&mut EditableText>,
260
mut commands: Commands,
261
) {
262
let editable_text_id = focus_lost.event_target();
263
264
let Ok(parent) = q_parent.get(editable_text_id) else {
265
return;
266
};
267
268
let Ok(number_format) = q_number_input.get(parent.get()) else {
269
return;
270
};
271
272
let Ok(editable_text) = q_text_input.get_mut(editable_text_id) else {
273
return;
274
};
275
276
let text_value = editable_text.value().to_string();
277
emit_value_change(text_value, *number_format, parent.0, &mut commands, true);
278
}
279
280
fn emit_value_change(
281
text_value: String,
282
format: NumberFormat,
283
source: Entity,
284
commands: &mut Commands,
285
is_final: bool,
286
) {
287
let text_value = text_value.trim();
288
if text_value.is_empty() {
289
return;
290
}
291
292
match format {
293
NumberFormat::F32 => {
294
match text_value.parse::<f32>() {
295
Ok(new_value) => {
296
commands.trigger(ValueChange {
297
source,
298
value: new_value,
299
is_final,
300
});
301
}
302
Err(_) => {
303
// TODO: Emit a validation error once these are defined
304
warn!("Invalid floating-point number in text edit");
305
}
306
}
307
}
308
NumberFormat::F64 => {
309
match text_value.parse::<f64>() {
310
Ok(new_value) => {
311
commands.trigger(ValueChange {
312
source,
313
value: new_value,
314
is_final,
315
});
316
}
317
Err(_) => {
318
// TODO: Emit a validation error once these are defined
319
warn!("Invalid floating-point number in text edit");
320
}
321
}
322
}
323
NumberFormat::I32 => {
324
match text_value.parse::<i32>() {
325
Ok(new_value) => {
326
commands.trigger(ValueChange {
327
source,
328
value: new_value,
329
is_final,
330
});
331
}
332
Err(_) => {
333
// TODO: Emit a validation error once these are defined
334
warn!("Invalid integer number in text edit");
335
}
336
}
337
}
338
NumberFormat::I64 => {
339
match text_value.parse::<i64>() {
340
Ok(new_value) => {
341
commands.trigger(ValueChange {
342
source,
343
value: new_value,
344
is_final,
345
});
346
}
347
Err(_) => {
348
// TODO: Emit a validation error once these are defined
349
warn!("Invalid integer number in text edit");
350
}
351
}
352
}
353
}
354
}
355
356