Parent-Child UI System (#122): - Add parent weak_ptr to UIDrawable for hierarchy tracking - Add setParent(), getParent(), removeFromParent() methods - UICollection now tracks owner and sets parent on append/insert - Auto-remove from old parent when adding to new collection Global Position Property (#102): - Add get_global_position() that walks up parent chain - Expose as read-only 'global_position' property on all UI types - Add UIDRAWABLE_PARENT_GETSETTERS macro for consistent bindings Dirty Flag System (#116): - Modify markDirty() to propagate up the parent chain - Add isDirty() and clearDirty() methods for render optimization Scene as Drawable (#118): - Add position, visible, opacity properties to Scene - Add setProperty()/getProperty() for animation support - Apply scene transformations in PyScene::render() - Fix lifecycle callbacks to clear errors when methods don't exist - Add GameEngine::getScene() public accessor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bfadab7486
commit
e3d8f54d46
19 changed files with 988 additions and 67 deletions
204
tests/unit/test_parent_child_system.py
Normal file
204
tests/unit/test_parent_child_system.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test #122: Parent-Child UI System
|
||||
Test #102: Global Position Property
|
||||
Test #116: Dirty Flag System (partial - propagation)
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_parent_property():
|
||||
"""Test that children get parent reference when added to Frame"""
|
||||
print("Testing parent property...")
|
||||
|
||||
# Create scene and get UI
|
||||
mcrfpy.createScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
# Create a parent frame
|
||||
parent = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
||||
ui.append(parent)
|
||||
|
||||
# Create a child caption
|
||||
child = mcrfpy.Caption(text="Child", pos=(10, 10))
|
||||
|
||||
# Before adding, parent should be None
|
||||
assert child.parent is None, f"Child should have no parent before adding, got: {child.parent}"
|
||||
|
||||
# Add child to parent
|
||||
parent.children.append(child)
|
||||
|
||||
# After adding, parent should be set
|
||||
assert child.parent is not None, "Child should have parent after adding"
|
||||
# The parent should be the same Frame we added to
|
||||
# (checking by position since identity comparison is tricky)
|
||||
assert child.parent.x == parent.x, f"Parent x mismatch: {child.parent.x} vs {parent.x}"
|
||||
assert child.parent.y == parent.y, f"Parent y mismatch: {child.parent.y} vs {parent.y}"
|
||||
|
||||
print(" - Parent property: PASS")
|
||||
|
||||
|
||||
def test_global_position():
|
||||
"""Test global position calculation through parent chain"""
|
||||
print("Testing global_position property...")
|
||||
|
||||
# Create scene and get UI
|
||||
mcrfpy.createScene("test2")
|
||||
ui = mcrfpy.sceneUI("test2")
|
||||
|
||||
# Create nested hierarchy:
|
||||
# root (50, 50)
|
||||
# -> child1 (20, 20) -> global (70, 70)
|
||||
# -> child2 (10, 10) -> global (80, 80)
|
||||
|
||||
root = mcrfpy.Frame(pos=(50, 50), size=(200, 200))
|
||||
ui.append(root)
|
||||
|
||||
child1 = mcrfpy.Frame(pos=(20, 20), size=(100, 100))
|
||||
root.children.append(child1)
|
||||
|
||||
child2 = mcrfpy.Caption(text="Deep", pos=(10, 10))
|
||||
child1.children.append(child2)
|
||||
|
||||
# Check global positions
|
||||
# root has no parent, global should equal local
|
||||
assert root.global_position.x == 50, f"Root global x: expected 50, got {root.global_position.x}"
|
||||
assert root.global_position.y == 50, f"Root global y: expected 50, got {root.global_position.y}"
|
||||
|
||||
# child1 is at (20, 20) inside root at (50, 50) -> global (70, 70)
|
||||
assert child1.global_position.x == 70, f"Child1 global x: expected 70, got {child1.global_position.x}"
|
||||
assert child1.global_position.y == 70, f"Child1 global y: expected 70, got {child1.global_position.y}"
|
||||
|
||||
# child2 is at (10, 10) inside child1 at global (70, 70) -> global (80, 80)
|
||||
assert child2.global_position.x == 80, f"Child2 global x: expected 80, got {child2.global_position.x}"
|
||||
assert child2.global_position.y == 80, f"Child2 global y: expected 80, got {child2.global_position.y}"
|
||||
|
||||
print(" - Global position: PASS")
|
||||
|
||||
|
||||
def test_parent_changes_on_move():
|
||||
"""Test that moving child to different parent updates parent reference"""
|
||||
print("Testing parent changes on move...")
|
||||
|
||||
mcrfpy.createScene("test3")
|
||||
ui = mcrfpy.sceneUI("test3")
|
||||
|
||||
parent1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100), fill_color=(255, 0, 0, 255))
|
||||
parent2 = mcrfpy.Frame(pos=(200, 0), size=(100, 100), fill_color=(0, 255, 0, 255))
|
||||
ui.append(parent1)
|
||||
ui.append(parent2)
|
||||
|
||||
child = mcrfpy.Caption(text="Movable", pos=(5, 5))
|
||||
parent1.children.append(child)
|
||||
|
||||
# Child should be in parent1
|
||||
assert child.parent is not None, "Child should have parent"
|
||||
assert child.parent.x == 0, f"Child parent should be parent1, x={child.parent.x}"
|
||||
|
||||
# Move child to parent2 (should auto-remove from parent1)
|
||||
parent2.children.append(child)
|
||||
|
||||
# Child should now be in parent2
|
||||
assert child.parent is not None, "Child should still have parent"
|
||||
assert child.parent.x == 200, f"Child parent should be parent2, x={child.parent.x}"
|
||||
|
||||
# parent1 should have no children
|
||||
assert len(parent1.children) == 0, f"parent1 should have 0 children, has {len(parent1.children)}"
|
||||
|
||||
# parent2 should have one child
|
||||
assert len(parent2.children) == 1, f"parent2 should have 1 child, has {len(parent2.children)}"
|
||||
|
||||
print(" - Parent changes on move: PASS")
|
||||
|
||||
|
||||
def test_remove_clears_parent():
|
||||
"""Test that removing child clears parent reference"""
|
||||
print("Testing remove clears parent...")
|
||||
|
||||
mcrfpy.createScene("test4")
|
||||
ui = mcrfpy.sceneUI("test4")
|
||||
|
||||
parent = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||
ui.append(parent)
|
||||
|
||||
child = mcrfpy.Caption(text="Removable", pos=(5, 5))
|
||||
parent.children.append(child)
|
||||
|
||||
assert child.parent is not None, "Child should have parent"
|
||||
|
||||
# Remove child
|
||||
parent.children.remove(child)
|
||||
|
||||
assert child.parent is None, f"Child should have no parent after remove, got: {child.parent}"
|
||||
assert len(parent.children) == 0, f"Parent should have no children after remove"
|
||||
|
||||
print(" - Remove clears parent: PASS")
|
||||
|
||||
|
||||
def test_scene_level_elements():
|
||||
"""Test that scene-level elements have no parent"""
|
||||
print("Testing scene-level elements...")
|
||||
|
||||
mcrfpy.createScene("test5")
|
||||
ui = mcrfpy.sceneUI("test5")
|
||||
|
||||
frame = mcrfpy.Frame(pos=(10, 10), size=(50, 50))
|
||||
ui.append(frame)
|
||||
|
||||
# Scene-level elements should have no parent
|
||||
assert frame.parent is None, f"Scene-level element should have no parent, got: {frame.parent}"
|
||||
|
||||
# Global position should equal local position
|
||||
assert frame.global_position.x == 10, f"Global x should equal local x"
|
||||
assert frame.global_position.y == 10, f"Global y should equal local y"
|
||||
|
||||
print(" - Scene-level elements: PASS")
|
||||
|
||||
|
||||
def test_all_drawable_types():
|
||||
"""Test parent/global_position on all drawable types"""
|
||||
print("Testing all drawable types...")
|
||||
|
||||
mcrfpy.createScene("test6")
|
||||
ui = mcrfpy.sceneUI("test6")
|
||||
|
||||
parent = mcrfpy.Frame(pos=(100, 100), size=(300, 300))
|
||||
ui.append(parent)
|
||||
|
||||
# Test all types
|
||||
types_to_test = [
|
||||
("Frame", mcrfpy.Frame(pos=(10, 10), size=(50, 50))),
|
||||
("Caption", mcrfpy.Caption(text="Test", pos=(10, 70))),
|
||||
("Sprite", mcrfpy.Sprite(pos=(10, 130))), # May need texture
|
||||
("Grid", mcrfpy.Grid(grid_size=(5, 5), pos=(10, 190), size=(80, 80))),
|
||||
]
|
||||
|
||||
for name, child in types_to_test:
|
||||
parent.children.append(child)
|
||||
assert child.parent is not None, f"{name} should have parent"
|
||||
# Global position should be local + parent's position
|
||||
expected_x = child.x + 100
|
||||
expected_y = child.y + 100
|
||||
assert child.global_position.x == expected_x, f"{name} global_x: expected {expected_x}, got {child.global_position.x}"
|
||||
assert child.global_position.y == expected_y, f"{name} global_y: expected {expected_y}, got {child.global_position.y}"
|
||||
|
||||
print(" - All drawable types: PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_parent_property()
|
||||
test_global_position()
|
||||
test_parent_changes_on_move()
|
||||
test_remove_clears_parent()
|
||||
test_scene_level_elements()
|
||||
test_all_drawable_types()
|
||||
|
||||
print("\n=== All tests passed! ===")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\nTEST FAILED: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
174
tests/unit/test_scene_properties.py
Normal file
174
tests/unit/test_scene_properties.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test Scene properties (#118: Scene as Drawable)"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Create test scenes
|
||||
mcrfpy.createScene("test_scene")
|
||||
|
||||
def test_scene_pos():
|
||||
"""Test Scene pos property"""
|
||||
print("Testing scene pos property...")
|
||||
|
||||
# Create a Scene subclass to test
|
||||
class TestScene(mcrfpy.Scene):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
scene = TestScene("scene_pos_test")
|
||||
|
||||
# Test initial position
|
||||
pos = scene.pos
|
||||
assert pos.x == 0.0, f"Initial pos.x should be 0.0, got {pos.x}"
|
||||
assert pos.y == 0.0, f"Initial pos.y should be 0.0, got {pos.y}"
|
||||
|
||||
# Test setting position with tuple
|
||||
scene.pos = (100.0, 200.0)
|
||||
pos = scene.pos
|
||||
assert pos.x == 100.0, f"pos.x should be 100.0, got {pos.x}"
|
||||
assert pos.y == 200.0, f"pos.y should be 200.0, got {pos.y}"
|
||||
|
||||
# Test setting position with Vector
|
||||
scene.pos = mcrfpy.Vector(50.0, 75.0)
|
||||
pos = scene.pos
|
||||
assert pos.x == 50.0, f"pos.x should be 50.0, got {pos.x}"
|
||||
assert pos.y == 75.0, f"pos.y should be 75.0, got {pos.y}"
|
||||
|
||||
print(" - Scene pos property: PASS")
|
||||
|
||||
def test_scene_visible():
|
||||
"""Test Scene visible property"""
|
||||
print("Testing scene visible property...")
|
||||
|
||||
class TestScene(mcrfpy.Scene):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
scene = TestScene("scene_vis_test")
|
||||
|
||||
# Test initial visibility (should be True)
|
||||
assert scene.visible == True, f"Initial visible should be True, got {scene.visible}"
|
||||
|
||||
# Test setting to False
|
||||
scene.visible = False
|
||||
assert scene.visible == False, f"visible should be False, got {scene.visible}"
|
||||
|
||||
# Test setting back to True
|
||||
scene.visible = True
|
||||
assert scene.visible == True, f"visible should be True, got {scene.visible}"
|
||||
|
||||
print(" - Scene visible property: PASS")
|
||||
|
||||
def test_scene_opacity():
|
||||
"""Test Scene opacity property"""
|
||||
print("Testing scene opacity property...")
|
||||
|
||||
class TestScene(mcrfpy.Scene):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
scene = TestScene("scene_opa_test")
|
||||
|
||||
# Test initial opacity (should be 1.0)
|
||||
assert abs(scene.opacity - 1.0) < 0.001, f"Initial opacity should be 1.0, got {scene.opacity}"
|
||||
|
||||
# Test setting opacity
|
||||
scene.opacity = 0.5
|
||||
assert abs(scene.opacity - 0.5) < 0.001, f"opacity should be 0.5, got {scene.opacity}"
|
||||
|
||||
# Test clamping to 0.0
|
||||
scene.opacity = -0.5
|
||||
assert scene.opacity >= 0.0, f"opacity should be clamped to >= 0.0, got {scene.opacity}"
|
||||
|
||||
# Test clamping to 1.0
|
||||
scene.opacity = 1.5
|
||||
assert scene.opacity <= 1.0, f"opacity should be clamped to <= 1.0, got {scene.opacity}"
|
||||
|
||||
print(" - Scene opacity property: PASS")
|
||||
|
||||
def test_scene_name():
|
||||
"""Test Scene name property (read-only)"""
|
||||
print("Testing scene name property...")
|
||||
|
||||
class TestScene(mcrfpy.Scene):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
scene = TestScene("my_test_scene")
|
||||
|
||||
# Test name
|
||||
assert scene.name == "my_test_scene", f"name should be 'my_test_scene', got {scene.name}"
|
||||
|
||||
# Name should be read-only (trying to set should raise)
|
||||
try:
|
||||
scene.name = "other_name"
|
||||
print(" - Scene name should be read-only: FAIL")
|
||||
sys.exit(1)
|
||||
except AttributeError:
|
||||
pass # Expected
|
||||
|
||||
print(" - Scene name property: PASS")
|
||||
|
||||
def test_scene_active():
|
||||
"""Test Scene active property"""
|
||||
print("Testing scene active property...")
|
||||
|
||||
class TestScene(mcrfpy.Scene):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
scene1 = TestScene("active_test_1")
|
||||
scene2 = TestScene("active_test_2")
|
||||
|
||||
# Activate scene1
|
||||
scene1.activate()
|
||||
assert scene1.active == True, f"scene1.active should be True after activation"
|
||||
assert scene2.active == False, f"scene2.active should be False"
|
||||
|
||||
# Activate scene2
|
||||
scene2.activate()
|
||||
assert scene1.active == False, f"scene1.active should be False after activating scene2"
|
||||
assert scene2.active == True, f"scene2.active should be True"
|
||||
|
||||
print(" - Scene active property: PASS")
|
||||
|
||||
def test_scene_get_ui():
|
||||
"""Test Scene get_ui method"""
|
||||
print("Testing scene get_ui method...")
|
||||
|
||||
class TestScene(mcrfpy.Scene):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
scene = TestScene("ui_test_scene")
|
||||
|
||||
# Get UI collection
|
||||
ui = scene.get_ui()
|
||||
assert ui is not None, "get_ui() should return a collection"
|
||||
|
||||
# Add some elements
|
||||
ui.append(mcrfpy.Frame(pos=(10, 20), size=(100, 100)))
|
||||
ui.append(mcrfpy.Caption(text="Test", pos=(50, 50)))
|
||||
|
||||
# Verify length
|
||||
assert len(ui) == 2, f"UI should have 2 elements, got {len(ui)}"
|
||||
|
||||
print(" - Scene get_ui method: PASS")
|
||||
|
||||
# Run all tests
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_scene_pos()
|
||||
test_scene_visible()
|
||||
test_scene_opacity()
|
||||
test_scene_name()
|
||||
test_scene_active()
|
||||
test_scene_get_ui()
|
||||
|
||||
print("\n=== All Scene property tests passed! ===")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\nFAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Loading…
Add table
Add a link
Reference in a new issue