Keep arrow keys in active text fields (#132)

This commit is contained in:
Alexander Sellite 2026-09-02 09:51:58 -04:00
parent 26b5b5d62f
commit bfbcba7109
2 changed files with 70 additions and 0 deletions

View file

@ -198,6 +198,40 @@ func _run() -> void:
standard_button.get_theme_stylebox("hover") == old_hover_style,
"mouse hover styling was not restored after controller use",
)
var text_entry := LineEdit.new()
text_entry.position = Vector2(520.0, 60.0)
text_entry.size = Vector2(220.0, 48.0)
text_entry.text = "editable text"
stage.add_child(text_entry)
standard_button.grab_focus()
await process_frame
presentation.call("_neutralize_current_navigation_focus")
text_entry.grab_focus()
await process_frame
_expect(
presentation.get("_neutral_focus_seed") == null,
"entering a text field leaves the prior menu focus seed armed",
)
for keycode: Key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN]:
var arrow_key := InputEventKey.new()
arrow_key.keycode = keycode
arrow_key.physical_keycode = keycode
arrow_key.pressed = true
presentation._input(arrow_key)
_expect(
root.gui_get_focus_owner() == text_entry,
"arrow key %s pulled focus out of an active text field" % keycode,
)
var modified_arrow := InputEventKey.new()
modified_arrow.keycode = KEY_LEFT
modified_arrow.physical_keycode = KEY_LEFT
modified_arrow.shift_pressed = true
modified_arrow.pressed = true
presentation._input(modified_arrow)
_expect(
root.gui_get_focus_owner() == text_entry,
"a modified arrow pulled focus out of an active text field",
)
var item_list := ItemList.new()
item_list.position = Vector2(80.0, 160.0)
item_list.size = Vector2(240.0, 150.0)

View file

@ -86,6 +86,15 @@ func _input(event: InputEvent) -> void:
event is InputEventJoypadButton or event is InputEventJoypadMotion
):
return
# Physical keyboard arrows belong to an active text editor. A mouse click can
# leave the previously focused menu button cached as the neutral controller
# seed; restoring that seed here would pull focus out of the editor before
# LineEdit/TextEdit can move its caret or selection.
if _keyboard_direction_belongs_to_text_entry(event):
_navigation_focus_active = false
_neutral_focus_seed = null
_set_controller_active(false)
return
var directional_navigation: bool = _is_directional_navigation(event)
if directional_navigation:
_directional_input_generation += 1
@ -217,6 +226,11 @@ func _on_focus_changed(control: Control) -> void:
if _directional_input_in_flight and _is_navigation_focus(control):
_navigation_focus_active = true
_neutral_focus_seed = null
elif not _is_navigation_focus(control):
# Text entry supersedes any button remembered before the field was clicked.
# Keeping that seed would make the next keyboard arrow restore the button.
_navigation_focus_active = false
_neutral_focus_seed = null
elif not _navigation_focus_active and _is_navigation_focus(control):
_capture_neutral_focus_seed(control)
return
@ -261,6 +275,28 @@ func _is_directional_navigation(event: InputEvent) -> bool:
)
func _keyboard_direction_belongs_to_text_entry(event: InputEvent) -> bool:
var key_event := event as InputEventKey
if key_event == null or not key_event.pressed:
return false
var focus_owner: Control = _active_focus_owner()
if not (focus_owner is LineEdit or focus_owner is TextEdit):
return false
# Check the keycodes directly as well as the built-in actions so modified
# editing gestures such as Shift+Left and Ctrl+Right cannot be mistaken for
# menu navigation when action matching requires exact modifier parity.
if key_event.keycode in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN]:
return true
if key_event.physical_keycode in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN]:
return true
return (
event.is_action_pressed(&"ui_left")
or event.is_action_pressed(&"ui_right")
or event.is_action_pressed(&"ui_up")
or event.is_action_pressed(&"ui_down")
)
func _is_navigation_focus(control: Control) -> bool:
# Text entry is an explicit interaction rather than menu navigation. It must
# retain direct focus for typing, including Chat and dialog fields.