use bevy::input::ButtonState;
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input_focus::{FocusedInput, InputFocus};
use bevy::picking::events::{PointerCancel, PointerDrag, PointerDragEnd, PointerDragStart};
use bevy::picking::pointer::PointerButton;
use bevy::prelude::*;
use bevy::ui::InteractionDisabled;
use bevy::ui_widgets::ValueChange;
use crate::model::{NumberError, format_number, parse_number, scrub_value, step_value};
use crate::text::{TextEdited, TextHint, TextInputOf, TextInputValue};
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct NumberValue(pub f32);
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct NumberStep(pub f32);
impl Default for NumberStep {
fn default() -> Self {
Self(1.0)
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct NumberRange {
pub min: f32,
pub max: f32,
}
impl Default for NumberRange {
fn default() -> Self {
Self {
min: f32::NEG_INFINITY,
max: f32::INFINITY,
}
}
}
pub const MIXED_HINT: &str = "--";
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct MixedValue;
#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct VecValue(pub Vec<f32>);
#[derive(Component, Clone, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct VecMixed(pub Vec<bool>);
#[derive(EntityEvent, Clone, Debug, PartialEq)]
pub struct VecChange {
#[event_target]
pub entity: Entity,
pub index: usize,
pub value: f32,
pub values: Vec<f32>,
pub is_final: bool,
}
#[derive(EntityEvent, Clone, Debug, PartialEq, Eq)]
pub struct NumberRefused {
#[event_target]
pub entity: Entity,
pub text: String,
pub reason: NumberError,
}
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct NumberInput;
#[derive(Component, Clone, Copy, Debug)]
pub(crate) struct NumberGrab {
start: f32,
moved: bool,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct VecPart {
pub root: Entity,
pub index: usize,
}
#[derive(EntityEvent, Clone, Copy, Debug)]
pub(crate) struct NumberStepBy {
#[event_target]
pub(crate) entity: Entity,
pub(crate) steps: f32,
}
fn text_of(value: f32, step: f32, mixed: bool) -> String {
if mixed {
String::new()
} else {
format_number(value, step)
}
}
pub(crate) fn number_self_update(
change: On<ValueChange<f32>>,
mut rows: Query<&mut NumberValue>,
mut commands: Commands,
) {
if let Ok(mut held) = rows.get_mut(change.source) {
held.set_if_neq(NumberValue(change.value));
commands.entity(change.source).remove::<MixedValue>();
}
}
type Reading = (
&'static NumberValue,
&'static NumberStep,
&'static NumberRange,
);
pub(crate) fn number_on_drag_start(
mut start: On<PointerDragStart>,
rows: Query<&NumberValue, Without<InteractionDisabled>>,
fields: Query<(Entity, &TextInputOf), With<NumberInput>>,
mut focus: ResMut<InputFocus>,
mut commands: Commands,
) {
if start.button != PointerButton::Primary {
return;
}
if let Ok(value) = rows.get(start.entity) {
start.propagate(false);
if let Some(held) = focus.get()
&& fields.get(held).is_ok_and(|(_, of)| of.0 == start.entity)
{
focus.clear();
}
commands.entity(start.entity).insert(NumberGrab {
start: value.0,
moved: false,
});
}
}
pub(crate) fn number_on_drag(
mut drag: On<PointerDrag>,
ui_scale: Res<UiScale>,
keys: Res<ButtonInput<KeyCode>>,
mut grabs: Query<(&mut NumberGrab, Reading)>,
mut commands: Commands,
) {
let Ok((mut grab, (value, step, range))) = grabs.get_mut(drag.entity) else {
return;
};
drag.propagate(false);
let held = |a, b| keys.pressed(a) || keys.pressed(b);
let to = scrub_value(
grab.start,
drag.distance.x / ui_scale.0,
step.0,
range.min,
range.max,
held(KeyCode::ShiftLeft, KeyCode::ShiftRight),
held(KeyCode::ControlLeft, KeyCode::ControlRight),
);
if to != value.0 {
grab.moved = true;
commands.trigger(ValueChange {
source: drag.entity,
value: to,
is_final: false,
});
}
}
pub(crate) fn number_on_drag_end(
mut end: On<PointerDragEnd>,
grabs: Query<(&NumberGrab, &NumberValue)>,
mut commands: Commands,
) {
let Ok((grab, value)) = grabs.get(end.entity) else {
return;
};
end.propagate(false);
if grab.moved {
commands.trigger(ValueChange {
source: end.entity,
value: value.0,
is_final: true,
});
}
commands.entity(end.entity).remove::<NumberGrab>();
}
pub(crate) fn number_on_cancel(
mut cancel: On<PointerCancel>,
grabs: Query<(&NumberGrab, &NumberValue)>,
mut commands: Commands,
) {
let Ok((grab, value)) = grabs.get(cancel.entity) else {
return;
};
cancel.propagate(false);
if grab.moved {
commands.trigger(ValueChange {
source: cancel.entity,
value: value.0,
is_final: true,
});
}
commands.entity(cancel.entity).remove::<NumberGrab>();
}
pub(crate) fn number_on_text(
change: On<ValueChange<String>>,
rows: Query<(
Reading,
Has<MixedValue>,
&TextInputValue,
Has<InteractionDisabled>,
)>,
mut commands: Commands,
) {
if !change.is_final {
return;
}
let row = change.source;
let Ok(((value, step, range), mixed, shown, disabled)) = rows.get(row) else {
return;
};
if disabled || (mixed && change.value.trim().is_empty()) {
return;
}
match parse_number(&change.value) {
Ok(typed) => {
let typed = crate::model::hold(typed, range.min, range.max);
if mixed || typed != value.0 {
commands.trigger(ValueChange {
source: row,
value: typed,
is_final: true,
});
}
let text = format_number(typed, step.0);
if shown.0 != text {
commands.entity(row).insert(TextInputValue(text));
}
}
Err(reason) => {
let text = text_of(value.0, step.0, mixed);
if shown.0 != text {
commands.entity(row).insert(TextInputValue(text));
}
commands.trigger(NumberRefused {
entity: row,
text: change.value.clone(),
reason,
});
}
}
}
pub(crate) fn number_on_key_input(
mut input: On<FocusedInput<KeyboardInput>>,
keys: Res<ButtonInput<KeyCode>>,
fields: Query<&TextInputOf, (With<NumberInput>, Without<InteractionDisabled>)>,
mut rows: Query<(Reading, Has<MixedValue>, &mut TextInputValue)>,
mut commands: Commands,
) {
if input.input.state != ButtonState::Pressed {
return;
}
let Ok(&TextInputOf(row)) = fields.get(input.focused_entity) else {
return;
};
let ten = if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
10.0
} else {
1.0
};
match input.input.key_code {
KeyCode::ArrowUp | KeyCode::ArrowDown => {
input.propagate(false);
let up = input.input.key_code == KeyCode::ArrowUp;
commands.trigger(NumberStepBy {
entity: input.focused_entity,
steps: if up { ten } else { -ten },
});
}
KeyCode::Escape => {
if let Ok(((value, step, _), mixed, mut shown)) = rows.get_mut(row) {
let text = text_of(value.0, step.0, mixed);
if shown.0 != text {
shown.0 = text;
}
commands.entity(row).remove::<TextEdited>();
}
}
_ => {}
}
}
pub(crate) fn number_on_step(
step: On<NumberStepBy>,
fields: Query<&TextInputOf, With<NumberInput>>,
rows: Query<(Reading, Has<MixedValue>), Without<InteractionDisabled>>,
mut commands: Commands,
) {
let Ok(&TextInputOf(row)) = fields.get(step.entity) else {
return;
};
let Ok(((value, by, range), mixed)) = rows.get(row) else {
return;
};
let to = step_value(value.0, step.steps, by.0, range.min, range.max);
if to != value.0 || mixed {
commands.trigger(ValueChange {
source: row,
value: to,
is_final: true,
});
}
}
type Numbers = (
Entity,
Ref<'static, NumberValue>,
Ref<'static, NumberStep>,
Has<MixedValue>,
Option<Ref<'static, MixedValue>>,
&'static mut TextInputValue,
&'static mut TextHint,
);
pub fn update_numbers(
mut rows: Query<Numbers, With<NumberRange>>,
mut unmixed: RemovedComponents<MixedValue>,
) {
let unmixed: Vec<Entity> = unmixed.read().collect();
for (entity, value, step, mixed, mark, mut text, mut hint) in &mut rows {
if !(value.is_changed()
|| step.is_changed()
|| mark.is_some_and(|m| m.is_added())
|| unmixed.contains(&entity))
{
continue;
}
let digits = text_of(value.0, step.0, mixed);
if text.0 != digits {
text.0 = digits;
}
let words = if mixed { MIXED_HINT } else { "" };
if hint.0 != words {
hint.0 = words.to_string();
}
}
}
pub(crate) fn vec_part_on_change(
change: On<ValueChange<f32>>,
parts: Query<&VecPart>,
mut roots: Query<(&mut VecValue, Option<&mut VecMixed>)>,
mut commands: Commands,
) {
let Ok(&VecPart { root, index }) = parts.get(change.source) else {
return;
};
let Ok((mut values, mixed)) = roots.get_mut(root) else {
return;
};
if let Some(slot) = values.0.get_mut(index) {
*slot = change.value;
}
if let Some(mut mixed) = mixed
&& mixed.0.get(index).copied().unwrap_or(false)
{
mixed.0[index] = false;
}
commands.trigger(VecChange {
entity: root,
index,
value: change.value,
values: values.0.clone(),
is_final: change.is_final,
});
}
type Vecs = (
Ref<'static, VecValue>,
Ref<'static, NumberStep>,
Ref<'static, NumberRange>,
Option<Ref<'static, VecMixed>>,
);
type Components = (
Entity,
&'static VecPart,
&'static mut NumberValue,
&'static mut NumberStep,
&'static mut NumberRange,
Has<MixedValue>,
);
pub fn update_vecs(
roots: Query<Vecs, Without<VecPart>>,
mut parts: Query<Components>,
mut commands: Commands,
) {
for (entity, part, mut value, mut step, mut range, mixed) in &mut parts {
let Ok((values, by, limits, marks)) = roots.get(part.root) else {
continue;
};
if !(values.is_changed()
|| by.is_changed()
|| limits.is_changed()
|| marks.as_ref().is_some_and(|m| m.is_changed()))
{
continue;
}
if let Some(&number) = values.0.get(part.index) {
value.set_if_neq(NumberValue(number));
}
step.set_if_neq(*by);
range.set_if_neq(*limits);
let wanted = marks
.as_ref()
.is_some_and(|m| m.0.get(part.index).copied().unwrap_or(false));
if wanted != mixed {
if wanted {
commands.entity(entity).insert(MixedValue);
} else {
commands.entity(entity).remove::<MixedValue>();
}
}
}
}