Got it! Drawing inspiration from your suggestions:
extends Node
func _ready():
$Viewport0/Control/Button.grab_focus()
$Viewport1/Control/Button.grab_focus()
func _input(event):
if event is InputEventKey or event is InputEventMouse:
# Keyboard/mouse
$Viewport0.input(event)
elif event.device == 0:
# Controller 0
$Viewport1.input(event)
This won't work in a stock build because Godot only allows 1 focused UI element in the tree. But that was easy to patch.
diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp
index 7f5dd3930..df072aa4f 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -2473,7 +2473,7 @@ void Viewport::_gui_control_grab_focus(Control *p_control) {
//no need for change
if (gui.key_focus && gui.key_focus == p_control)
return;
- get_tree()->call_group_flags(SceneTree::GROUP_CALL_REALTIME, "_viewports", "_gui_remove_focus");
+ _gui_remove_focus();
gui.key_focus = p_control;
p_control->notification(Control::NOTIFICATION_FOCUS_ENTER);
p_control->update();