Simplify on_enter/on_exit callbacks to position-only signature

BREAKING CHANGE: Hover callbacks now take only (pos) instead of (pos, button, action)

- Add PyHoverCallable class for on_enter/on_exit/on_move callbacks (position-only)
- Add PyCellHoverCallable class for on_cell_enter/on_cell_exit callbacks
- Change UIDrawable member types from PyClickCallable to PyHoverCallable
- Update PyScene::do_mouse_hover() to call hover callbacks with only position
- Add tryCallPythonMethod overload for position-only subclass method calls
- Update UIGrid::fireCellEnter/fireCellExit to use position-only signature
- Update all tests for new callback signatures

New callback signatures:
| Callback       | Old                      | New        |
|----------------|--------------------------|------------|
| on_enter       | (pos, button, action)    | (pos)      |
| on_exit        | (pos, button, action)    | (pos)      |
| on_move        | (pos, button, action)    | (pos)      |
| on_cell_enter  | (cell_pos, button, action)| (cell_pos)|
| on_cell_exit   | (cell_pos, button, action)| (cell_pos)|
| on_click       | unchanged                | unchanged  |
| on_cell_click  | unchanged                | unchanged  |

closes #230

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-01-28 17:36:02 -05:00
commit 2daebc84b5
12 changed files with 598 additions and 71 deletions

View file

