diff --git a/src/Animation.cpp b/src/Animation.cpp index 22ba433..9b4c7ba 100644 --- a/src/Animation.cpp +++ b/src/Animation.cpp @@ -5,14 +5,6 @@ #include "McRFPy_API.h" #include "GameEngine.h" #include "PythonObjectCache.h" -// #229 - Includes for animation callback target conversion -#include "UIFrame.h" -#include "UICaption.h" -#include "UISprite.h" -#include "UIGrid.h" -#include "UILine.h" -#include "UICircle.h" -#include "UIArc.h" #include #include #include @@ -403,239 +395,26 @@ void Animation::applyValue(UIEntity* entity, const AnimationValue& value) { }, value); } -// #229 - Helper to convert UIDrawable target to Python object -static PyObject* convertDrawableToPython(std::shared_ptr drawable) { - if (!drawable) { - Py_RETURN_NONE; - } - - // Check cache first - if (drawable->serial_number != 0) { - PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number); - if (cached) { - return cached; // Already INCREF'd by lookup - } - } - - PyTypeObject* type = nullptr; - PyObject* obj = nullptr; - - switch (drawable->derived_type()) { - case PyObjectsEnum::UIFRAME: - { - type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"); - if (!type) return nullptr; - auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0); - if (pyObj) { - pyObj->data = std::static_pointer_cast(drawable); - pyObj->weakreflist = NULL; - } - obj = (PyObject*)pyObj; - break; - } - case PyObjectsEnum::UICAPTION: - { - type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"); - if (!type) return nullptr; - auto pyObj = (PyUICaptionObject*)type->tp_alloc(type, 0); - if (pyObj) { - pyObj->data = std::static_pointer_cast(drawable); - pyObj->font = nullptr; - pyObj->weakreflist = NULL; - } - obj = (PyObject*)pyObj; - break; - } - case PyObjectsEnum::UISPRITE: - { - type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"); - if (!type) return nullptr; - auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0); - if (pyObj) { - pyObj->data = std::static_pointer_cast(drawable); - pyObj->weakreflist = NULL; - } - obj = (PyObject*)pyObj; - break; - } - case PyObjectsEnum::UIGRID: - { - type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"); - if (!type) return nullptr; - auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0); - if (pyObj) { - pyObj->data = std::static_pointer_cast(drawable); - pyObj->weakreflist = NULL; - } - obj = (PyObject*)pyObj; - break; - } - case PyObjectsEnum::UILINE: - { - type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"); - if (!type) return nullptr; - auto pyObj = (PyUILineObject*)type->tp_alloc(type, 0); - if (pyObj) { - pyObj->data = std::static_pointer_cast(drawable); - pyObj->weakreflist = NULL; - } - obj = (PyObject*)pyObj; - break; - } - case PyObjectsEnum::UICIRCLE: - { - type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"); - if (!type) return nullptr; - auto pyObj = (PyUICircleObject*)type->tp_alloc(type, 0); - if (pyObj) { - pyObj->data = std::static_pointer_cast(drawable); - pyObj->weakreflist = NULL; - } - obj = (PyObject*)pyObj; - break; - } - case PyObjectsEnum::UIARC: - { - type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"); - if (!type) return nullptr; - auto pyObj = (PyUIArcObject*)type->tp_alloc(type, 0); - if (pyObj) { - pyObj->data = std::static_pointer_cast(drawable); - pyObj->weakreflist = NULL; - } - obj = (PyObject*)pyObj; - break; - } - default: - Py_RETURN_NONE; - } - - if (type) { - Py_DECREF(type); - } - - return obj ? obj : Py_None; -} - -// #229 - Helper to convert UIEntity target to Python object -static PyObject* convertEntityToPython(std::shared_ptr entity) { - if (!entity) { - Py_RETURN_NONE; - } - - // Check cache first - if (entity->serial_number != 0) { - PyObject* cached = PythonObjectCache::getInstance().lookup(entity->serial_number); - if (cached) { - return cached; // Already INCREF'd by lookup - } - } - - PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); - if (!type) { - Py_RETURN_NONE; - } - - auto pyObj = (PyUIEntityObject*)type->tp_alloc(type, 0); - Py_DECREF(type); - - if (!pyObj) { - Py_RETURN_NONE; - } - - pyObj->data = entity; - pyObj->weakreflist = NULL; - - return (PyObject*)pyObj; -} - -// #229 - Helper to convert AnimationValue to Python object -static PyObject* animationValueToPython(const AnimationValue& value) { - return std::visit([](const auto& val) -> PyObject* { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - return PyFloat_FromDouble(val); - } - else if constexpr (std::is_same_v) { - return PyLong_FromLong(val); - } - else if constexpr (std::is_same_v>) { - // Sprite frame list - return current frame as int - // (the interpolate function returns the current frame) - if (!val.empty()) { - return PyLong_FromLong(val.back()); - } - return PyLong_FromLong(0); - } - else if constexpr (std::is_same_v) { - return Py_BuildValue("(iiii)", val.r, val.g, val.b, val.a); - } - else if constexpr (std::is_same_v) { - return Py_BuildValue("(ff)", val.x, val.y); - } - else if constexpr (std::is_same_v) { - return PyUnicode_FromString(val.c_str()); - } - - Py_RETURN_NONE; - }, value); -} - void Animation::triggerCallback() { if (!pythonCallback) return; - + // Ensure we only trigger once if (callbackTriggered) return; callbackTriggered = true; - + PyGILState_STATE gstate = PyGILState_Ensure(); - - // #229 - Pass (target, property, final_value) instead of (None, None) - // Convert target to Python object - PyObject* targetObj = nullptr; - if (auto drawable = targetWeak.lock()) { - targetObj = convertDrawableToPython(drawable); - } else if (auto entity = entityTargetWeak.lock()) { - targetObj = convertEntityToPython(entity); - } - - // If target conversion failed, use None - if (!targetObj) { - targetObj = Py_None; - Py_INCREF(targetObj); - } - - // Property name - PyObject* propertyObj = PyUnicode_FromString(targetProperty.c_str()); - if (!propertyObj) { - Py_DECREF(targetObj); - PyGILState_Release(gstate); - return; - } - - // Final value (interpolated at t=1.0) - PyObject* valueObj = animationValueToPython(interpolate(1.0f)); - if (!valueObj) { - Py_DECREF(targetObj); - Py_DECREF(propertyObj); - PyGILState_Release(gstate); - return; - } - - PyObject* args = Py_BuildValue("(OOO)", targetObj, propertyObj, valueObj); - Py_DECREF(targetObj); - Py_DECREF(propertyObj); - Py_DECREF(valueObj); - - if (!args) { - PyGILState_Release(gstate); - return; - } - + + // TODO: In future, create PyAnimation wrapper for this animation + // For now, pass None for both parameters + PyObject* args = PyTuple_New(2); + Py_INCREF(Py_None); + Py_INCREF(Py_None); + PyTuple_SetItem(args, 0, Py_None); // animation parameter + PyTuple_SetItem(args, 1, Py_None); // target parameter + PyObject* result = PyObject_CallObject(pythonCallback, args); Py_DECREF(args); - + if (!result) { std::cerr << "Animation callback raised an exception:" << std::endl; PyErr_Print(); @@ -648,7 +427,7 @@ void Animation::triggerCallback() { } else { Py_DECREF(result); } - + PyGILState_Release(gstate); } diff --git a/src/PyCallable.cpp b/src/PyCallable.cpp index 2ea603e..b6927c0 100644 --- a/src/PyCallable.cpp +++ b/src/PyCallable.cpp @@ -186,123 +186,3 @@ void PyKeyCallable::call(std::string key, std::string action) std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; } } - -// #230 - PyHoverCallable implementation (position-only for on_enter/on_exit/on_move) -PyHoverCallable::PyHoverCallable(PyObject* _target) -: PyCallable(_target) -{} - -PyHoverCallable::PyHoverCallable() -: PyCallable(Py_None) -{} - -void PyHoverCallable::call(sf::Vector2f mousepos) -{ - if (target == Py_None || target == NULL) return; - - // Create a Vector object for the position - PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - if (!vector_type) { - std::cerr << "Failed to get Vector type for hover callback" << std::endl; - PyErr_Print(); - PyErr_Clear(); - return; - } - PyObject* pos = PyObject_CallFunction(vector_type, "ff", mousepos.x, mousepos.y); - Py_DECREF(vector_type); - if (!pos) { - std::cerr << "Failed to create Vector object for hover callback" << std::endl; - PyErr_Print(); - PyErr_Clear(); - return; - } - - // #230 - Hover callbacks take only (pos), not (pos, button, action) - PyObject* args = Py_BuildValue("(O)", pos); - Py_DECREF(pos); - - PyObject* retval = PyCallable::call(args, NULL); - Py_DECREF(args); - if (!retval) - { - std::cerr << "Hover callback raised an exception:" << std::endl; - PyErr_Print(); - PyErr_Clear(); - - // Check if we should exit on exception - if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) { - McRFPy_API::signalPythonException(); - } - } else if (retval != Py_None) - { - std::cout << "HoverCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; - Py_DECREF(retval); - } else { - Py_DECREF(retval); - } -} - -PyObject* PyHoverCallable::borrow() -{ - return target; -} - -// #230 - PyCellHoverCallable implementation (cell position-only for on_cell_enter/on_cell_exit) -PyCellHoverCallable::PyCellHoverCallable(PyObject* _target) -: PyCallable(_target) -{} - -PyCellHoverCallable::PyCellHoverCallable() -: PyCallable(Py_None) -{} - -void PyCellHoverCallable::call(sf::Vector2i cellpos) -{ - if (target == Py_None || target == NULL) return; - - // Create a Vector object for the cell position - PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - if (!vector_type) { - std::cerr << "Failed to get Vector type for cell hover callback" << std::endl; - PyErr_Print(); - PyErr_Clear(); - return; - } - PyObject* pos = PyObject_CallFunction(vector_type, "ii", cellpos.x, cellpos.y); - Py_DECREF(vector_type); - if (!pos) { - std::cerr << "Failed to create Vector object for cell hover callback" << std::endl; - PyErr_Print(); - PyErr_Clear(); - return; - } - - // #230 - Cell hover callbacks take only (cell_pos), not (cell_pos, button, action) - PyObject* args = Py_BuildValue("(O)", pos); - Py_DECREF(pos); - - PyObject* retval = PyCallable::call(args, NULL); - Py_DECREF(args); - if (!retval) - { - std::cerr << "Cell hover callback raised an exception:" << std::endl; - PyErr_Print(); - PyErr_Clear(); - - // Check if we should exit on exception - if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) { - McRFPy_API::signalPythonException(); - } - } else if (retval != Py_None) - { - std::cout << "CellHoverCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; - Py_DECREF(retval); - } else { - Py_DECREF(retval); - } -} - -PyObject* PyCellHoverCallable::borrow() -{ - return target; -} diff --git a/src/PyCallable.h b/src/PyCallable.h index 848d33d..5fa876c 100644 --- a/src/PyCallable.h +++ b/src/PyCallable.h @@ -39,33 +39,3 @@ public: PyKeyCallable(PyObject*); PyKeyCallable(); }; - -// #230 - Hover callbacks (on_enter, on_exit, on_move) take only position -class PyHoverCallable: public PyCallable -{ -public: - void call(sf::Vector2f mousepos); - PyObject* borrow(); - PyHoverCallable(PyObject*); - PyHoverCallable(); - PyHoverCallable(const PyHoverCallable& other) : PyCallable(other) {} - PyHoverCallable& operator=(const PyHoverCallable& other) { - PyCallable::operator=(other); - return *this; - } -}; - -// #230 - Cell hover callbacks (on_cell_enter, on_cell_exit) take only cell position -class PyCellHoverCallable: public PyCallable -{ -public: - void call(sf::Vector2i cellpos); - PyObject* borrow(); - PyCellHoverCallable(PyObject*); - PyCellHoverCallable(); - PyCellHoverCallable(const PyCellHoverCallable& other) : PyCallable(other) {} - PyCellHoverCallable& operator=(const PyCellHoverCallable& other) { - PyCallable::operator=(other); - return *this; - } -}; diff --git a/src/PyScene.cpp b/src/PyScene.cpp index c00aadc..500bfce 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -127,81 +127,6 @@ static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name, return called; } -// #230: Overload for hover events that take only position (no button/action) -static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name, - sf::Vector2f mousepos) { - if (!drawable->is_python_subclass) return false; - - PyObject* pyObj = PythonObjectCache::getInstance().lookup(drawable->serial_number); - if (!pyObj) return false; - - // Check and refresh cache if needed - PyObject* type = (PyObject*)Py_TYPE(pyObj); - if (!drawable->isCallbackCacheValid(type)) { - drawable->refreshCallbackCache(pyObj); - } - - // Check if this method exists in the cache - bool has_method = false; - if (strcmp(method_name, "on_enter") == 0) { - has_method = drawable->callback_cache.has_on_enter; - } else if (strcmp(method_name, "on_exit") == 0) { - has_method = drawable->callback_cache.has_on_exit; - } else if (strcmp(method_name, "on_move") == 0) { - has_method = drawable->callback_cache.has_on_move; - } - - if (!has_method) { - Py_DECREF(pyObj); - return false; - } - - // Get the method - PyObject* method = PyObject_GetAttrString(pyObj, method_name); - bool called = false; - - if (method && PyCallable_Check(method) && method != Py_None) { - // Create Vector object for position - PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - if (!vector_type) { - PyErr_Print(); - PyErr_Clear(); - Py_XDECREF(method); - Py_DECREF(pyObj); - return false; - } - PyObject* pos = PyObject_CallFunction(vector_type, "ff", mousepos.x, mousepos.y); - Py_DECREF(vector_type); - if (!pos) { - PyErr_Print(); - PyErr_Clear(); - Py_XDECREF(method); - Py_DECREF(pyObj); - return false; - } - - // #230: Call with just (Vector) signature for hover events - PyObject* args = Py_BuildValue("(O)", pos); - Py_DECREF(pos); - - PyObject* result = PyObject_Call(method, args, NULL); - Py_DECREF(args); - - if (result) { - Py_DECREF(result); - called = true; - } else { - PyErr_Print(); - } - } - - PyErr_Clear(); - Py_XDECREF(method); - Py_DECREF(pyObj); - - return called; -} - // Check if a UIDrawable can potentially handle an event // (has either a callable property OR is a Python subclass that might have a method) static bool canHandleEvent(UIDrawable* drawable, const char* event_type) { @@ -349,33 +274,30 @@ void PyScene::do_mouse_hover(int x, int y) // Mouse entered drawable->hovered = true; // #184: Try property-assigned callable first, then Python subclass method - // #230: Hover callbacks now take only (pos) if (drawable->on_enter_callable && !drawable->on_enter_callable->isNone()) { - drawable->on_enter_callable->call(mousepos); + drawable->on_enter_callable->call(mousepos, "enter", "start"); } else if (drawable->is_python_subclass) { - tryCallPythonMethod(drawable, "on_enter", mousepos); + tryCallPythonMethod(drawable, "on_enter", mousepos, "enter", "start"); } } else if (!is_inside && was_hovered) { // Mouse exited drawable->hovered = false; // #184: Try property-assigned callable first, then Python subclass method - // #230: Hover callbacks now take only (pos) if (drawable->on_exit_callable && !drawable->on_exit_callable->isNone()) { - drawable->on_exit_callable->call(mousepos); + drawable->on_exit_callable->call(mousepos, "exit", "start"); } else if (drawable->is_python_subclass) { - tryCallPythonMethod(drawable, "on_exit", mousepos); + tryCallPythonMethod(drawable, "on_exit", mousepos, "exit", "start"); } } // #141 - Fire on_move if mouse is inside and has a move/on_move callback // #184: Try property-assigned callable first, then Python subclass method - // #230: Hover callbacks now take only (pos) // Check is_python_subclass before function call to avoid overhead on hot path if (is_inside) { if (drawable->on_move_callable && !drawable->on_move_callable->isNone()) { - drawable->on_move_callable->call(mousepos); + drawable->on_move_callable->call(mousepos, "move", "start"); } else if (drawable->is_python_subclass) { - tryCallPythonMethod(drawable, "on_move", mousepos); + tryCallPythonMethod(drawable, "on_move", mousepos, "move", "start"); } } diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index 996e1bf..a3aff1a 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -60,16 +60,16 @@ UIDrawable::UIDrawable(const UIDrawable& other) if (other.click_callable) { click_callable = std::make_unique(*other.click_callable); } - // #140, #230 - Deep copy enter/exit callables (now PyHoverCallable) + // #140 - Deep copy enter/exit callables if (other.on_enter_callable) { - on_enter_callable = std::make_unique(*other.on_enter_callable); + on_enter_callable = std::make_unique(*other.on_enter_callable); } if (other.on_exit_callable) { - on_exit_callable = std::make_unique(*other.on_exit_callable); + on_exit_callable = std::make_unique(*other.on_exit_callable); } - // #141, #230 - Deep copy move callable (now PyHoverCallable) + // #141 - Deep copy move callable if (other.on_move_callable) { - on_move_callable = std::make_unique(*other.on_move_callable); + on_move_callable = std::make_unique(*other.on_move_callable); } // Deep copy render texture if needed @@ -100,20 +100,20 @@ UIDrawable& UIDrawable::operator=(const UIDrawable& other) { } else { click_callable.reset(); } - // #140, #230 - Deep copy enter/exit callables (now PyHoverCallable) + // #140 - Deep copy enter/exit callables if (other.on_enter_callable) { - on_enter_callable = std::make_unique(*other.on_enter_callable); + on_enter_callable = std::make_unique(*other.on_enter_callable); } else { on_enter_callable.reset(); } if (other.on_exit_callable) { - on_exit_callable = std::make_unique(*other.on_exit_callable); + on_exit_callable = std::make_unique(*other.on_exit_callable); } else { on_exit_callable.reset(); } - // #141, #230 - Deep copy move callable (now PyHoverCallable) + // #141 - Deep copy move callable if (other.on_move_callable) { - on_move_callable = std::make_unique(*other.on_move_callable); + on_move_callable = std::make_unique(*other.on_move_callable); } else { on_move_callable.reset(); } @@ -311,10 +311,10 @@ void UIDrawable::click_register(PyObject* callable) click_callable = std::make_unique(callable); } -// #140, #230 - Mouse enter/exit callback registration (now PyHoverCallable) +// #140 - Mouse enter/exit callback registration void UIDrawable::on_enter_register(PyObject* callable) { - on_enter_callable = std::make_unique(callable); + on_enter_callable = std::make_unique(callable); } void UIDrawable::on_enter_unregister() @@ -324,7 +324,7 @@ void UIDrawable::on_enter_unregister() void UIDrawable::on_exit_register(PyObject* callable) { - on_exit_callable = std::make_unique(callable); + on_exit_callable = std::make_unique(callable); } void UIDrawable::on_exit_unregister() @@ -332,10 +332,10 @@ void UIDrawable::on_exit_unregister() on_exit_callable.reset(); } -// #141, #230 - Mouse move callback registration (now PyHoverCallable) +// #141 - Mouse move callback registration void UIDrawable::on_move_register(PyObject* callable) { - on_move_callable = std::make_unique(callable); + on_move_callable = std::make_unique(callable); } void UIDrawable::on_move_unregister() diff --git a/src/UIDrawable.h b/src/UIDrawable.h index 7f0e053..7f3be1e 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -45,9 +45,9 @@ public: // Mouse input handling - callable objects for click, enter, exit, move events std::unique_ptr click_callable; - std::unique_ptr on_enter_callable; // #140, #230 - position-only - std::unique_ptr on_exit_callable; // #140, #230 - position-only - std::unique_ptr on_move_callable; // #141, #230 - position-only + std::unique_ptr on_enter_callable; // #140 + std::unique_ptr on_exit_callable; // #140 + std::unique_ptr on_move_callable; // #141 virtual UIDrawable* click_at(sf::Vector2f point) = 0; void click_register(PyObject*); diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 9af5354..4612e85 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -40,9 +40,8 @@ UIGrid::UIGrid() box.setPosition(position); // Sync box position box.setFillColor(sf::Color(0, 0, 0, 0)); - // #228 - Initialize render texture to game resolution (small default until game init) + // Initialize render texture (small default size) renderTexture.create(1, 1); - renderTextureSize = {1, 1}; // Initialize output sprite output.setTextureRect(sf::IntRect(0, 0, 0, 0)); @@ -77,8 +76,8 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _x box.setPosition(position); // Sync box position box.setFillColor(sf::Color(0,0,0,0)); - // #228 - create renderTexture sized to game resolution (dynamically resized as needed) - ensureRenderTextureSize(); + // create renderTexture with maximum theoretical size; sprite can resize to show whatever amount needs to be rendered + renderTexture.create(1920, 1080); // TODO - renderTexture should be window size; above 1080p this will cause rendering errors // Only initialize sprite if texture is available if (ptex) { @@ -146,9 +145,6 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) // Check visibility if (!visible) return; - // #228 - Ensure renderTexture matches current game resolution - ensureRenderTextureSize(); - // TODO: Apply opacity to output sprite // Get cell dimensions - use texture if available, otherwise defaults @@ -468,26 +464,6 @@ UIGrid::~UIGrid() } } -void UIGrid::ensureRenderTextureSize() -{ - // Get game resolution (or use sensible defaults during early init) - sf::Vector2u resolution{1920, 1080}; - if (Resources::game) { - resolution = Resources::game->getGameResolution(); - } - - // Clamp to reasonable maximum (SFML texture size limits) - unsigned int required_w = std::min(resolution.x, 4096u); - unsigned int required_h = std::min(resolution.y, 4096u); - - // Only recreate if size changed - if (renderTextureSize.x != required_w || renderTextureSize.y != required_h) { - renderTexture.create(required_w, required_h); - renderTextureSize = {required_w, required_h}; - output.setTexture(renderTexture.getTexture()); - } -} - PyObjectsEnum UIGrid::derived_type() { return PyObjectsEnum::UIGRID; @@ -2363,12 +2339,11 @@ PyObject* UIGrid::get_on_cell_enter(PyUIGridObject* self, void* closure) { Py_RETURN_NONE; } -// #230 - Cell hover callbacks now use PyCellHoverCallable (position-only) int UIGrid::set_on_cell_enter(PyUIGridObject* self, PyObject* value, void* closure) { if (value == Py_None) { self->data->on_cell_enter_callable.reset(); } else { - self->data->on_cell_enter_callable = std::make_unique(value); + self->data->on_cell_enter_callable = std::make_unique(value); } return 0; } @@ -2382,12 +2357,11 @@ PyObject* UIGrid::get_on_cell_exit(PyUIGridObject* self, void* closure) { Py_RETURN_NONE; } -// #230 - Cell hover callbacks now use PyCellHoverCallable (position-only) int UIGrid::set_on_cell_exit(PyUIGridObject* self, PyObject* value, void* closure) { if (value == Py_None) { self->data->on_cell_exit_callable.reset(); } else { - self->data->on_cell_exit_callable = std::make_unique(value); + self->data->on_cell_exit_callable = std::make_unique(value); } return 0; } @@ -2579,26 +2553,6 @@ static PyObject* createCellCallbackArgs(sf::Vector2i cell, const std::string& bu return args; } -// #230 - Helper to create cell hover callback arguments: (Vector) only -static PyObject* createCellHoverArgs(sf::Vector2i cell) { - // 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, "ii", cell.x, cell.y); - Py_DECREF(vector_type); - if (!cell_pos) { - PyErr_Print(); - return nullptr; - } - - PyObject* args = Py_BuildValue("(O)", cell_pos); - Py_DECREF(cell_pos); - 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 @@ -2650,12 +2604,23 @@ bool UIGrid::fireCellClick(sf::Vector2i cell, const std::string& button, const s return false; } -// #230 - Fire cell enter callback with position-only signature (cell_pos) -bool UIGrid::fireCellEnter(sf::Vector2i cell) { - // Try property-assigned callback first (now PyCellHoverCallable) +// 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()) { - on_cell_enter_callable->call(cell); - return true; + 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 @@ -2666,8 +2631,7 @@ bool UIGrid::fireCellEnter(sf::Vector2i cell) { if (cell_callback_cache.has_on_cell_enter) { PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_enter"); if (method && PyCallable_Check(method)) { - // #230: Cell hover takes only (cell_pos) - PyObject* args = createCellHoverArgs(cell); + PyObject* args = createCellCallbackArgs(cell, button, action); if (args) { PyObject* result = PyObject_CallObject(method, args); Py_DECREF(args); @@ -2691,12 +2655,23 @@ bool UIGrid::fireCellEnter(sf::Vector2i cell) { return false; } -// #230 - Fire cell exit callback with position-only signature (cell_pos) -bool UIGrid::fireCellExit(sf::Vector2i cell) { - // Try property-assigned callback first (now PyCellHoverCallable) +// 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()) { - on_cell_exit_callable->call(cell); - return true; + 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 @@ -2707,8 +2682,7 @@ bool UIGrid::fireCellExit(sf::Vector2i cell) { if (cell_callback_cache.has_on_cell_exit) { PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_exit"); if (method && PyCallable_Check(method)) { - // #230: Cell hover takes only (cell_pos) - PyObject* args = createCellHoverArgs(cell); + PyObject* args = createCellCallbackArgs(cell, button, action); if (args) { PyObject* result = PyObject_CallObject(method, args); Py_DECREF(args); @@ -2733,23 +2707,19 @@ bool UIGrid::fireCellExit(sf::Vector2i cell) { } // #142 - Update cell hover state and fire callbacks -// #230 - Cell hover callbacks now take only (cell_pos), no button/action void UIGrid::updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action) { - (void)button; // #230 - No longer used for hover callbacks - (void)action; // #230 - No longer used for hover callbacks - 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()); + fireCellExit(hovered_cell.value(), button, action); } // Fire enter callback for new cell if (new_cell.has_value()) { - fireCellEnter(new_cell.value()); + fireCellEnter(new_cell.value(), button, action); } hovered_cell = new_cell; diff --git a/src/UIGrid.h b/src/UIGrid.h index 7b8191c..68efb96 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -84,10 +84,6 @@ public: std::shared_ptr getTexture(); sf::Sprite sprite, output; sf::RenderTexture renderTexture; - sf::Vector2u renderTextureSize{0, 0}; // Track current allocation for resize detection - - // Helper to ensure renderTexture matches game resolution - void ensureRenderTextureSize(); // Intermediate texture for camera_rotation (larger than viewport to hold rotated content) sf::RenderTexture rotationTexture; @@ -135,10 +131,9 @@ public: TCOD_fov_algorithm_t fov_algorithm; // Default FOV algorithm (from mcrfpy.default_fov) int fov_radius; // Default FOV radius - // #142, #230 - Grid cell mouse events - // Cell hover callbacks take only (cell_pos); cell click still takes (cell_pos, button, action) - std::unique_ptr on_cell_enter_callable; - std::unique_ptr on_cell_exit_callable; + // #142 - Grid cell mouse events + std::unique_ptr on_cell_enter_callable; + 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 @@ -163,13 +158,11 @@ public: // Now takes button/action for consistent callback signatures void updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action); - // Fire cell callbacks - // #230: Cell hover callbacks (enter/exit) now take only (cell_pos) - // Cell click still takes (cell_pos, button, 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); - bool fireCellExit(sf::Vector2i cell); + 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); diff --git a/tests/cookbook/primitives/demo_drag_drop_frame.py b/tests/cookbook/primitives/demo_drag_drop_frame.py deleted file mode 100644 index 804d6f8..0000000 --- a/tests/cookbook/primitives/demo_drag_drop_frame.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/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() diff --git a/tests/regression/issue_callback_refcount_test.py b/tests/regression/issue_callback_refcount_test.py index f2397b4..da4b563 100644 --- a/tests/regression/issue_callback_refcount_test.py +++ b/tests/regression/issue_callback_refcount_test.py @@ -49,10 +49,9 @@ 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 - # #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 + frame.on_enter = lambda pos, button, action: None + frame.on_exit = lambda pos, button, action: None + frame.on_move = lambda pos, button, action: None for name in ['on_enter', 'on_exit', 'on_move']: for i in range(5): diff --git a/tests/unit/test_animation_callback_simple.py b/tests/unit/test_animation_callback_simple.py index 26fdcf0..f75d33d 100644 --- a/tests/unit/test_animation_callback_simple.py +++ b/tests/unit/test_animation_callback_simple.py @@ -10,13 +10,11 @@ print("=" * 30) # Global state to track callback callback_count = 0 -# #229 - Animation callbacks now receive (target, property, value) instead of (anim, target) -def my_callback(target, prop, value): +def my_callback(anim, target): """Simple callback that prints when animation completes""" global callback_count callback_count += 1 print(f"Animation completed! Callback #{callback_count}") - print(f" Target: {type(target).__name__}, Property: {prop}, Value: {value}") # Create scene callback_demo = mcrfpy.Scene("callback_demo") diff --git a/tests/unit/test_callback_enums.py b/tests/unit/test_callback_enums.py index a9b3657..ebc392b 100644 --- a/tests/unit/test_callback_enums.py +++ b/tests/unit/test_callback_enums.py @@ -61,14 +61,13 @@ 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)) - # #230 - Cell hover callbacks now only receive (cell_pos) - def on_cell_enter(self, cell_pos): + 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)) + self.cell_events.append(('enter', cell_pos.x, cell_pos.y, button, action)) - def on_cell_exit(self, cell_pos): + 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)) + 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)) @@ -79,9 +78,8 @@ try: # Manually call methods to verify signature works grid.on_cell_click(mcrfpy.Vector(1.0, 2.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)) + 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" diff --git a/tests/unit/test_callback_vector.py b/tests/unit/test_callback_vector.py index c918f08..f81aa6b 100644 --- a/tests/unit/test_callback_vector.py +++ b/tests/unit/test_callback_vector.py @@ -25,8 +25,7 @@ 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__}") -# #230 - Hover callbacks now receive only (pos), not (pos, button, action) -def test_on_enter_callback_signature(pos): +def test_on_enter_callback_signature(pos, button, action): """Test on_enter callback receives Vector.""" if isinstance(pos, mcrfpy.Vector): results.append(("on_enter pos is Vector", True)) @@ -35,7 +34,7 @@ def test_on_enter_callback_signature(pos): 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): +def test_on_exit_callback_signature(pos, button, action): """Test on_exit callback receives Vector.""" if isinstance(pos, mcrfpy.Vector): results.append(("on_exit pos is Vector", True)) @@ -44,7 +43,7 @@ def test_on_exit_callback_signature(pos): 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): +def test_on_move_callback_signature(pos, button, action): """Test on_move callback receives Vector.""" if isinstance(pos, mcrfpy.Vector): results.append(("on_move pos is Vector", True)) @@ -53,9 +52,8 @@ def test_on_move_callback_signature(pos): results.append(("on_move pos is Vector", False)) print(f"FAIL: on_move receives {type(pos).__name__} instead of 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.""" +def test_cell_click_callback_signature(cell_pos): + """Test on_cell_click callback receives Vector.""" 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}") @@ -63,7 +61,6 @@ def test_cell_click_callback_signature(cell_pos, button, action): 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): @@ -122,15 +119,11 @@ 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") - # #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_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)) test_cell_enter_callback_signature(mcrfpy.Vector(2, 7)) test_cell_exit_callback_signature(mcrfpy.Vector(8, 1)) @@ -154,4 +147,4 @@ def run_test(runtime): sys.exit(1) # Run the test -mcrfpy.Timer("test", run_test, 100) +mcrfpy.setTimer("test", run_test, 100) diff --git a/tests/unit/test_mouse_enter_exit.py b/tests/unit/test_mouse_enter_exit.py index 84e6598..9caae60 100644 --- a/tests/unit/test_mouse_enter_exit.py +++ b/tests/unit/test_mouse_enter_exit.py @@ -21,11 +21,11 @@ def test_callback_assignment(): frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200)) ui.append(frame) - # #230 - Hover callbacks now receive only (pos) - 1 argument - def on_enter_cb(pos): + # Callbacks receive (x, y, button, action) - 4 arguments + def on_enter_cb(x, y, button, action): pass - def on_exit_cb(pos): + def on_exit_cb(x, y, button, action): pass # Test assignment @@ -87,8 +87,7 @@ def test_all_types_have_events(): ("Grid", mcrfpy.Grid(grid_size=(5, 5), pos=(0, 0), size=(100, 100))), ] - # #230 - Hover callbacks now receive only (pos) - def dummy_cb(pos): + def dummy_cb(x, y, button, action): pass for name, obj in types_to_test: @@ -130,16 +129,15 @@ def test_enter_exit_simulation(): frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200)) ui.append(frame) - # #230 - Hover callbacks now receive only (pos) - def on_enter(pos): + def on_enter(x, y, button, action): global enter_count, enter_positions enter_count += 1 - enter_positions.append((pos.x, pos.y)) + enter_positions.append((x, y)) - def on_exit(pos): + def on_exit(x, y, button, action): global exit_count, exit_positions exit_count += 1 - exit_positions.append((pos.x, pos.y)) + exit_positions.append((x, y)) frame.on_enter = on_enter frame.on_exit = on_exit diff --git a/tests/unit/test_uidrawable_monkeypatch.py b/tests/unit/test_uidrawable_monkeypatch.py index b464ad6..6bdb540 100644 --- a/tests/unit/test_uidrawable_monkeypatch.py +++ b/tests/unit/test_uidrawable_monkeypatch.py @@ -26,14 +26,9 @@ 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 on_click.""" + """Create properly typed callback arguments for testing.""" 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 @@ -161,8 +156,7 @@ try: initial_gen = getattr(TrackedFrame, '_mcrf_callback_gen', 0) # Add a callback method - # #230 - Hover callbacks now only receive (pos) - def tracked_on_enter(self, pos): + def tracked_on_enter(self, pos, button, action): pass TrackedFrame.on_enter = tracked_on_enter @@ -190,26 +184,26 @@ try: self.events.append('click') MultiCallbackFrame.on_click = multi_on_click - # Add on_enter - #230: now only takes (pos) - def multi_on_enter(self, pos): + # Add on_enter + def multi_on_enter(self, pos, button, action): self.events.append('enter') MultiCallbackFrame.on_enter = multi_on_enter - # Add on_exit - #230: now only takes (pos) - def multi_on_exit(self, pos): + # Add on_exit + def multi_on_exit(self, pos, button, action): self.events.append('exit') MultiCallbackFrame.on_exit = multi_on_exit - # Add on_move - #230: now only takes (pos) - def multi_on_move(self, pos): + # Add on_move + def multi_on_move(self, pos, button, action): self.events.append('move') MultiCallbackFrame.on_move = multi_on_move # Call all methods frame.on_click(*make_click_args()) - frame.on_enter(*make_hover_args()) - frame.on_exit(*make_hover_args()) - frame.on_move(*make_hover_args()) + frame.on_enter(*make_click_args()) + frame.on_exit(*make_click_args()) + frame.on_move(*make_click_args()) assert frame.events == ['click', 'enter', 'exit', 'move'], \ f"All callbacks should fire, got: {frame.events}" diff --git a/tests/unit/test_uidrawable_subclass_callbacks.py b/tests/unit/test_uidrawable_subclass_callbacks.py index 15996ca..c4b29e4 100644 --- a/tests/unit/test_uidrawable_subclass_callbacks.py +++ b/tests/unit/test_uidrawable_subclass_callbacks.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 """ -Test UIDrawable subclass callback methods (#184, #230) +Test UIDrawable subclass callback methods (#184) 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 signatures: -- on_click: (pos: Vector, button: MouseButton, action: InputState) -- on_enter/on_exit/on_move: (pos: Vector) - #230: simplified to position-only +Callback signature: (pos: Vector, button: MouseButton, action: InputState) +This matches property callbacks for consistency. """ import mcrfpy import sys @@ -42,7 +41,6 @@ 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""" @@ -50,13 +48,13 @@ class HoverFrame(mcrfpy.Frame): super().__init__(*args, **kwargs) self.events = [] - def on_enter(self, pos): + def on_enter(self, pos, button, action): self.events.append(('enter', pos.x, pos.y)) - def on_exit(self, pos): + def on_exit(self, pos, button, action): self.events.append(('exit', pos.x, pos.y)) - def on_move(self, pos): + def on_move(self, pos, button, action): self.events.append(('move', pos.x, pos.y)) @@ -266,12 +264,11 @@ 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)) - hover.on_exit(mcrfpy.Vector(30.0, 40.0)) - hover.on_move(mcrfpy.Vector(50.0, 60.0)) + 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) 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]}"