cell, scene callbacks support for derived classes + enum args

This commit is contained in:
John McCardle 2026-01-27 22:38:37 -05:00
commit d12bfd224c
6 changed files with 648 additions and 109 deletions

View file

@ -4,6 +4,7 @@
#include "PyVector.h"
#include "PyMouseButton.h"
#include "PyInputState.h"
#include "PyKey.h"
PyCallable::PyCallable(PyObject* _target)
{
@ -143,7 +144,32 @@ PyKeyCallable::PyKeyCallable()
void PyKeyCallable::call(std::string key, std::string action)
{
if (target == Py_None || target == NULL) return;
PyObject* args = Py_BuildValue("(ss)", key.c_str(), action.c_str());
// Convert key string to Key enum
sf::Keyboard::Key sfml_key = PyKey::from_legacy_string(key.c_str());
PyObject* key_enum = PyObject_CallFunction(PyKey::key_enum_class, "i", static_cast<int>(sfml_key));
if (!key_enum) {
std::cerr << "Failed to create Key enum for key: " << key << std::endl;
PyErr_Print();
PyErr_Clear();
return;
}
// Convert action string to InputState enum
int action_val = (action == "start" || action == "pressed") ? 0 : 1; // PRESSED = 0, RELEASED = 1
PyObject* action_enum = PyObject_CallFunction(PyInputState::input_state_enum_class, "i", action_val);
if (!action_enum) {
Py_DECREF(key_enum);
std::cerr << "Failed to create InputState enum for action: " << action << std::endl;
PyErr_Print();
PyErr_Clear();
return;
}
PyObject* args = Py_BuildValue("(OO)", key_enum, action_enum);
Py_DECREF(key_enum);
Py_DECREF(action_enum);
PyObject* retval = PyCallable::call(args, NULL);
if (!retval)
{

View file

@ -195,14 +195,43 @@ void PyScene::do_mouse_input(std::string button, std::string type)
// #184: Try property-assigned callable first (fast path)
if (target->click_callable && !target->click_callable->isNone()) {
target->click_callable->call(mousepos, button, type);
// Also fire grid cell click if applicable
if (target->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = static_cast<UIGrid*>(target);
if (grid->last_clicked_cell.has_value()) {
grid->fireCellClick(grid->last_clicked_cell.value(), button, type);
grid->last_clicked_cell = std::nullopt;
}
}
return; // Stop after first handler
}
// #184: Try Python subclass method
if (tryCallPythonMethod(target, "on_click", mousepos, button.c_str(), type.c_str())) {
// Also fire grid cell click if applicable
if (target->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = static_cast<UIGrid*>(target);
if (grid->last_clicked_cell.has_value()) {
grid->fireCellClick(grid->last_clicked_cell.value(), button, type);
grid->last_clicked_cell = std::nullopt;
}
}
return; // Stop after first handler
}
// Fire grid cell click even if no on_click handler (but has cell click handler)
if (target->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = static_cast<UIGrid*>(target);
if (grid->last_clicked_cell.has_value()) {
bool handled = grid->fireCellClick(grid->last_clicked_cell.value(), button, type);
grid->last_clicked_cell = std::nullopt;
if (handled) {
return; // Stop after handling cell click
}
}
}
// Element claimed the click but had no handler - still stop propagation
// (This maintains consistent behavior for subclasses that don't define on_click)
if (target->is_python_subclass) {
@ -286,7 +315,8 @@ void PyScene::do_mouse_hover(int x, int y)
auto grid = static_cast<UIGrid*>(drawable);
// #142 - Update cell hover tracking for grid
grid->updateCellHover(mousepos);
// Pass "none" for button and "move" for action during hover
grid->updateCellHover(mousepos, "none", "move");
if (grid->children) {
for (auto& child : *grid->children) {

View file

@ -4,6 +4,8 @@
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include "PyTransition.h"
#include "PyKey.h"
#include "PyInputState.h"
#include <iostream>
// Static map to store Python scene objects by name
@ -390,12 +392,38 @@ void PySceneClass::call_on_key(PySceneObject* self, const std::string& key, cons
// Look for on_key attribute on the Python object
// This handles both:
// 1. Subclass methods: class MyScene(Scene): def on_key(self, k, s): ...
// 2. Instance attributes: ts.on_key = lambda k, s: ... (when subclass shadows property)
// 1. Subclass methods: class MyScene(Scene): def on_key(self, key, action): ...
// 2. Instance attributes: ts.on_key = lambda k, a: ... (when subclass shadows property)
PyObject* attr = PyObject_GetAttrString((PyObject*)self, "on_key");
if (attr && PyCallable_Check(attr) && attr != Py_None) {
// Call it - works for both bound methods and regular callables
PyObject* result = PyObject_CallFunction(attr, "ss", key.c_str(), action.c_str());
// Convert key string to Key enum
sf::Keyboard::Key sfml_key = PyKey::from_legacy_string(key.c_str());
PyObject* key_enum = PyObject_CallFunction(PyKey::key_enum_class, "i", static_cast<int>(sfml_key));
if (!key_enum) {
std::cerr << "Failed to create Key enum for key: " << key << std::endl;
PyErr_Print();
Py_DECREF(attr);
PyGILState_Release(gstate);
return;
}
// Convert action string to InputState enum
int action_val = (action == "start" || action == "pressed") ? 0 : 1; // PRESSED = 0, RELEASED = 1
PyObject* action_enum = PyObject_CallFunction(PyInputState::input_state_enum_class, "i", action_val);
if (!action_enum) {
std::cerr << "Failed to create InputState enum for action: " << action << std::endl;
Py_DECREF(key_enum);
PyErr_Print();
Py_DECREF(attr);
PyGILState_Release(gstate);
return;
}
// Call it with typed args - works for both bound methods and regular callables
PyObject* result = PyObject_CallFunctionObjArgs(attr, key_enum, action_enum, NULL);
Py_DECREF(key_enum);
Py_DECREF(action_enum);
if (result) {
Py_DECREF(result);
} else {
@ -485,7 +513,7 @@ PyGetSetDef PySceneClass::getsetters[] = {
"Use to add, remove, or iterate over UI elements. Changes are reflected immediately."), NULL},
{"on_key", (getter)PySceneClass_get_on_key, (setter)PySceneClass_set_on_key,
MCRF_PROPERTY(on_key, "Keyboard event handler (callable or None). "
"Function receives (key: str, action: str) for keyboard events. "
"Function receives (key: Key, action: InputState) for keyboard events. "
"Set to None to remove the handler."), NULL},
{NULL}
};

View file

@ -13,6 +13,8 @@
#include "PyHeightMap.h" // #199 - HeightMap application methods
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
#include "PyMouseButton.h" // For MouseButton enum
#include "PyInputState.h" // For InputState enum
#include <algorithm>
#include <cmath> // #142 - for std::floor, std::isnan
#include <cstring> // #150 - for strcmp
@ -681,71 +683,22 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
}
// No entity handled it, check if grid itself has handler
// #184: Also check for Python subclass (might have on_click method)
if (click_callable || is_python_subclass) {
// #142 - Fire on_cell_click if we have the callback and clicked on a valid cell
if (on_cell_click_callable) {
// #184: Also check for Python subclass (might have on_click or on_cell_click method)
// Store clicked cell for later callback firing (with button/action from PyScene)
int cell_x = static_cast<int>(std::floor(grid_x));
int cell_y = static_cast<int>(std::floor(grid_y));
// Only fire if within valid grid bounds
if (cell_x >= 0 && cell_x < this->grid_w && cell_y >= 0 && cell_y < this->grid_h) {
// Create Vector object for cell position - must fetch finalized type from module
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_type) {
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)cell_x, (float)cell_y);
Py_DECREF(vector_type);
if (cell_pos) {
PyObject* args = Py_BuildValue("(O)", cell_pos);
Py_DECREF(cell_pos);
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
last_clicked_cell = sf::Vector2i(cell_x, cell_y);
} else {
Py_DECREF(result);
}
}
}
}
last_clicked_cell = std::nullopt;
}
// Return this if we have any handler (property callback, subclass method, or cell callback)
if (click_callable || is_python_subclass || on_cell_click_callable) {
return this;
}
// #142 - Even without click_callable, fire on_cell_click if present
// Note: We fire the callback but DON'T return this, because PyScene::do_mouse_input
// would try to call click_callable which doesn't exist
if (on_cell_click_callable) {
int cell_x = static_cast<int>(std::floor(grid_x));
int cell_y = static_cast<int>(std::floor(grid_y));
// Only fire if within valid grid bounds
if (cell_x >= 0 && cell_x < this->grid_w && cell_y >= 0 && cell_y < this->grid_h) {
// Create Vector object for cell position - must fetch finalized type from module
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_type) {
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)cell_x, (float)cell_y);
Py_DECREF(vector_type);
if (cell_pos) {
PyObject* args = Py_BuildValue("(O)", cell_pos);
Py_DECREF(cell_pos);
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
}
}
// Don't return this - no click_callable to call
}
}
return nullptr;
}
@ -2484,45 +2437,179 @@ sf::Vector2f UIGrid::getEffectiveCellSize() const {
return sf::Vector2f(cell_w * zoom, cell_h * zoom);
}
// #142 - Update cell hover state and fire callbacks
void UIGrid::updateCellHover(sf::Vector2f mousepos) {
auto new_cell = screenToCell(mousepos);
// Helper function to convert button string to MouseButton enum value
static int buttonStringToEnum(const std::string& button) {
if (button == "left") return 0; // MouseButton.LEFT
if (button == "right") return 1; // MouseButton.RIGHT
if (button == "middle") return 2; // MouseButton.MIDDLE
if (button == "wheel_up") return 3; // MouseButton.WHEEL_UP
if (button == "wheel_down") return 4; // MouseButton.WHEEL_DOWN
return 0; // Default to LEFT
}
// Check if cell changed
if (new_cell != hovered_cell) {
// Fire exit callback for old cell
if (hovered_cell.has_value() && on_cell_exit_callable) {
// Create Vector object for cell position - must fetch finalized type from module
// Helper function to convert action string to InputState enum value
static int actionStringToEnum(const std::string& action) {
if (action == "start" || action == "pressed") return 0; // InputState.PRESSED
if (action == "end" || action == "released") return 1; // InputState.RELEASED
return 0; // Default to PRESSED
}
// #142 - Refresh cell callback cache for Python subclass method support
void UIGrid::refreshCellCallbackCache(PyObject* pyObj) {
if (!pyObj || !is_python_subclass) {
cell_callback_cache.valid = false;
return;
}
// Get the class's callback generation counter
PyObject* cls = (PyObject*)Py_TYPE(pyObj);
uint32_t current_gen = 0;
PyObject* gen_obj = PyObject_GetAttrString(cls, "_mcrf_callback_gen");
if (gen_obj) {
current_gen = static_cast<uint32_t>(PyLong_AsUnsignedLong(gen_obj));
Py_DECREF(gen_obj);
} else {
PyErr_Clear();
}
// Check if cache is still valid
if (cell_callback_cache.valid && cell_callback_cache.generation == current_gen) {
return; // Cache is fresh
}
// Refresh cache - check for each cell callback method
cell_callback_cache.has_on_cell_click = false;
cell_callback_cache.has_on_cell_enter = false;
cell_callback_cache.has_on_cell_exit = false;
// Check class hierarchy for each method
PyTypeObject* type = Py_TYPE(pyObj);
while (type && type != &mcrfpydef::PyUIGridType && type != &PyBaseObject_Type) {
if (type->tp_dict) {
if (!cell_callback_cache.has_on_cell_click) {
PyObject* method = PyDict_GetItemString(type->tp_dict, "on_cell_click");
if (method && PyCallable_Check(method)) {
cell_callback_cache.has_on_cell_click = true;
}
}
if (!cell_callback_cache.has_on_cell_enter) {
PyObject* method = PyDict_GetItemString(type->tp_dict, "on_cell_enter");
if (method && PyCallable_Check(method)) {
cell_callback_cache.has_on_cell_enter = true;
}
}
if (!cell_callback_cache.has_on_cell_exit) {
PyObject* method = PyDict_GetItemString(type->tp_dict, "on_cell_exit");
if (method && PyCallable_Check(method)) {
cell_callback_cache.has_on_cell_exit = true;
}
}
}
type = type->tp_base;
}
cell_callback_cache.generation = current_gen;
cell_callback_cache.valid = true;
}
// Helper to create typed cell callback arguments: (Vector, MouseButton, InputState)
static PyObject* createCellCallbackArgs(sf::Vector2i cell, const std::string& button, const std::string& action) {
// Create Vector object for cell position
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_type) {
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)hovered_cell->x, (float)hovered_cell->y);
if (!vector_type) {
PyErr_Print();
return nullptr;
}
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)cell.x, (float)cell.y);
Py_DECREF(vector_type);
if (cell_pos) {
PyObject* args = Py_BuildValue("(O)", cell_pos);
if (!cell_pos) {
PyErr_Print();
return nullptr;
}
// Create MouseButton enum
int button_val = buttonStringToEnum(button);
PyObject* button_enum = PyObject_CallFunction(PyMouseButton::mouse_button_enum_class, "i", button_val);
if (!button_enum) {
Py_DECREF(cell_pos);
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
PyErr_Print();
return nullptr;
}
// Create InputState enum
int action_val = actionStringToEnum(action);
PyObject* action_enum = PyObject_CallFunction(PyInputState::input_state_enum_class, "i", action_val);
if (!action_enum) {
Py_DECREF(cell_pos);
Py_DECREF(button_enum);
PyErr_Print();
return nullptr;
}
PyObject* args = Py_BuildValue("(OOO)", cell_pos, button_enum, action_enum);
Py_DECREF(cell_pos);
Py_DECREF(button_enum);
Py_DECREF(action_enum);
return args;
}
// Fire cell click callback with full signature (cell_pos, button, action)
bool UIGrid::fireCellClick(sf::Vector2i cell, const std::string& button, const std::string& action) {
// Try property-assigned callback first
if (on_cell_click_callable && !on_cell_click_callable->isNone()) {
PyObject* args = createCellCallbackArgs(cell, button, action);
if (args) {
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell exit callback raised an exception:" << std::endl;
std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
}
return true;
}
}
// Fire enter callback for new cell
if (new_cell.has_value() && on_cell_enter_callable) {
// Create Vector object for cell position - must fetch finalized type from module
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_type) {
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)new_cell->x, (float)new_cell->y);
Py_DECREF(vector_type);
if (cell_pos) {
PyObject* args = Py_BuildValue("(O)", cell_pos);
Py_DECREF(cell_pos);
// Try Python subclass method
if (is_python_subclass) {
PyObject* pyObj = PythonObjectCache::getInstance().lookup(this->serial_number);
if (pyObj) {
refreshCellCallbackCache(pyObj);
if (cell_callback_cache.has_on_cell_click) {
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_click");
if (method && PyCallable_Check(method)) {
PyObject* args = createCellCallbackArgs(cell, button, action);
if (args) {
PyObject* result = PyObject_CallObject(method, args);
Py_DECREF(args);
Py_DECREF(method);
Py_DECREF(pyObj);
if (!result) {
std::cerr << "Cell click method raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
return true;
}
}
Py_XDECREF(method);
}
Py_DECREF(pyObj);
}
}
return false;
}
// Fire cell enter callback with full signature (cell_pos, button, action)
bool UIGrid::fireCellEnter(sf::Vector2i cell, const std::string& button, const std::string& action) {
// Try property-assigned callback first
if (on_cell_enter_callable && !on_cell_enter_callable->isNone()) {
PyObject* args = createCellCallbackArgs(cell, button, action);
if (args) {
PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
@ -2532,8 +2619,107 @@ void UIGrid::updateCellHover(sf::Vector2f mousepos) {
} else {
Py_DECREF(result);
}
return true;
}
}
// Try Python subclass method
if (is_python_subclass) {
PyObject* pyObj = PythonObjectCache::getInstance().lookup(this->serial_number);
if (pyObj) {
refreshCellCallbackCache(pyObj);
if (cell_callback_cache.has_on_cell_enter) {
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_enter");
if (method && PyCallable_Check(method)) {
PyObject* args = createCellCallbackArgs(cell, button, action);
if (args) {
PyObject* result = PyObject_CallObject(method, args);
Py_DECREF(args);
Py_DECREF(method);
Py_DECREF(pyObj);
if (!result) {
std::cerr << "Cell enter method raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
return true;
}
}
Py_XDECREF(method);
}
Py_DECREF(pyObj);
}
}
return false;
}
// Fire cell exit callback with full signature (cell_pos, button, action)
bool UIGrid::fireCellExit(sf::Vector2i cell, const std::string& button, const std::string& action) {
// Try property-assigned callback first
if (on_cell_exit_callable && !on_cell_exit_callable->isNone()) {
PyObject* args = createCellCallbackArgs(cell, button, action);
if (args) {
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell exit callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
return true;
}
}
// Try Python subclass method
if (is_python_subclass) {
PyObject* pyObj = PythonObjectCache::getInstance().lookup(this->serial_number);
if (pyObj) {
refreshCellCallbackCache(pyObj);
if (cell_callback_cache.has_on_cell_exit) {
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_exit");
if (method && PyCallable_Check(method)) {
PyObject* args = createCellCallbackArgs(cell, button, action);
if (args) {
PyObject* result = PyObject_CallObject(method, args);
Py_DECREF(args);
Py_DECREF(method);
Py_DECREF(pyObj);
if (!result) {
std::cerr << "Cell exit method raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
return true;
}
}
Py_XDECREF(method);
}
Py_DECREF(pyObj);
}
}
return false;
}
// #142 - Update cell hover state and fire callbacks
void UIGrid::updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action) {
auto new_cell = screenToCell(mousepos);
// Check if cell changed
if (new_cell != hovered_cell) {
// Fire exit callback for old cell
if (hovered_cell.has_value()) {
fireCellExit(hovered_cell.value(), button, action);
}
// Fire enter callback for new cell
if (new_cell.has_value()) {
fireCellEnter(new_cell.value(), button, action);
}
hovered_cell = new_cell;

View file

@ -136,6 +136,17 @@ public:
std::unique_ptr<PyClickCallable> on_cell_exit_callable;
std::unique_ptr<PyClickCallable> on_cell_click_callable;
std::optional<sf::Vector2i> hovered_cell; // Currently hovered cell or nullopt
std::optional<sf::Vector2i> last_clicked_cell; // Cell clicked during click_at
// Grid-specific cell callback cache (separate from UIDrawable::CallbackCache)
struct CellCallbackCache {
uint32_t generation = 0;
bool valid = false;
bool has_on_cell_click = false;
bool has_on_cell_enter = false;
bool has_on_cell_exit = false;
};
CellCallbackCache cell_callback_cache;
// #142 - Cell coordinate conversion (screen pos -> cell coords)
std::optional<sf::Vector2i> screenToCell(sf::Vector2f screen_pos) const;
@ -144,7 +155,17 @@ public:
sf::Vector2f getEffectiveCellSize() const;
// #142 - Update cell hover state (called from PyScene)
void updateCellHover(sf::Vector2f mousepos);
// Now takes button/action for consistent callback signatures
void updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action);
// Fire cell callbacks with full signature (cell_pos, button, action)
// Returns true if a callback was fired
bool fireCellClick(sf::Vector2i cell, const std::string& button, const std::string& action);
bool fireCellEnter(sf::Vector2i cell, const std::string& button, const std::string& action);
bool fireCellExit(sf::Vector2i cell, const std::string& button, const std::string& action);
// Refresh cell callback cache for subclass method support
void refreshCellCallbackCache(PyObject* pyObj);
// Property system for animations
bool setProperty(const std::string& name, float value) override;

View file

@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""
Test unified callback signatures with enum types (#184)
This tests that all callbacks now use consistent typed arguments:
- Drawable callbacks: (pos: Vector, button: MouseButton, action: InputState)
- Grid cell callbacks: (cell_pos: Vector, button: MouseButton, action: InputState)
- Scene on_key: (key: Key, action: InputState)
"""
import mcrfpy
import sys
# Test results tracking
results = []
def test_passed(name):
results.append((name, True, None))
print(f" PASS: {name}")
def test_failed(name, error):
results.append((name, False, str(error)))
print(f" FAIL: {name}: {error}")
# ==============================================================================
# Test 1: Verify enum types exist and are accessible
# ==============================================================================
print("\n=== Callback Enum Signature Tests ===\n")
try:
# Check MouseButton enum
assert hasattr(mcrfpy, 'MouseButton'), "MouseButton enum should exist"
assert hasattr(mcrfpy.MouseButton, 'LEFT'), "MouseButton.LEFT should exist"
assert hasattr(mcrfpy.MouseButton, 'RIGHT'), "MouseButton.RIGHT should exist"
# Check InputState enum
assert hasattr(mcrfpy, 'InputState'), "InputState enum should exist"
assert hasattr(mcrfpy.InputState, 'PRESSED'), "InputState.PRESSED should exist"
assert hasattr(mcrfpy.InputState, 'RELEASED'), "InputState.RELEASED should exist"
# Check Key enum
assert hasattr(mcrfpy, 'Key'), "Key enum should exist"
assert hasattr(mcrfpy.Key, 'A'), "Key.A should exist"
assert hasattr(mcrfpy.Key, 'ESCAPE'), "Key.ESCAPE should exist"
test_passed("Enum types exist and are accessible")
except Exception as e:
test_failed("Enum types exist and are accessible", e)
# ==============================================================================
# Test 2: Grid cell callback with enum signature (subclass method)
# ==============================================================================
try:
class GridWithCellCallbacks(mcrfpy.Grid):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.cell_events = []
def on_cell_click(self, cell_pos, button, action):
# Verify types
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):
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))
def on_cell_exit(self, cell_pos, button, action):
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))
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = GridWithCellCallbacks(grid_size=(5, 5), texture=texture, pos=(0, 0), size=(100, 100))
# Verify grid is subclass
assert isinstance(grid, mcrfpy.Grid), "Should be Grid instance"
assert type(grid) is not mcrfpy.Grid, "Should be subclass"
# 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)
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"
assert grid.cell_events[1][0] == 'enter', "Second event should be enter"
assert grid.cell_events[2][0] == 'exit', "Third event should be exit"
test_passed("Grid subclass cell callbacks with enum signature")
except Exception as e:
test_failed("Grid subclass cell callbacks with enum signature", e)
# ==============================================================================
# Test 3: Grid cell callback with property-assigned callable
# ==============================================================================
try:
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(5, 5), texture=texture, pos=(0, 0), size=(100, 100))
cell_events = []
def on_cell_click_handler(cell_pos, button, action):
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
cell_events.append(('click', cell_pos.x, cell_pos.y, button, action))
grid.on_cell_click = on_cell_click_handler
# Manually call to test (normally engine would call this)
grid.on_cell_click(mcrfpy.Vector(1.0, 2.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
assert len(cell_events) == 1, f"Should have 1 event, got {len(cell_events)}"
assert cell_events[0][3] == mcrfpy.MouseButton.LEFT, "Button should be MouseButton.LEFT"
assert cell_events[0][4] == mcrfpy.InputState.PRESSED, "Action should be InputState.PRESSED"
test_passed("Grid property cell callback with enum signature")
except Exception as e:
test_failed("Grid property cell callback with enum signature", e)
# ==============================================================================
# Test 4: Scene on_key callback with enum signature (subclass method)
# ==============================================================================
try:
class MyScene(mcrfpy.Scene):
def __init__(self, name):
super().__init__(name)
self.key_events = []
def on_key(self, key, action):
# Verify types - key should be Key enum, action should be InputState enum
self.key_events.append((key, action))
scene = MyScene("test_key_enum_scene")
# Manually call to test signature (normally engine would call this)
scene.on_key(mcrfpy.Key.A, mcrfpy.InputState.PRESSED)
scene.on_key(mcrfpy.Key.ESCAPE, mcrfpy.InputState.RELEASED)
assert len(scene.key_events) == 2, f"Should have 2 events, got {len(scene.key_events)}"
assert scene.key_events[0][0] == mcrfpy.Key.A, f"First key should be Key.A, got {scene.key_events[0][0]}"
assert scene.key_events[0][1] == mcrfpy.InputState.PRESSED, f"First action should be PRESSED"
assert scene.key_events[1][0] == mcrfpy.Key.ESCAPE, f"Second key should be Key.ESCAPE"
assert scene.key_events[1][1] == mcrfpy.InputState.RELEASED, f"Second action should be RELEASED"
test_passed("Scene subclass on_key with enum signature")
except Exception as e:
test_failed("Scene subclass on_key with enum signature", e)
# ==============================================================================
# Test 5: Scene on_key callback with property-assigned callable
# ==============================================================================
try:
scene = mcrfpy.Scene("test_key_prop_scene")
key_events = []
def on_key_handler(key, action):
key_events.append((key, action))
scene.on_key = on_key_handler
# Manually call to test (normally engine would call this via the callable)
scene.on_key(mcrfpy.Key.SPACE, mcrfpy.InputState.PRESSED)
assert len(key_events) == 1, f"Should have 1 event, got {len(key_events)}"
# Note: When calling directly on Python side, we pass enums directly
# The engine conversion happens internally when calling through C++
test_passed("Scene property on_key accepts enum args")
except Exception as e:
test_failed("Scene property on_key accepts enum args", e)
# ==============================================================================
# Test 6: Verify MouseButton enum values
# ==============================================================================
try:
# Verify the enum values are usable in comparisons
left = mcrfpy.MouseButton.LEFT
right = mcrfpy.MouseButton.RIGHT
assert left != right, "LEFT should not equal RIGHT"
assert left == mcrfpy.MouseButton.LEFT, "LEFT should equal itself"
# Verify we can use in conditions
def check_button(button):
if button == mcrfpy.MouseButton.LEFT:
return "left"
elif button == mcrfpy.MouseButton.RIGHT:
return "right"
return "other"
assert check_button(mcrfpy.MouseButton.LEFT) == "left"
assert check_button(mcrfpy.MouseButton.RIGHT) == "right"
test_passed("MouseButton enum values work correctly")
except Exception as e:
test_failed("MouseButton enum values work correctly", e)
# ==============================================================================
# Test 7: Verify InputState enum values
# ==============================================================================
try:
pressed = mcrfpy.InputState.PRESSED
released = mcrfpy.InputState.RELEASED
assert pressed != released, "PRESSED should not equal RELEASED"
assert pressed == mcrfpy.InputState.PRESSED, "PRESSED should equal itself"
test_passed("InputState enum values work correctly")
except Exception as e:
test_failed("InputState enum values work correctly", e)
# ==============================================================================
# Test 8: Verify Key enum values
# ==============================================================================
try:
a_key = mcrfpy.Key.A
esc_key = mcrfpy.Key.ESCAPE
assert a_key != esc_key, "A should not equal ESCAPE"
assert a_key == mcrfpy.Key.A, "A should equal itself"
# Verify various keys exist
assert hasattr(mcrfpy.Key, 'SPACE'), "SPACE should exist"
assert hasattr(mcrfpy.Key, 'ENTER'), "ENTER should exist"
assert hasattr(mcrfpy.Key, 'UP'), "UP should exist"
assert hasattr(mcrfpy.Key, 'DOWN'), "DOWN should exist"
test_passed("Key enum values work correctly")
except Exception as e:
test_failed("Key enum values work correctly", e)
# ==============================================================================
# Summary
# ==============================================================================
print("\n=== Test Summary ===")
passed = sum(1 for _, p, _ in results if p)
total = len(results)
print(f"Passed: {passed}/{total}")
if passed == total:
print("\nAll tests passed!")
sys.exit(0)
else:
print("\nSome tests failed:")
for name, p, err in results:
if not p:
print(f" - {name}: {err}")
sys.exit(1)