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:
parent
e14f3cb9fc
commit
2daebc84b5
12 changed files with 598 additions and 71 deletions
278
tests/cookbook/primitives/demo_drag_drop_frame.py
Normal file
278
tests/cookbook/primitives/demo_drag_drop_frame.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Drag and Drop (Frame) Demo - Sort colored frames into target bins
|
||||
|
||||
Interactive controls:
|
||||
Mouse drag: Move frames
|
||||
ESC: Return to menu
|
||||
|
||||
This demonstrates:
|
||||
- Frame drag and drop using on_click + on_move (Pythonic method override pattern)
|
||||
- Hit testing for drop targets
|
||||
- State tracking and validation
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class DraggableFrame(mcrfpy.Frame):
|
||||
"""A frame that can be dragged around the screen.
|
||||
|
||||
Uses Pythonic method override pattern - just define on_click and on_move
|
||||
methods directly, no need for self.on_click = self._on_click assignment.
|
||||
"""
|
||||
|
||||
def __init__(self, pos, size, color, color_type):
|
||||
"""
|
||||
Args:
|
||||
pos: Initial position tuple (x, y)
|
||||
size: Size tuple (w, h)
|
||||
color: Fill color tuple (r, g, b)
|
||||
color_type: 'red' or 'blue' for sorting validation
|
||||
"""
|
||||
super().__init__(pos, size, fill_color=color, outline=2, outline_color=(255, 255, 255))
|
||||
self.color_type = color_type
|
||||
self.dragging = False
|
||||
self.drag_offset = (0, 0)
|
||||
self.original_pos = pos
|
||||
# No need for self.on_click = self._on_click - just define on_click method below!
|
||||
|
||||
def on_click(self, pos, button, action):
|
||||
"""Handle click events for drag start/end.
|
||||
|
||||
Args:
|
||||
pos: mcrfpy.Vector with x, y coordinates
|
||||
button: mcrfpy.MouseButton enum (LEFT, RIGHT, etc.)
|
||||
action: mcrfpy.InputState enum (PRESSED, RELEASED)
|
||||
"""
|
||||
if button != mcrfpy.MouseButton.LEFT:
|
||||
return
|
||||
|
||||
if action == mcrfpy.InputState.PRESSED:
|
||||
# Begin dragging - calculate offset from frame origin
|
||||
self.dragging = True
|
||||
self.drag_offset = (pos.x - self.x, pos.y - self.y)
|
||||
elif action == mcrfpy.InputState.RELEASED:
|
||||
if self.dragging:
|
||||
self.dragging = False
|
||||
# Notify demo of drop
|
||||
if hasattr(self, 'on_drop_callback'):
|
||||
self.on_drop_callback(self)
|
||||
|
||||
def on_move(self, pos):
|
||||
"""Handle mouse movement for dragging.
|
||||
|
||||
Args:
|
||||
pos: mcrfpy.Vector with x, y coordinates
|
||||
Note: #230 - on_move now only receives position, not button/action
|
||||
"""
|
||||
if self.dragging:
|
||||
self.x = pos.x - self.drag_offset[0]
|
||||
self.y = pos.y - self.drag_offset[1]
|
||||
|
||||
|
||||
class DragDropFrameDemo:
|
||||
"""Demo showing frame drag and drop with sorting bins."""
|
||||
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("demo_drag_drop_frame")
|
||||
self.ui = self.scene.children
|
||||
self.draggables = []
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
"""Build the demo UI."""
|
||||
# Background
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(30, 30, 35))
|
||||
self.ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(
|
||||
text="Drag & Drop: Sort by Color",
|
||||
pos=(512, 30),
|
||||
font_size=28,
|
||||
fill_color=(255, 255, 255)
|
||||
)
|
||||
title.outline = 2
|
||||
title.outline_color = (0, 0, 0)
|
||||
self.ui.append(title)
|
||||
|
||||
# Score caption
|
||||
self.score_caption = mcrfpy.Caption(
|
||||
text="Sorted: 0 / 8",
|
||||
pos=(512, 70),
|
||||
font_size=20,
|
||||
fill_color=(200, 200, 200)
|
||||
)
|
||||
self.ui.append(self.score_caption)
|
||||
|
||||
# Target bins (bottom half)
|
||||
# Red bin on the left
|
||||
self.red_bin = mcrfpy.Frame(
|
||||
pos=(20, 500),
|
||||
size=(482, 248),
|
||||
fill_color=(96, 0, 0),
|
||||
outline=3,
|
||||
outline_color=(200, 50, 50)
|
||||
)
|
||||
self.ui.append(self.red_bin)
|
||||
|
||||
red_label = mcrfpy.Caption(
|
||||
text="RED BIN",
|
||||
pos=(261, 600),
|
||||
font_size=32,
|
||||
fill_color=(200, 100, 100)
|
||||
)
|
||||
self.ui.append(red_label)
|
||||
|
||||
# Blue bin on the right
|
||||
self.blue_bin = mcrfpy.Frame(
|
||||
pos=(522, 500),
|
||||
size=(482, 248),
|
||||
fill_color=(0, 0, 96),
|
||||
outline=3,
|
||||
outline_color=(50, 50, 200)
|
||||
)
|
||||
self.ui.append(self.blue_bin)
|
||||
|
||||
blue_label = mcrfpy.Caption(
|
||||
text="BLUE BIN",
|
||||
pos=(763, 600),
|
||||
font_size=32,
|
||||
fill_color=(100, 100, 200)
|
||||
)
|
||||
self.ui.append(blue_label)
|
||||
|
||||
# Create draggable frames (top half)
|
||||
# 4 red frames, 4 blue frames, arranged in 2 rows
|
||||
frame_size = (100, 80)
|
||||
spacing = 20
|
||||
start_x = 100
|
||||
start_y = 120
|
||||
|
||||
positions = []
|
||||
for row in range(2):
|
||||
for col in range(4):
|
||||
x = start_x + col * (frame_size[0] + spacing + 80)
|
||||
y = start_y + row * (frame_size[1] + spacing + 40)
|
||||
positions.append((x, y))
|
||||
|
||||
# Interleave red and blue
|
||||
colors = [
|
||||
((255, 64, 64), 'red'),
|
||||
((64, 64, 255), 'blue'),
|
||||
((255, 64, 64), 'red'),
|
||||
((64, 64, 255), 'blue'),
|
||||
((64, 64, 255), 'blue'),
|
||||
((255, 64, 64), 'red'),
|
||||
((64, 64, 255), 'blue'),
|
||||
((255, 64, 64), 'red'),
|
||||
]
|
||||
|
||||
for i, (pos, (color, color_type)) in enumerate(zip(positions, colors)):
|
||||
frame = DraggableFrame(pos, frame_size, color, color_type)
|
||||
frame.on_drop_callback = self._on_frame_drop
|
||||
self.draggables.append(frame)
|
||||
self.ui.append(frame)
|
||||
|
||||
# Add label inside frame
|
||||
label = mcrfpy.Caption(
|
||||
text=f"{i+1}",
|
||||
pos=(40, 25),
|
||||
font_size=24,
|
||||
fill_color=(255, 255, 255)
|
||||
)
|
||||
frame.children.append(label)
|
||||
|
||||
# Instructions
|
||||
instr = mcrfpy.Caption(
|
||||
text="Drag red frames to red bin, blue frames to blue bin | ESC to exit",
|
||||
pos=(512, 470),
|
||||
font_size=14,
|
||||
fill_color=(150, 150, 150)
|
||||
)
|
||||
self.ui.append(instr)
|
||||
|
||||
# Initial score update
|
||||
self._update_score()
|
||||
|
||||
def _point_in_frame(self, x, y, frame):
|
||||
"""Check if point (x, y) is inside frame."""
|
||||
return (frame.x <= x <= frame.x + frame.w and
|
||||
frame.y <= y <= frame.y + frame.h)
|
||||
|
||||
def _frame_in_bin(self, draggable, bin_frame):
|
||||
"""Check if draggable frame's center is in bin."""
|
||||
center_x = draggable.x + draggable.w / 2
|
||||
center_y = draggable.y + draggable.h / 2
|
||||
return self._point_in_frame(center_x, center_y, bin_frame)
|
||||
|
||||
def _on_frame_drop(self, frame):
|
||||
"""Called when a frame is dropped."""
|
||||
self._update_score()
|
||||
|
||||
def _update_score(self):
|
||||
"""Count and display correctly sorted frames."""
|
||||
correct = 0
|
||||
for frame in self.draggables:
|
||||
if frame.color_type == 'red' and self._frame_in_bin(frame, self.red_bin):
|
||||
correct += 1
|
||||
frame.outline_color = (0, 255, 0) # Green outline for correct
|
||||
elif frame.color_type == 'blue' and self._frame_in_bin(frame, self.blue_bin):
|
||||
correct += 1
|
||||
frame.outline_color = (0, 255, 0)
|
||||
else:
|
||||
frame.outline_color = (255, 255, 255) # White outline otherwise
|
||||
|
||||
self.score_caption.text = f"Sorted: {correct} / 8"
|
||||
|
||||
if correct == 8:
|
||||
self.score_caption.text = "All Sorted! Well done!"
|
||||
self.score_caption.fill_color = (100, 255, 100)
|
||||
|
||||
def on_key(self, key, state):
|
||||
"""Handle keyboard input."""
|
||||
if state != "start":
|
||||
return
|
||||
if key == "Escape":
|
||||
# Return to cookbook menu or exit
|
||||
try:
|
||||
from cookbook_main import main
|
||||
main()
|
||||
except:
|
||||
sys.exit(0)
|
||||
|
||||
def activate(self):
|
||||
"""Activate the demo scene."""
|
||||
self.scene.on_key = self.on_key
|
||||
mcrfpy.current_scene = self.scene
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the demo."""
|
||||
demo = DragDropFrameDemo()
|
||||
demo.activate()
|
||||
|
||||
# Headless screenshot
|
||||
try:
|
||||
if mcrfpy.headless_mode():
|
||||
from mcrfpy import automation
|
||||
# Move some frames to bins for screenshot
|
||||
demo.draggables[0].x = 100
|
||||
demo.draggables[0].y = 550
|
||||
demo.draggables[1].x = 600
|
||||
demo.draggables[1].y = 550
|
||||
demo._update_score()
|
||||
|
||||
mcrfpy.Timer("screenshot", lambda rt: (
|
||||
automation.screenshot("screenshots/primitives/drag_drop_frame.png"),
|
||||
sys.exit(0)
|
||||
), 100)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -49,9 +49,10 @@ def test_callback_refcount():
|
|||
errors.append(f"on_click returned non-callable after repeated access: {type(final_cb)}")
|
||||
|
||||
# Test on_enter, on_exit, on_move
|
||||
frame.on_enter = lambda pos, button, action: None
|
||||
frame.on_exit = lambda pos, button, action: None
|
||||
frame.on_move = lambda pos, button, action: None
|
||||
# #230 - Hover callbacks now take only (pos)
|
||||
frame.on_enter = lambda pos: None
|
||||
frame.on_exit = lambda pos: None
|
||||
frame.on_move = lambda pos: None
|
||||
|
||||
for name in ['on_enter', 'on_exit', 'on_move']:
|
||||
for i in range(5):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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]}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue