use super::{
can_insert_char, command_modifier_pressed, commit_numeric_field, control_pressed,
set_checkable_state, step_number_field, sync_display_change, word_modifier_pressed,
};
use crate::input::{
DisabledInput, InputClipboard, InputField, InputSubmitMessage, InputTextEngine,
InputValueChangedMessage, active_input_entity, key_is_submit, push_value_changed,
};
use crate::text::FontResource;
use bevy::input::ButtonState;
use bevy::input::{
ButtonInput,
keyboard::{Key, KeyCode, KeyboardInput},
};
use bevy::input_focus::InputFocus;
use bevy::prelude::*;
use bevy::text::ComputedTextBlock;
fn filter_pasted_text(field: &InputField, text: &str) -> String {
let is_multiline = field.is_multiline();
text.chars()
.filter(|chr| {
if *chr == '\n' {
return is_multiline;
}
can_insert_char(field, *chr)
})
.collect()
}
pub(crate) fn handle_keyboard_input(
mut commands: Commands,
mut keyboard_inputs: MessageReader<KeyboardInput>,
fields_marker: Query<(), With<InputField>>,
mut fields: Query<(Entity, &mut InputField, Has<DisabledInput>)>,
text_nodes: Query<&ComputedTextBlock, With<crate::input::InputText>>,
viewports: Query<&ComputedNode, With<crate::input::InputViewport>>,
input_focus: Res<InputFocus>,
keys: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
font_resource: Res<FontResource>,
mut value_changed: MessageWriter<InputValueChangedMessage>,
mut input_submit: MessageWriter<InputSubmitMessage>,
mut clipboard: NonSendMut<InputClipboard>,
text_engine: Res<InputTextEngine>,
) {
let Some(active) = active_input_entity(&input_focus, &fields_marker) else {
return;
};
let Ok((entity, mut field, disabled)) = fields.get_mut(active) else {
return;
};
if disabled {
return;
}
let mut display_changed = false;
let mut pending_value_message = false;
let extend_selection = super::shift_pressed(&keys);
let word_modifier = word_modifier_pressed(&keys);
let command_modifier = command_modifier_pressed(&keys);
let control_modifier = control_pressed(&keys);
for keyboard_input in keyboard_inputs.read() {
if keyboard_input.state != ButtonState::Pressed {
continue;
}
match (&keyboard_input.logical_key, &keyboard_input.text) {
(Key::Character(key), _) if command_modifier && key.eq_ignore_ascii_case("a") => {
display_changed |= field.edit_state.select_all();
}
(Key::Character(key), _) if command_modifier && key.eq_ignore_ascii_case("c") => {
if let Some(range) = field.edit_state.selection_range() {
let selected = field.edit_state.committed()[range].to_string();
clipboard.set_text(&selected);
}
}
(Key::Character(key), _) if command_modifier && key.eq_ignore_ascii_case("x") => {
if let Some(range) = field.edit_state.selection_range() {
let selected = field.edit_state.committed()[range].to_string();
clipboard.set_text(&selected);
let current = field.value().to_string();
field.undo_stack.record(¤t);
field.edit_state.backspace();
pending_value_message = true;
display_changed = true;
}
}
(Key::Character(key), _) if command_modifier && key.eq_ignore_ascii_case("v") => {
if let Some(text) = clipboard.get_text() {
let filtered = filter_pasted_text(&field, &text);
if !filtered.is_empty() {
let current = field.value().to_string();
field.undo_stack.record(¤t);
field.edit_state.insert_text(&filtered);
pending_value_message = true;
display_changed = true;
}
}
}
(Key::Character(key), _) if command_modifier && key.eq_ignore_ascii_case("z") => {
let shift = super::shift_pressed(&keys);
let current = field.value().to_string();
if shift {
if let Some(prev) = field.undo_stack.redo(¤t) {
field.edit_state.set_text(prev);
pending_value_message = true;
display_changed = true;
}
} else if let Some(prev) = field.undo_stack.undo(¤t) {
field.edit_state.set_text(prev);
pending_value_message = true;
display_changed = true;
}
}
(Key::Backspace, _) => {
let current = field.value().to_string();
field.undo_stack.record(¤t);
let edited = if word_modifier {
field.edit_state.backspace_word()
} else {
field.edit_state.backspace()
};
pending_value_message |= edited;
display_changed |= edited;
}
(Key::Delete, _) => {
let current = field.value().to_string();
field.undo_stack.record(¤t);
let edited = if word_modifier {
field.edit_state.delete_word_forward()
} else {
field.edit_state.delete_forward()
};
pending_value_message |= edited;
display_changed |= edited;
}
(Key::ArrowLeft, _) => {
display_changed |= if word_modifier {
field.edit_state.move_word_left(extend_selection)
} else {
field.edit_state.move_left(extend_selection)
};
}
(Key::ArrowRight, _) => {
display_changed |= if word_modifier {
field.edit_state.move_word_right(extend_selection)
} else {
field.edit_state.move_right(extend_selection)
};
}
(Key::Home, _) => {
display_changed |= field.edit_state.move_home(extend_selection);
}
(Key::End, _) => {
display_changed |= field.edit_state.move_end(extend_selection);
}
(Key::ArrowUp, _) => {
if field.is_multiline() {
if let (Some(viewport_entity), Some(text_entity)) =
(field.viewport_entity, field.text_entity)
&& let (Ok(block), Ok(viewport)) =
(text_nodes.get(text_entity), viewports.get(viewport_entity))
&& let Some((byte, preferred_x)) = text_engine.move_byte_vertically(
block,
viewport.inverse_scale_factor(),
field.edit_state.display_caret_byte(),
field.preferred_caret_x,
-1,
)
{
field.edit_state.set_caret(byte, extend_selection);
field.preferred_caret_x = Some(preferred_x);
display_changed = true;
}
} else {
let edited = step_number_field(&mut field, 1.0);
pending_value_message |= edited;
display_changed |= edited;
}
}
(Key::ArrowDown, _) => {
if field.is_multiline() {
if let (Some(viewport_entity), Some(text_entity)) =
(field.viewport_entity, field.text_entity)
&& let (Ok(block), Ok(viewport)) =
(text_nodes.get(text_entity), viewports.get(viewport_entity))
&& let Some((byte, preferred_x)) = text_engine.move_byte_vertically(
block,
viewport.inverse_scale_factor(),
field.edit_state.display_caret_byte(),
field.preferred_caret_x,
1,
)
{
field.edit_state.set_caret(byte, extend_selection);
field.preferred_caret_x = Some(preferred_x);
display_changed = true;
}
} else {
let edited = step_number_field(&mut field, -1.0);
pending_value_message |= edited;
display_changed |= edited;
}
}
(key, _) if key_is_submit(key) => {
if field.input_type == crate::input::InputType::Checkbox {
let next_checked = !field.checked;
let changed = set_checkable_state(
entity,
&mut field,
next_checked,
&mut value_changed,
);
pending_value_message |= changed;
display_changed |= changed;
} else if field.input_type == crate::input::InputType::Radio {
let changed =
set_checkable_state(entity, &mut field, true, &mut value_changed);
pending_value_message |= changed;
display_changed |= changed;
} else if field.is_multiline() {
let current = field.value().to_string();
field.undo_stack.record(¤t);
let edited = field.edit_state.insert_text("\n");
pending_value_message |= edited;
display_changed |= edited;
} else {
let committed = commit_numeric_field(entity, &mut field, &mut value_changed);
display_changed |= committed;
if committed {
pending_value_message = false;
}
input_submit.write(InputSubmitMessage {
entity,
name: field.name.clone(),
});
}
}
(_, Some(inserted_text)) if !control_modifier && !command_modifier => {
let filtered: String = inserted_text
.chars()
.filter(|chr| can_insert_char(&field, *chr))
.collect();
if !filtered.is_empty() {
let current = field.value().to_string();
field.undo_stack.record(¤t);
let edited = field.edit_state.insert_text(&filtered);
pending_value_message |= edited;
display_changed |= edited;
}
}
_ => {}
}
}
if display_changed && !field.is_checkable() {
sync_display_change(&mut commands, &font_resource, &mut field, disabled, &time);
}
if pending_value_message && !field.is_checkable() {
push_value_changed(&mut value_changed, entity, &field);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{InputType, TextEditState, UndoStack};
fn field(input_type: InputType, value: &str) -> InputField {
InputField {
name: "input".to_string(),
input_type,
checked: false,
placeholder: String::new(),
viewport_entity: None,
text_entity: None,
selection_entity: None,
caret_entity: None,
edit_state: TextEditState::with_text(value),
initial_value: value.to_string(),
initial_checked: false,
min: None,
max: None,
step: None,
caret_blink_resume_at: 0.0,
preferred_caret_x: None,
undo_stack: UndoStack::default(),
}
}
#[test]
fn paste_preserves_newlines_for_textarea() {
let field = field(InputType::Textarea, "");
assert_eq!(filter_pasted_text(&field, "a\nb"), "a\nb");
}
#[test]
fn paste_removes_newlines_for_single_line_input() {
let field = field(InputType::Text, "");
assert_eq!(filter_pasted_text(&field, "a\nb"), "ab");
}
}