@ -61,13 +61,14 @@ try:
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
self.cell_events.append(('click', cell_pos.x, cell_pos.y, button, action))
def on_cell_enter(self, cell_pos, button, action):
# #230 - Cell hover callbacks now only receive (cell_pos)
def on_cell_enter(self, cell_pos):
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
self.cell_events.append(('enter', cell_pos.x, cell_pos.y, button, action))
self.cell_events.append(('enter', cell_pos.x, cell_pos.y))
def on_cell_exit(self, cell_pos, button, action):
def on_cell_exit(self, cell_pos):
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
self.cell_events.append(('exit', cell_pos.x, cell_pos.y, button, action))
self.cell_events.append(('exit', cell_pos.x, cell_pos.y))
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = GridWithCellCallbacks(grid_size=(5, 5), texture=texture, pos=(0, 0), size=(100, 100))
@ -78,8 +79,9 @@ try:
# Manually call methods to verify signature works
grid.on_cell_click(mcrfpy.Vector(1.0, 2.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
grid.on_cell_enter(mcrfpy.Vector(3.0, 4.0), mcrfpy.MouseButton.RIGHT, mcrfpy.InputState.RELEASED)
grid.on_cell_exit(mcrfpy.Vector(5.0, 6.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
# #230 - Cell hover callbacks now only receive (cell_pos)
grid.on_cell_enter(mcrfpy.Vector(3.0, 4.0))
grid.on_cell_exit(mcrfpy.Vector(5.0, 6.0))
assert len(grid.cell_events) == 3, f"Should have 3 events, got {len(grid.cell_events)}"
assert grid.cell_events[0][0] == 'click', "First event should be click"

View file

@ -25,7 +25,8 @@ def test_click_callback_signature(pos, button, action):
results.append(("on_click button/action are strings", False))
print(f"FAIL: button={type(button).__name__}, action={type(action).__name__}")
def test_on_enter_callback_signature(pos, button, action):
# #230 - Hover callbacks now receive only (pos), not (pos, button, action)
def test_on_enter_callback_signature(pos):
"""Test on_enter callback receives Vector."""
if isinstance(pos, mcrfpy.Vector):
results.append(("on_enter pos is Vector", True))
@ -34,7 +35,7 @@ def test_on_enter_callback_signature(pos, button, action):
results.append(("on_enter pos is Vector", False))
print(f"FAIL: on_enter receives {type(pos).__name__} instead of Vector")
def test_on_exit_callback_signature(pos, button, action):
def test_on_exit_callback_signature(pos):
"""Test on_exit callback receives Vector."""
if isinstance(pos, mcrfpy.Vector):
results.append(("on_exit pos is Vector", True))
@ -43,7 +44,7 @@ def test_on_exit_callback_signature(pos, button, action):
results.append(("on_exit pos is Vector", False))
print(f"FAIL: on_exit receives {type(pos).__name__} instead of Vector")
def test_on_move_callback_signature(pos, button, action):
def test_on_move_callback_signature(pos):
"""Test on_move callback receives Vector."""
if isinstance(pos, mcrfpy.Vector):
results.append(("on_move pos is Vector", True))
@ -52,8 +53,9 @@ def test_on_move_callback_signature(pos, button, action):
results.append(("on_move pos is Vector", False))
print(f"FAIL: on_move receives {type(pos).__name__} instead of Vector")
def test_cell_click_callback_signature(cell_pos):
"""Test on_cell_click callback receives Vector."""
# #230 - Cell click still receives (cell_pos, button, action)
def test_cell_click_callback_signature(cell_pos, button, action):
"""Test on_cell_click callback receives Vector, MouseButton, InputState."""
if isinstance(cell_pos, mcrfpy.Vector):
results.append(("on_cell_click pos is Vector", True))
print(f"PASS: on_cell_click receives Vector: {cell_pos}")
@ -61,6 +63,7 @@ def test_cell_click_callback_signature(cell_pos):
results.append(("on_cell_click pos is Vector", False))
print(f"FAIL: on_cell_click receives {type(cell_pos).__name__} instead of Vector")
# #230 - Cell hover callbacks now receive only (cell_pos)
def test_cell_enter_callback_signature(cell_pos):
"""Test on_cell_enter callback receives Vector."""
if isinstance(cell_pos, mcrfpy.Vector):
@ -119,11 +122,15 @@ def run_test(runtime):
print("\n--- Simulating callback calls ---")
# Test that the callbacks are set up correctly
# on_click still takes (pos, button, action)
test_click_callback_signature(mcrfpy.Vector(150, 150), "left", "start")
test_on_enter_callback_signature(mcrfpy.Vector(100, 100), "enter", "start")
test_on_exit_callback_signature(mcrfpy.Vector(300, 300), "exit", "start")
test_on_move_callback_signature(mcrfpy.Vector(125, 175), "move", "start")
test_cell_click_callback_signature(mcrfpy.Vector(5, 3))
# #230 - Hover callbacks now take only (pos)
test_on_enter_callback_signature(mcrfpy.Vector(100, 100))
test_on_exit_callback_signature(mcrfpy.Vector(300, 300))
test_on_move_callback_signature(mcrfpy.Vector(125, 175))
# #230 - on_cell_click still takes (cell_pos, button, action)
test_cell_click_callback_signature(mcrfpy.Vector(5, 3), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
# #230 - Cell hover callbacks now take only (cell_pos)
test_cell_enter_callback_signature(mcrfpy.Vector(2, 7))
test_cell_exit_callback_signature(mcrfpy.Vector(8, 1))
@ -147,4 +154,4 @@ def run_test(runtime):
sys.exit(1)
# Run the test
mcrfpy.setTimer("test", run_test, 100)
mcrfpy.Timer("test", run_test, 100)

View file

@ -21,11 +21,11 @@ def test_callback_assignment():
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
ui.append(frame)
# Callbacks receive (x, y, button, action) - 4 arguments
def on_enter_cb(x, y, button, action):
# #230 - Hover callbacks now receive only (pos) - 1 argument
def on_enter_cb(pos):
pass
def on_exit_cb(x, y, button, action):
def on_exit_cb(pos):
pass
# Test assignment
@ -87,7 +87,8 @@ def test_all_types_have_events():
("Grid", mcrfpy.Grid(grid_size=(5, 5), pos=(0, 0), size=(100, 100))),
]
def dummy_cb(x, y, button, action):
# #230 - Hover callbacks now receive only (pos)
def dummy_cb(pos):
pass
for name, obj in types_to_test:
@ -129,15 +130,16 @@ def test_enter_exit_simulation():
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
ui.append(frame)
def on_enter(x, y, button, action):
# #230 - Hover callbacks now receive only (pos)
def on_enter(pos):
global enter_count, enter_positions
enter_count += 1
enter_positions.append((x, y))
enter_positions.append((pos.x, pos.y))
def on_exit(x, y, button, action):
def on_exit(pos):
global exit_count, exit_positions
exit_count += 1
exit_positions.append((x, y))
exit_positions.append((pos.x, pos.y))
frame.on_enter = on_enter
frame.on_exit = on_exit

View file

@ -26,9 +26,14 @@ def test_failed(name, error):
# Helper to create typed callback arguments
def make_click_args(x=0.0, y=0.0):
"""Create properly typed callback arguments for testing."""
"""Create properly typed callback arguments for testing on_click."""
return (mcrfpy.Vector(x, y), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
# #230 - Hover callbacks now only receive position
def make_hover_args(x=0.0, y=0.0):
"""Create properly typed callback arguments for testing on_enter/on_exit/on_move."""
return (mcrfpy.Vector(x, y),)
# ==============================================================================
# Test Classes
@ -156,7 +161,8 @@ try:
initial_gen = getattr(TrackedFrame, '_mcrf_callback_gen', 0)
# Add a callback method
def tracked_on_enter(self, pos, button, action):
# #230 - Hover callbacks now only receive (pos)
def tracked_on_enter(self, pos):
pass
TrackedFrame.on_enter = tracked_on_enter
@ -184,26 +190,26 @@ try:
self.events.append('click')
MultiCallbackFrame.on_click = multi_on_click
# Add on_enter
def multi_on_enter(self, pos, button, action):
# Add on_enter - #230: now only takes (pos)
def multi_on_enter(self, pos):
self.events.append('enter')
MultiCallbackFrame.on_enter = multi_on_enter
# Add on_exit
def multi_on_exit(self, pos, button, action):
# Add on_exit - #230: now only takes (pos)
def multi_on_exit(self, pos):
self.events.append('exit')
MultiCallbackFrame.on_exit = multi_on_exit
# Add on_move
def multi_on_move(self, pos, button, action):
# Add on_move - #230: now only takes (pos)
def multi_on_move(self, pos):
self.events.append('move')
MultiCallbackFrame.on_move = multi_on_move
# Call all methods
frame.on_click(*make_click_args())
frame.on_enter(*make_click_args())
frame.on_exit(*make_click_args())
frame.on_move(*make_click_args())
frame.on_enter(*make_hover_args())
frame.on_exit(*make_hover_args())
frame.on_move(*make_hover_args())
assert frame.events == ['click', 'enter', 'exit', 'move'], \
f"All callbacks should fire, got: {frame.events}"

View file

@ -1,13 +1,14 @@
#!/usr/bin/env python3
"""
Test UIDrawable subclass callback methods (#184)
Test UIDrawable subclass callback methods (#184, #230)
This tests the ability to define callback methods (on_click, on_enter,
on_exit, on_move) directly in Python subclasses of UIDrawable types
(Frame, Caption, Sprite, Grid, Line, Circle, Arc).
Callback signature: (pos: Vector, button: MouseButton, action: InputState)
This matches property callbacks for consistency.
Callback signatures:
- on_click: (pos: Vector, button: MouseButton, action: InputState)
- on_enter/on_exit/on_move: (pos: Vector) - #230: simplified to position-only
"""
import mcrfpy
import sys
@ -41,6 +42,7 @@ class ClickableFrame(mcrfpy.Frame):
# ==============================================================================
# Test 2: Frame subclass with all hover callbacks
# #230: Hover callbacks now take only (pos), not (pos, button, action)
# ==============================================================================
class HoverFrame(mcrfpy.Frame):
"""Frame subclass with on_enter, on_exit, on_move"""
@ -48,13 +50,13 @@ class HoverFrame(mcrfpy.Frame):
super().__init__(*args, **kwargs)
self.events = []
def on_enter(self, pos, button, action):
def on_enter(self, pos):
self.events.append(('enter', pos.x, pos.y))
def on_exit(self, pos, button, action):
def on_exit(self, pos):
self.events.append(('exit', pos.x, pos.y))
def on_move(self, pos, button, action):
def on_move(self, pos):
self.events.append(('move', pos.x, pos.y))
@ -264,11 +266,12 @@ except Exception as e:
test_failed("Subclass methods are callable and work", e)
# Test 11: Verify HoverFrame methods work with typed arguments
# #230: Hover callbacks now take only (pos)
try:
hover = HoverFrame(pos=(250, 100), size=(100, 100))
hover.on_enter(mcrfpy.Vector(10.0, 20.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
hover.on_exit(mcrfpy.Vector(30.0, 40.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
hover.on_move(mcrfpy.Vector(50.0, 60.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
hover.on_enter(mcrfpy.Vector(10.0, 20.0))
hover.on_exit(mcrfpy.Vector(30.0, 40.0))
hover.on_move(mcrfpy.Vector(50.0, 60.0))
assert len(hover.events) == 3, f"Should have 3 events, got {len(hover.events)}"
assert hover.events[0] == ('enter', 10.0, 20.0), f"Event mismatch: {hover.events[0]}"
assert hover.events[1] == ('exit', 30.0, 40.0), f"Event mismatch: {hover.events[1]}"