diff --git a/IconButton.gd b/IconButton.gd index 5ba2701..c7515d2 100644 --- a/IconButton.gd +++ b/IconButton.gd @@ -110,6 +110,24 @@ func _blocked_by_modal() -> bool: return false +# Fire on the PRESS instead of the release. +# +# A browser on a phone delivers the touch down and then never delivers the +# release. Measured directly on the deployed build: presses arrive with +# correct coordinates, land inside the button, and the button visibly latches +# into its held state - and no release ever follows, in _gui_input or in raw +# _input. A button that only emits on release therefore never emits at all, +# which is exactly the "Play does nothing" report. +# +# The cost is the drag-off-to-cancel affordance below, which is worth much +# less than the button working. Everywhere else keeps it, because everywhere +# else delivers releases. +# +# Settable rather than a constant so a test can exercise both paths without +# needing to be a web build. +@export var fire_on_press: bool = OS.has_feature("web") + + func _gui_input(event: InputEvent) -> void: if _blocked_by_modal(): return @@ -125,6 +143,12 @@ func _handle_press(is_pressed: bool, at_position: Vector2) -> void: if is_pressed: pressed_down = true queue_redraw() + if fire_on_press: + # Nothing is coming to complete this press, so complete it here. + # Not latched: with no release to clear it, the button would + # otherwise stay drawn in its held state forever. + pressed_down = false + pressed.emit() elif pressed_down: pressed_down = false queue_redraw() diff --git a/MainMenu.gd b/MainMenu.gd index fb07dce..c9cd8a3 100644 --- a/MainMenu.gd +++ b/MainMenu.gd @@ -94,12 +94,34 @@ func _on_play_pressed() -> void: _start("res://Main.tscn") -# change_scene_to_file() returns an error and otherwise fails SILENTLY - the -# menu simply stays up, which is indistinguishable from the button not having -# been pressed at all. Reporting it turns "nothing happens" into something a -# player can read back. +# "Nothing happens when I press Play" covers three different faults, and they +# are indistinguishable from the outside: the press never reaching this +# handler, the scene change refusing, or the scene change reporting success +# and not taking effect. Each writes a DIFFERENT line here, so one look at +# the screen says which one it is. +# +# (nothing at all) - the press never got here +# "Loading Main.tscn..." - it got here but the scene never finished +# loading +# "Could not load ..." - the load returned nothing +# "Starting Main.tscn..." - loaded and the change reported OK, yet the +# menu is somehow still up +# "Could not start (...)" - the change refused, with the error number +# +# The scene is also loaded HERE rather than left to change_scene_to_file(), +# which loads internally and reports a single error for both steps. Doing it +# in two makes the failure specific, and takes whatever that call does about +# background loading out of the picture - the web export is built without +# thread support, so anything expecting a loader thread is worth removing +# from the path rather than trusting. func _start(scene_path: String) -> void: - var result: int = get_tree().change_scene_to_file(scene_path) + status_label.text = "Loading %s..." % scene_path.get_file() + var packed: PackedScene = load(scene_path) + if packed == null: + status_label.text = "Could not load %s" % scene_path.get_file() + return + status_label.text = "Starting %s..." % scene_path.get_file() + var result: int = get_tree().change_scene_to_packed(packed) if result != OK: status_label.text = "Could not start (%s, error %d)" % [scene_path.get_file(), result] diff --git a/tests/test_icon_button_press_fires.gd b/tests/test_icon_button_press_fires.gd new file mode 100644 index 0000000..0675d27 --- /dev/null +++ b/tests/test_icon_button_press_fires.gd @@ -0,0 +1,94 @@ +extends SceneTree + +# Headless test: run with +# godot --headless -s res://tests/test_icon_button_press_fires.gd +# A browser on a phone delivers the touch DOWN and never delivers the +# release. Measured on the deployed web build: presses arrived with correct +# coordinates, landed inside the button, the button latched into its held +# state, and no release ever followed. IconButton emits `pressed` only on +# release, so on the web nothing ever fired - the "Play does nothing" bug. +# +# fire_on_press exists for exactly that platform. This test does not care +# whether THIS run is a web build; it exercises both settings directly, which +# is the only way a headless Linux run can cover the web path at all. +# +# Covers: +# A. with fire_on_press, a press alone emits - and emits ONCE even if a +# release does arrive after it +# B. with fire_on_press, the button does not stay latched in its held +# state (nothing is coming to clear it) +# C. without it, the release still drives the press, and dragging off +# still cancels - the behaviour every other platform keeps +# D. the default follows OS.has_feature("web") + +var presses: int = 0 + + +func _initialize() -> void: + var button_script: GDScript = load("res://IconButton.gd") + + # --- D. the default is the platform rule -------------------------- + var default_button: Control = button_script.new() + default_button.size = Vector2(120, 120) + if default_button.fire_on_press != OS.has_feature("web"): + return _fail("fire_on_press defaults to %s, expected OS.has_feature('web') = %s" \ + % [default_button.fire_on_press, OS.has_feature("web")]) + default_button.free() + print("fire_on_press defaults to the web feature flag (%s here)" % OS.has_feature("web")) + + # --- A + B. the web path ------------------------------------------ + var web_button: Control = button_script.new() + web_button.size = Vector2(120, 120) + web_button.fire_on_press = true + web_button.pressed.connect(_count) + root.add_child(web_button) + + presses = 0 + web_button._handle_press(true, Vector2(60, 60)) + if presses != 1: + return _fail("a press alone emitted %d times with fire_on_press, expected 1" % presses) + if web_button.pressed_down: + return _fail("the button latched held with fire_on_press - nothing will ever clear it") + # A release may still arrive on a platform that sends one; it must not + # produce a second press for the same tap. + web_button._handle_press(false, Vector2(60, 60)) + if presses != 1: + return _fail("a release after a fired press emitted again (%d total)" % presses) + print("fire_on_press: one tap emits exactly once and does not latch") + + # --- C. every other platform -------------------------------------- + var native_button: Control = button_script.new() + native_button.size = Vector2(120, 120) + native_button.fire_on_press = false + native_button.pressed.connect(_count) + root.add_child(native_button) + + presses = 0 + native_button._handle_press(true, Vector2(60, 60)) + if presses != 0: + return _fail("a press emitted without a release while fire_on_press was off") + if not native_button.pressed_down: + return _fail("the button did not latch held while waiting for its release") + native_button._handle_press(false, Vector2(60, 60)) + if presses != 1: + return _fail("the release did not emit (%d)" % presses) + + # Dragging off before lifting still cancels. + presses = 0 + native_button._handle_press(true, Vector2(60, 60)) + native_button._handle_press(false, Vector2(400, 400)) + if presses != 0: + return _fail("releasing outside the button still counted as a press") + print("release path unchanged: release fires, dragging off cancels") + + print("TEST_ICON_BUTTON_PRESS_FIRES: PASS") + quit(0) + + +func _count() -> void: + presses += 1 + + +func _fail(message: String) -> void: + print("TEST_ICON_BUTTON_PRESS_FIRES: FAIL (%s)" % message) + quit(1) diff --git a/tests/test_icon_button_press_fires.gd.uid b/tests/test_icon_button_press_fires.gd.uid new file mode 100644 index 0000000..b1647ad --- /dev/null +++ b/tests/test_icon_button_press_fires.gd.uid @@ -0,0 +1 @@ +uid://1icvlueb00ab