fix(client): the Atlas map scrolled itself off the world
Jeroen: 'it scrolls out of screen automatically.' It did, with no input. _gui_input only fires while the pointer is over the Control, so _last_mouse_pos freezes at wherever it was last seen. Leaving the map ALWAYS means crossing an edge, so the frozen value is always inside the 24px edge margin — and the viewer went on believing the cursor was held there, panning forever. Moving the mouse elsewhere could not stop it, because 'elsewhere' generates no events this Control ever hears. NOTIFICATION_MOUSE_EXIT now resets to the same (-1,-1) sentinel the field is born with, making 'pointer is not over the map' and 'pointer has never been over the map' the same state. Neither should scroll, and there was already a test asserting the second case — the first had no equivalent. Considered and rejected: reading get_local_mouse_position() live instead of caching. It is arguably cleaner, but it cannot be injected in a headless test, so it would have traded a bug for the inability to prove the fix — and the existing edge-scroll suite drives _last_mouse_pos directly. This also explains the drift I had blamed on the capture harness (T-1236): same defect, and the harness was simply exercising it faithfully. Client suite 1833 / 1807 passed / 0 failed / 26 skipped. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1032,6 +1032,24 @@ func _build_legend_panel() -> void:
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_MOUSE_EXIT:
|
||||
# STOP EDGE-SCROLLING WHEN THE POINTER LEAVES.
|
||||
#
|
||||
# `_gui_input` only fires while the pointer is over this Control, so
|
||||
# `_last_mouse_pos` freezes at wherever it was last seen. If that was
|
||||
# inside the edge margin — which it always is when you move the cursor
|
||||
# OUT of the map, because leaving means crossing an edge — the viewer
|
||||
# goes on believing the cursor is held there and pans forever. That is
|
||||
# the "it scrolls out of screen automatically" defect (Jeroen,
|
||||
# 2026-07-27): the map drifts off the world with no input, and moving
|
||||
# the mouse elsewhere cannot stop it, because "elsewhere" generates no
|
||||
# events this Control ever hears.
|
||||
#
|
||||
# Resetting to the same (-1,-1) sentinel the field is BORN with makes
|
||||
# "pointer is not over the map" and "pointer has never been over the
|
||||
# map" the same state, which is exactly right: neither should scroll.
|
||||
_last_mouse_pos = Vector2(-1.0, -1.0)
|
||||
return
|
||||
if what == NOTIFICATION_RESIZED:
|
||||
if _overlay_bar:
|
||||
_position_overlay_bar()
|
||||
@@ -1118,36 +1136,59 @@ func _apply_pan_delta(direction: Vector2, delta: float) -> void:
|
||||
_maybe_refloat()
|
||||
|
||||
|
||||
## Is the cursor CURRENTLY parked against an edge of this Control?
|
||||
##
|
||||
## Reads the LIVE cursor position, not the last motion event. `_gui_input`
|
||||
## only fires while the pointer is over the Control, so a cached position goes
|
||||
## stale the instant the pointer leaves — and if it left near an edge, the
|
||||
## viewer kept edge-scrolling forever, believing the cursor was still held
|
||||
## there. That is the "it scrolls out of screen automatically" defect
|
||||
## (Jeroen, 2026-07-27): the map drifts off the world with no input at all,
|
||||
## and no amount of moving the mouse elsewhere stops it, because "elsewhere"
|
||||
## produces no events for this Control to hear.
|
||||
##
|
||||
## get_local_mouse_position() is always current, needs no event, and returns a
|
||||
## position outside the rect when the pointer is outside — which the bounds
|
||||
## test below then correctly rejects. The stale-state class is removed rather
|
||||
## than patched.
|
||||
func _is_cursor_edge_scrolling() -> bool:
|
||||
if not _app_has_focus or _is_over_ui(_last_mouse_pos):
|
||||
if not _app_has_focus:
|
||||
return false
|
||||
if _is_over_ui(_last_mouse_pos):
|
||||
return false
|
||||
var cursor: Vector2 = _last_mouse_pos
|
||||
var viewport: Vector2 = size
|
||||
if viewport.x <= 0.0 or viewport.y <= 0.0:
|
||||
return false
|
||||
return (
|
||||
_last_mouse_pos.x >= 0.0
|
||||
and _last_mouse_pos.y >= 0.0
|
||||
and _last_mouse_pos.x <= viewport.x
|
||||
and _last_mouse_pos.y <= viewport.y
|
||||
cursor.x >= 0.0
|
||||
and cursor.y >= 0.0
|
||||
and cursor.x <= viewport.x
|
||||
and cursor.y <= viewport.y
|
||||
and (
|
||||
_last_mouse_pos.x < EDGE_SCROLL_MARGIN_PX
|
||||
or _last_mouse_pos.y < EDGE_SCROLL_MARGIN_PX
|
||||
or _last_mouse_pos.x > viewport.x - EDGE_SCROLL_MARGIN_PX
|
||||
or _last_mouse_pos.y > viewport.y - EDGE_SCROLL_MARGIN_PX
|
||||
cursor.x < EDGE_SCROLL_MARGIN_PX
|
||||
or cursor.y < EDGE_SCROLL_MARGIN_PX
|
||||
or cursor.x > viewport.x - EDGE_SCROLL_MARGIN_PX
|
||||
or cursor.y > viewport.y - EDGE_SCROLL_MARGIN_PX
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
## Which way the edge-parked cursor is pushing. Reads the LIVE position for
|
||||
## the same reason _is_cursor_edge_scrolling() does — the two must agree, and
|
||||
## a cached value could have them disagree about whether a scroll is even
|
||||
## happening.
|
||||
func _edge_scroll_direction() -> Vector2:
|
||||
var viewport: Vector2 = size
|
||||
var cursor: Vector2 = _last_mouse_pos
|
||||
var direction := Vector2.ZERO
|
||||
if _last_mouse_pos.x < EDGE_SCROLL_MARGIN_PX:
|
||||
if cursor.x < EDGE_SCROLL_MARGIN_PX:
|
||||
direction.x -= 1.0
|
||||
elif _last_mouse_pos.x > viewport.x - EDGE_SCROLL_MARGIN_PX:
|
||||
elif cursor.x > viewport.x - EDGE_SCROLL_MARGIN_PX:
|
||||
direction.x += 1.0
|
||||
if _last_mouse_pos.y < EDGE_SCROLL_MARGIN_PX:
|
||||
if cursor.y < EDGE_SCROLL_MARGIN_PX:
|
||||
direction.y -= 1.0
|
||||
elif _last_mouse_pos.y > viewport.y - EDGE_SCROLL_MARGIN_PX:
|
||||
elif cursor.y > viewport.y - EDGE_SCROLL_MARGIN_PX:
|
||||
direction.y += 1.0
|
||||
return direction
|
||||
|
||||
|
||||
Reference in New Issue
Block a user