use bevy::input::ButtonState;
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input_focus::{AcquireFocus, FocusCause, FocusedInput, InputFocus};
use bevy::prelude::*;
use bevy::text::{EditableText, TextEdit, TextEditChange, TextReadWriteMode};
use bevy::ui::InteractionDisabled;
use bevy::ui_widgets::{TextInput, ValueChange};
#[derive(Component, Clone, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct TextInputValue(pub String);
#[derive(Component, Clone, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct TextHint(pub String);
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
pub struct TextLimit(pub usize);
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct TextEditing;
pub fn is_text_editing(editing: Query<(), With<TextEditing>>) -> bool {
!editing.is_empty()
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct TextInputOf(pub Entity);
#[derive(Component, Clone, Copy, Debug, Default)]
pub(crate) struct AreaField;
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct TextEdited;
pub(crate) fn text_on_edit(
change: On<TextEditChange>,
fields: Query<(&EditableText, &TextInputOf)>,
rows: Query<&TextInputValue>,
mut commands: Commands,
) {
let Ok((text, &TextInputOf(row))) = fields.get(change.event_target()) else {
return;
};
let Ok(held) = rows.get(row) else {
return;
};
let now = text.value().to_string();
if now != held.0 {
commands.entity(row).insert(TextEdited);
commands.trigger(ValueChange {
source: row,
value: now,
is_final: false,
});
}
}
pub(crate) fn text_self_update(
change: On<ValueChange<String>>,
mut rows: Query<&mut TextInputValue>,
) {
if let Ok(mut held) = rows.get_mut(change.source)
&& held.0 != change.value
{
held.0.clone_from(&change.value);
}
}
fn finish(commands: &mut Commands, row: Entity, value: String) {
commands.entity(row).remove::<TextEdited>();
commands.trigger(ValueChange {
source: row,
value,
is_final: true,
});
}
pub(crate) fn text_on_key_input(
input: On<FocusedInput<KeyboardInput>>,
fields: Query<(&EditableText, &TextInputOf, Has<AreaField>), Without<InteractionDisabled>>,
keys: Res<ButtonInput<KeyCode>>,
mut commands: Commands,
) {
if input.input.state != ButtonState::Pressed
|| input.input.repeat
|| !matches!(input.input.key_code, KeyCode::Enter | KeyCode::NumpadEnter)
{
return;
}
let Ok((text, &TextInputOf(row), area)) = fields.get(input.focused_entity) else {
return;
};
let chord = keys.any_pressed([
KeyCode::ControlLeft,
KeyCode::ControlRight,
KeyCode::SuperLeft,
KeyCode::SuperRight,
]);
if (chord || !area) && !text.is_composing() {
finish(&mut commands, row, text.value().to_string());
}
}
type EnabledField = (With<TextInput>, Without<InteractionDisabled>);
pub(crate) fn text_row_on_acquire(
mut acquire: On<AcquireFocus>,
fields: Query<(Entity, &TextInputOf), EnabledField>,
mut focus: ResMut<InputFocus>,
) {
let row = acquire.focused_entity;
let Some((field, _)) = fields.iter().find(|(_, of)| of.0 == row) else {
return;
};
acquire.propagate(false);
if focus.get() != Some(field) {
focus.set(field, FocusCause::Pressed);
}
}
pub fn track_text_editing(
mut focus: ResMut<InputFocus>,
fields: Query<(Entity, &TextInputOf), With<TextInput>>,
editing: Query<(Entity, &TextInputValue, Has<TextEdited>), With<TextEditing>>,
mut commands: Commands,
) {
if !focus.is_changed() {
return;
}
let mut current = focus.get();
if let Some(row) = current
&& let Some((field, _)) = fields.iter().find(|(_, of)| of.0 == row)
{
focus.set(field, FocusCause::Auto);
current = Some(field);
}
let now = current.and_then(|f| fields.get(f).ok()).map(|(_, of)| of.0);
for (row, value, edited) in &editing {
if Some(row) != now {
commands.entity(row).remove::<TextEditing>();
if edited {
finish(&mut commands, row, value.0.clone());
}
}
}
if let Some(row) = now
&& !editing.contains(row)
{
commands.entity(row).insert(TextEditing);
}
}
type Rows = (
Entity,
&'static TextInputValue,
Option<&'static TextHint>,
Option<&'static TextLimit>,
Has<InteractionDisabled>,
);
type Stale = Or<(
Changed<TextInputValue>,
Changed<TextHint>,
Changed<TextLimit>,
Added<InteractionDisabled>,
)>;
pub fn update_text_inputs(
rows: Query<Rows, Stale>,
mut fields: Query<
(
Entity,
&TextInputOf,
&mut EditableText,
Has<InteractionDisabled>,
),
With<TextInput>,
>,
mut hints: Query<(&TextInputOf, &mut Text, &mut Node), Without<TextInput>>,
mut enabled: RemovedComponents<InteractionDisabled>,
mut commands: Commands,
) {
for (row, value, hint, limit, disabled) in &rows {
for (field, of, mut text, field_disabled) in &mut fields {
if of.0 != row {
continue;
}
if text.value().to_string() != value.0 {
text.editor_mut().set_text(&value.0);
text.queue_edit(TextEdit::TextEnd(false));
}
let max = limit.map(|l| l.0);
if text.max_characters != max {
text.max_characters = max;
}
if disabled && !field_disabled {
commands
.entity(field)
.insert((InteractionDisabled, TextReadWriteMode::ReadOnly));
}
}
for (of, mut shown, mut node) in &mut hints {
if of.0 != row {
continue;
}
let words = hint.map_or("", |h| h.0.as_str());
if shown.0 != words {
words.clone_into(&mut shown.0);
}
let display = if value.0.is_empty() && !words.is_empty() {
Display::Flex
} else {
Display::None
};
if node.display != display {
node.display = display;
}
}
}
for row in enabled.read() {
for (field, of, _, field_disabled) in &fields {
if of.0 == row && field_disabled {
commands
.entity(field)
.remove::<InteractionDisabled>()
.insert(TextReadWriteMode::Editable);
}
}
}
}