diff --git a/src/PyCallable.cpp b/src/PyCallable.cpp index 4835d7a..b6927c0 100644 --- a/src/PyCallable.cpp +++ b/src/PyCallable.cpp @@ -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(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) { diff --git a/src/PyScene.cpp b/src/PyScene.cpp index 7c1bbc3..500bfce 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -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(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(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(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(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) { diff --git a/src/PySceneObject.cpp b/src/PySceneObject.cpp index e943fbf..8d78ca5 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -4,6 +4,8 @@ #include "McRFPy_API.h" #include "McRFPy_Doc.h" #include "PyTransition.h" +#include "PyKey.h" +#include "PyInputState.h" #include // 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(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} }; diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 7aea414..4612e85 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -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 #include // #142 - for std::floor, std::isnan #include // #150 - for strcmp @@ -681,69 +683,20 @@ 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) { - int cell_x = static_cast(std::floor(grid_x)); - int cell_y = static_cast(std::floor(grid_y)); + // #184: Also check for Python subclass (might have on_click or on_cell_click method) - // 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); - } - } - } - } - } - return this; + // Store clicked cell for later callback firing (with button/action from PyScene) + int cell_x = static_cast(std::floor(grid_x)); + int cell_y = static_cast(std::floor(grid_y)); + if (cell_x >= 0 && cell_x < this->grid_w && cell_y >= 0 && cell_y < this->grid_h) { + last_clicked_cell = sf::Vector2i(cell_x, cell_y); + } else { + last_clicked_cell = std::nullopt; } - // #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(std::floor(grid_x)); - int cell_y = static_cast(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 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; } return nullptr; @@ -2484,56 +2437,289 @@ sf::Vector2f UIGrid::getEffectiveCellSize() const { return sf::Vector2f(cell_w * zoom, cell_h * zoom); } +// 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 +} + +// 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(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) { + 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) { + 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); + 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 click 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_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) { + std::cerr << "Cell enter 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_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) { +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() && on_cell_exit_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)hovered_cell->x, (float)hovered_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_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); - } - } - } + if (hovered_cell.has_value()) { + fireCellExit(hovered_cell.value(), button, action); } // 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); - PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args); - Py_DECREF(args); - if (!result) { - std::cerr << "Cell enter callback raised an exception:" << std::endl; - PyErr_Print(); - PyErr_Clear(); - } else { - Py_DECREF(result); - } - } - } + if (new_cell.has_value()) { + fireCellEnter(new_cell.value(), button, action); } hovered_cell = new_cell; diff --git a/src/UIGrid.h b/src/UIGrid.h index 18b367d..68efb96 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -136,6 +136,17 @@ public: std::unique_ptr on_cell_exit_callable; std::unique_ptr on_cell_click_callable; std::optional hovered_cell; // Currently hovered cell or nullopt + std::optional 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 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; diff --git a/tests/unit/test_callback_enums.py b/tests/unit/test_callback_enums.py new file mode 100644 index 0000000..ebc392b --- /dev/null +++ b/tests/unit/test_callback_enums.py @@ -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)