Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 104 additions & 57 deletions crates/bevy_feathers/src/controls/number_input.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::str::FromStr;

use bevy_app::PropagateOver;
use bevy_ecs::{
component::Component,
Expand Down Expand Up @@ -66,6 +68,8 @@ pub struct FeathersNumberInputProps {
pub label_text: Option<&'static str>,
/// Indicate what size numbers we are editing.
pub number_format: NumberFormat,
/// Whether to emit a value change when the [`EditableText`] is empty
pub emit_value_change_on_empty: bool,
}

impl Default for FeathersNumberInputProps {
Expand All @@ -74,6 +78,7 @@ impl Default for FeathersNumberInputProps {
sigil_color: tokens::TEXT_INPUT_BG,
label_text: None,
number_format: NumberFormat::F32,
emit_value_change_on_empty: false,
}
}
}
Expand All @@ -85,6 +90,7 @@ impl FeathersNumberInput {
ThemeBorderColor({props.sigil_color})
FeathersNumberInput
template_value(props.number_format)
template_value(EmitValueChangeOnEmpty(props.emit_value_change_on_empty))
on(number_input_on_update)
Children [
{
Expand Down Expand Up @@ -142,6 +148,11 @@ pub enum NumberFormat {
I64,
}

/// Decides whether to emit a [`ValueChange`] event on empty text
#[derive(Component, Default, Clone, Copy, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct EmitValueChangeOnEmpty(pub bool);

/// Represents numbers in different formats.
#[derive(Debug, PartialEq, Clone, Copy, Reflect)]
pub enum NumberInputValue {
Expand All @@ -153,6 +164,8 @@ pub enum NumberInputValue {
I32(i32),
/// An `i64` value
I64(i64),
/// An empty value
Empty,
}

impl core::fmt::Display for NumberInputValue {
Expand All @@ -162,6 +175,7 @@ impl core::fmt::Display for NumberInputValue {
NumberInputValue::F64(v) => write!(f, "{}", v),
NumberInputValue::I32(v) => write!(f, "{}", v),
NumberInputValue::I64(v) => write!(f, "{}", v),
NumberInputValue::Empty => write!(f, ""),
}
}
}
Expand All @@ -180,15 +194,15 @@ pub struct UpdateNumberInput {
fn number_input_on_text_change(
change: On<TextEditChange>,
q_parent: Query<&ChildOf>,
q_number_input: Query<&NumberFormat, With<FeathersNumberInput>>,
q_number_input: Query<(&NumberFormat, &EmitValueChangeOnEmpty), With<FeathersNumberInput>>,
q_text_input: Query<&EditableText>,
mut commands: Commands,
) {
let Ok(parent) = q_parent.get(change.event_target()) else {
return;
};

let Ok(number_format) = q_number_input.get(parent.get()) else {
let Ok((number_format, emit_value_change_on_empty)) = q_number_input.get(parent.get()) else {
return;
};

Expand All @@ -197,7 +211,14 @@ fn number_input_on_text_change(
};

let text_value = editable_text.value().to_string();
emit_value_change(text_value, *number_format, parent.0, &mut commands, false);
emit_value_change(
text_value,
*number_format,
emit_value_change_on_empty.0,
parent.0,
&mut commands,
false,
);
}

fn number_input_on_update(
Expand Down Expand Up @@ -233,7 +254,7 @@ fn number_input_on_update(
fn number_input_on_enter_key(
key_input: On<FocusedInput<KeyboardInput>>,
q_parent: Query<&ChildOf>,
q_number_input: Query<&NumberFormat, With<FeathersNumberInput>>,
q_number_input: Query<(&NumberFormat, &EmitValueChangeOnEmpty), With<FeathersNumberInput>>,
q_text_input: Query<&EditableText>,
mut commands: Commands,
) {
Expand All @@ -245,7 +266,7 @@ fn number_input_on_enter_key(
return;
};

let Ok(number_format) = q_number_input.get(parent.get()) else {
let Ok((number_format, emit_value_change_on_empty)) = q_number_input.get(parent.get()) else {
return;
};

Expand All @@ -254,13 +275,20 @@ fn number_input_on_enter_key(
};

let text_value = editable_text.value().to_string();
emit_value_change(text_value, *number_format, parent.0, &mut commands, true);
emit_value_change(
text_value,
*number_format,
emit_value_change_on_empty.0,
parent.0,
&mut commands,
true,
);
}

fn number_input_on_focus_loss(
focus_lost: On<FocusLost>,
q_parent: Query<&ChildOf>,
q_number_input: Query<&NumberFormat, With<FeathersNumberInput>>,
q_number_input: Query<(&NumberFormat, &EmitValueChangeOnEmpty), With<FeathersNumberInput>>,
mut q_text_input: Query<&mut EditableText>,
mut commands: Commands,
) {
Expand All @@ -270,7 +298,7 @@ fn number_input_on_focus_loss(
return;
};

let Ok(number_format) = q_number_input.get(parent.get()) else {
let Ok((number_format, emit_value_change_on_empty)) = q_number_input.get(parent.get()) else {
return;
};

Expand All @@ -279,80 +307,99 @@ fn number_input_on_focus_loss(
};

let text_value = editable_text.value().to_string();
emit_value_change(text_value, *number_format, parent.0, &mut commands, true);
emit_value_change(
text_value,
*number_format,
emit_value_change_on_empty.0,
parent.0,
&mut commands,
true,
);
}

fn emit_value_change(
text_value: String,
format: NumberFormat,
allow_empty: bool,
source: Entity,
commands: &mut Commands,
is_final: bool,
) {
let text_value = text_value.trim();
if text_value.is_empty() {
if !allow_empty && text_value.is_empty() {
return;
}

match format {
NumberFormat::F32 => {
match text_value.parse::<f32>() {
Ok(new_value) => {
commands.trigger(ValueChange {
source,
value: new_value,
is_final,
});
}
Err(_) => {
// TODO: Emit a validation error once these are defined
warn!("Invalid floating-point number in text edit");
}
}
}
NumberFormat::F64 => {
match text_value.parse::<f64>() {
Ok(new_value) => {
NumberFormat::F32 => emit_value_change_with_type::<f32>(
text_value,
source,
is_final,
allow_empty,
commands,
"floating-point",
),
NumberFormat::F64 => emit_value_change_with_type::<f64>(
text_value,
source,
is_final,
allow_empty,
commands,
"floating-point",
),
NumberFormat::I32 => emit_value_change_with_type::<i32>(
text_value,
source,
is_final,
allow_empty,
commands,
"integer",
),
NumberFormat::I64 => emit_value_change_with_type::<i64>(
text_value,
source,
is_final,
allow_empty,
commands,
"integer",
),
}
}

fn emit_value_change_with_type<T: 'static + Send + Sync + FromStr>(
text_value: &str,
source: Entity,
is_final: bool,
allow_empty: bool,
commands: &mut Commands,
number_type_str: &str,
) {
if allow_empty && text_value.is_empty() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would move the logic of

if !allow_empty && text_value.is_empty() {
        return;
    }

from emit_value_change into this function block

Otherwise, it appears that this case isn’t explicitly handled in this fn as is. The else block doesn’t appear to handle that case correctly (since it’s assuming the logic happened outside the function), so it can cause a moment of confusion.

commands.trigger(ValueChange::<Option<T>> {
source,
value: None,
is_final,
});
} else {
match text_value.parse::<T>() {
Ok(new_value) => {
if allow_empty {
commands.trigger(ValueChange {
source,
value: new_value,
value: Some(new_value),
is_final,
});
}
Err(_) => {
// TODO: Emit a validation error once these are defined
warn!("Invalid floating-point number in text edit");
}
}
}
NumberFormat::I32 => {
match text_value.parse::<i32>() {
Ok(new_value) => {
} else {
commands.trigger(ValueChange {
source,
value: new_value,
is_final,
});
}
Err(_) => {
// TODO: Emit a validation error once these are defined
warn!("Invalid integer number in text edit");
}
}
}
NumberFormat::I64 => {
match text_value.parse::<i64>() {
Ok(new_value) => {
commands.trigger(ValueChange {
source,
value: new_value,
is_final,
});
}
Err(_) => {
// TODO: Emit a validation error once these are defined
warn!("Invalid integer number in text edit");
}
Err(_) => {
// TODO: Emit a validation error once these are defined
warn!("Invalid {number_type_str} number in text edit");
}
}
}
Expand Down
27 changes: 18 additions & 9 deletions examples/ui/widgets/feathers_gallery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use bevy::{
struct DemoWidgetStates {
rgb_color: Srgba,
hsl_color: Hsla,
scalar_prop: f32,
scalar_prop: Option<f32>,
vec3_prop: Vec3,
}

Expand Down Expand Up @@ -68,7 +68,7 @@ fn main() {
.insert_resource(DemoWidgetStates {
rgb_color: palettes::tailwind::EMERALD_800.with_alpha(0.7),
hsl_color: palettes::tailwind::AMBER_800.into(),
scalar_prop: 7.0,
scalar_prop: Some(7.0),
vec3_prop: Vec3::new(10.1, 7.124, 100.0),
})
.add_systems(Startup, scene.spawn())
Expand Down Expand Up @@ -625,14 +625,16 @@ fn demo_column_2() -> impl Scene {
label("A standard group"),
label_small("Scalar property"),
(
@FeathersNumberInput
@FeathersNumberInput {
@emit_value_change_on_empty: true,
}
DemoScalarField
Node {
flex_grow: 1.0,
max_width: px(100),
}
on(
|value_change: On<ValueChange<f32>>,
|value_change: On<ValueChange<Option<f32>>>,
mut states: ResMut<DemoWidgetStates>| {
if value_change.is_final {
states.scalar_prop = value_change.value;
Expand All @@ -648,7 +650,7 @@ fn demo_column_2() -> impl Scene {
max_width: px(100),
}
on(
|value_change: On<ValueChange<f32>>,
|value_change: On<ValueChange<Option<f32>>>,
mut states: ResMut<DemoWidgetStates>| {
if value_change.is_final {
states.scalar_prop = value_change.value;
Expand Down Expand Up @@ -837,10 +839,17 @@ fn update_colors(
}

for scalar_input_ent in q_scalar_input.iter() {
commands.trigger(UpdateNumberInput {
entity: scalar_input_ent,
value: NumberInputValue::F32(states.scalar_prop),
});
if let Some(v) = states.scalar_prop {
commands.trigger(UpdateNumberInput {
entity: scalar_input_ent,
value: NumberInputValue::F32(v),
});
} else {
commands.trigger(UpdateNumberInput {
entity: scalar_input_ent,
value: NumberInputValue::Empty,
});
}
}

for (vec3_input_ent, axis) in q_vec3_input.iter() {
Expand Down