#93: drive the research menu with a mouse, and close the hole under it

Two separate faults, both reported on the same ticket.

The menu was inert under a mouse. Input reaches the tree as raw events
through BuildController._unhandled_input, because a Control that consumes
them through the GUI system swallows the drag, and the tree only ever read
touch events. That was invisible while the project set
emulate_touch_from_mouse and every click arrived as a synthetic touch; with
that setting gone, nothing selects and nothing scrolls, and there is no
other way to research anything on a desktop. The press and the pan are now
the behaviour, with touch and mouse as two ways in to them. Motion pans only
while the button is held - a hovering pointer must not drag the tree out
from under the reader - and the wheel scrolls, which is the first thing a
desktop player reaches for.

The panel also still had the buy button's hole in it. That button went away
in #83 and nothing moved down into the space, leaving ~180px of dead panel
below the readout while the tree above it stayed cramped. The readout moves
down 154px and the canvas takes the height: 1190px to 1344px.

The test drives clicks through BuildController rather than into the view,
since the missing piece was the routing - calling the view directly would
have passed against the bug as reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-22 23:29:34 +02:00
commit 4883197ef3
5 changed files with 242 additions and 11 deletions

View file

@ -13,6 +13,14 @@ extends Control
# before the menu can act on them, which is the bug that made the formation
# editor's drag silently do nothing. Everything in this menu must stay
# mouse_filter = IGNORE.
#
# #93: a mouse is a first-class way to drive this menu, not something
# emulation turns into touches. The project used to set
# emulate_touch_from_mouse, which made every click arrive here as an
# InputEventScreenTouch for free; with that off, a menu that only reads
# touch events is simply dead under a mouse - which is what the report was.
# So the press and the pan are the behaviour, and touch and mouse are two
# ways in to them.
signal node_picked(id: String)
@ -29,6 +37,9 @@ const LOCKED_PULSE: float = 0.5
@export var node_radius: float = 46.0
@export var row_spacing: float = 250.0
# One wheel notch: about a third of the default row spacing, so a notch is a
# clear step without skipping a whole depth of the tree.
const WHEEL_STEP: float = 90.0
# #83: a second tap on the node already selected buys it. The separate buy
# button is gone - it sat at the bottom of the panel, a long way from the node
@ -93,13 +104,43 @@ func _state_color(node: Dictionary) -> Color:
# Returns true when the press landed on a node, so the caller knows the
# touch was consumed.
func handle_touch(event: InputEventScreenTouch) -> bool:
return handle_press(event.pressed, event.position)
func handle_drag(event: InputEventScreenDrag) -> bool:
return handle_pan(event.relative)
# A left click is the same gesture as a tap. Other buttons are left alone so
# they stay available for whatever else wants them.
func handle_mouse_button(event: InputEventMouseButton) -> bool:
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
return handle_pan(Vector2(0.0, WHEEL_STEP))
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
return handle_pan(Vector2(0.0, -WHEEL_STEP))
if event.button_index != MOUSE_BUTTON_LEFT:
return false
return handle_press(event.pressed, event.position)
# Only while the button is held. A touch drag cannot arrive without a finger
# on the glass, but the mouse moves across the menu the whole time it is
# open, and panning the tree under a hovering pointer would make the menu
# impossible to read.
func handle_mouse_motion(event: InputEventMouseMotion) -> bool:
if not _dragging:
return false
return handle_pan(event.relative)
func handle_press(pressed: bool, at: Vector2) -> bool:
if not visible:
return false
if not event.pressed:
if not pressed:
_dragging = false
return true
_dragging = true
var local: Vector2 = event.position - global_position
var local: Vector2 = at - global_position
for node in GameState.SKILL_TREE:
if local.distance_to(node_screen_pos(node)) <= node_radius * 1.25:
var confirming: bool = selected_id == node.id
@ -112,12 +153,12 @@ func handle_touch(event: InputEventScreenTouch) -> bool:
return false
func handle_drag(event: InputEventScreenDrag) -> bool:
func handle_pan(relative: Vector2) -> bool:
if not visible:
return false
# Dragging a finger upward reveals higher research; dragging downward
# returns to the root. Clamp hard so the canvas never leaves an empty gap.
scroll_y = clampf(scroll_y - event.relative.y, 0.0, _max_scroll())
scroll_y = clampf(scroll_y - relative.y, 0.0, _max_scroll())
queue_redraw()
return true