Skip to content

Instantly share code, notes, and snippets.

@HungryProton
Created March 24, 2026 15:00
Show Gist options
  • Select an option

  • Save HungryProton/d5c2acdd71bcfc3dba17fe040204929f to your computer and use it in GitHub Desktop.

Select an option

Save HungryProton/d5c2acdd71bcfc3dba17fe040204929f to your computer and use it in GitHub Desktop.
Hack to force the controls focus to follow the hover
extends Node
## User Interface Manager
## Add as an autoload
##
## Handle switching between gamepad and M+K
## Automatically gives focus to a hovered button
signal gamepad_changed(enabled: bool)
var _controls: Dictionary = {} # A list of controls that can grab focus
var _last_focused: Control
var _viewport: Viewport
var _gamepad_active: bool = true:
set(val):
if _gamepad_active == val:
return
_gamepad_active = val
if _gamepad_active:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
gamepad_changed.emit(_gamepad_active)
func _enter_tree() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
_viewport = get_tree().get_root().get_viewport()
get_tree().node_added.connect(_on_node_added)
get_tree().node_removed.connect(_on_node_removed)
_viewport.gui_focus_changed.connect(_on_gui_focus_changed)
func _input(event: InputEvent) -> void:
_gamepad_active = event is InputEventJoypadButton or event is InputEventJoypadMotion
# Ensure pressing any UI_ACTION will focus a valid control (if there is any).
# Useful when switching from M+K to gamepad when nothing is focused.
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion or event is InputEventMouseButton:
return
if _viewport.gui_get_focus_owner():
return # A control already have the focus
var ui_action_triggered := false
for action: String in UI_ACTION:
if Input.is_action_just_pressed(action):
ui_action_triggered = true
break
# User pressed one of the ui action
if ui_action_triggered:
# If nothing was focused already, find a visible control on screen
if not is_instance_valid(_last_focused) or not _last_focused.is_visible_in_tree():
for control: Control in _controls.keys():
if control.is_visible_in_tree():
_last_focused = control
break
if not _last_focused:
return
_last_focused.grab_focus()
_viewport.set_input_as_handled()
func is_gamepad_active() -> bool:
return _gamepad_active
func _on_control_hovered(control: Control) -> void:
if control.focus_mode != Control.FOCUS_NONE:
control.grab_focus.call_deferred()
func _on_control_hovered_stopped(control: Control) -> void:
control.release_focus()
func _on_node_added(node: Node) -> void:
if not node is Control:
return
var control := node as Control
if control.focus_mode != Control.FOCUS_NONE:
_controls[node] = true
if control is Button:
var button := node as Button
button.pressed.connect(_on_button_pressed.bind(button))
button.mouse_entered.connect(_on_control_hovered.bind(button))
button.mouse_exited.connect(_on_control_hovered_stopped.bind(button))
func _on_node_removed(node: Node) -> void:
if node is Control:
var _res := _controls.erase(node)
func _on_gui_focus_changed(control: Control) -> void:
if is_instance_valid(control):
_last_focused = control
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment