McRogueFace/src/UIDrawable.cpp
John McCardle 9d67e33370 fix(render): content invalidation propagates again; clear hover on focus loss
closes #368
closes #367

#368 -- Frame(cache_subtree=True) silently froze its subtree's CONTENT.

markContentDirty() gated its walk up the parent chain on
(!was_dirty || !p->render_dirty). That is sound only if render_dirty is reliably
cleared once a drawable has been drawn. It is not: clearDirty() had exactly one
call site in the whole engine (UIGridView, added by #364 on this branch). UIFrame's
non-render-texture path, UICaption, UISprite, UILine, UICircle and UIArc never
cleared it, so render_dirty was a write-once latch -- true from an object's first
mutation until it died.

With the flag stuck true, was_dirty was always true AND the parent's render_dirty
was always true, so the guard was always false and content invalidation propagated
nowhere. A caching ancestor went on re-blitting a stale composite: text never
updated, colours never changed, sprite indices never changed.

Only movement survived, because x/y route through markCompositeDirty, whose walk
was already unconditional. That asymmetry is why the bug read as intermittent
rather than total, and it is why my original issue writeup got the shape wrong --
its repro's third step was a move, so it looked like the latch cleared. It does
not. Measured on master: 4/4 recolours and 3/3 caption edits under a cache were
ALL dropped, including the first.

The fix makes markContentDirty's walk unconditional, matching markCompositeDirty.
The alternative -- repair the guard by having every drawable clear the flag
honestly -- buys a shorter walk at the price of an invariant that silently breaks
the day someone adds a drawable and forgets. Not worth it here: parent chains are
shallow, and this is the exact cost markCompositeDirty has always paid on every
position change without anyone noticing.

A/B on the changed path (200k content mutations, best of 3):

    depth        1      4      8     16
    before     82ns   86ns   82ns   87ns    <- flat because it did nothing
    after      76ns  114ns  175ns  295ns

The old build's flatness IS the defect. Entity movement -- the actual hot path --
is unchanged (164ns/move either way), since it was already on the unconditional
composite path. Crucible shows no regression (step_swarm 12.9 -> 10.7ms,
entity_churn 36.3 -> 31.5ms, within noise).

Regression test asserts every consecutive content mutation repaints through a
cache at depths 1 and 5, AND that idle frames stay byte-identical -- without that
last assertion, "mark everything dirty always" would pass and the cache would be
worthless.

#367 -- Losing window focus stranded hover exactly as a window-leave did (#363).

An unfocused window is delivered no MouseMoved, so the hover walk never runs and
whatever was hovered when focus left stays lit while the game sits in the
background: on_exit/on_cell_exit never fire and hovered_cell keeps naming a cell
the user is no longer pointing at. sf::Event::LostFocus was already produced by
every backend -- SDL2Renderer even translates SDL_WINDOWEVENT_FOCUS_LOST into it --
and, like MouseLeft before #363, had nobody listening.

Routed to the same PyScene::do_mouse_leave() that #363 added. Once unfocused the
engine genuinely does not know where the pointer is, so "hovering nothing" is the
only truthful state to hold; the next MouseMoved after focus returns restores it.

automation.loseFocus() injects the event, via a new injectWindowEvent() rather than
injectMouseEvent -- a focus event has no coordinates and no button, and should not
borrow a signature that implies it does. api_surface golden re-baselined: +1 line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 07:40:20 -04:00

2376 lines
84 KiB
C++

#include "UIDrawable.h"
#include <iostream>
#include "UIFrame.h"
#include "UICaption.h"
#include "UISprite.h"
#include "PyGridData.h"
#include "UIGridView.h"
#include "UILine.h"
#include "UICircle.h"
#include "UIArc.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include "Animation.h"
#include "PyAnimation.h"
#include "PyEasing.h"
#include "PySceneObject.h" // #183: For scene parent lookup
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
// Helper function to extract UIDrawable* from any Python UI object
// Returns nullptr and sets Python error on failure
static UIDrawable* extractDrawable(PyObject* self, PyObjectsEnum objtype) {
switch (objtype) {
case PyObjectsEnum::UIFRAME:
return ((PyUIFrameObject*)self)->data.get();
case PyObjectsEnum::UICAPTION:
return ((PyUICaptionObject*)self)->data.get();
case PyObjectsEnum::UISPRITE:
return ((PyUISpriteObject*)self)->data.get();
case PyObjectsEnum::UILINE:
return ((PyUILineObject*)self)->data.get();
case PyObjectsEnum::UICIRCLE:
return ((PyUICircleObject*)self)->data.get();
case PyObjectsEnum::UIARC:
return ((PyUIArcObject*)self)->data.get();
case PyObjectsEnum::UIGRIDVIEW:
return ((PyUIGridViewObject*)self)->data.get();
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return nullptr;
}
}
// Helper to extract shared_ptr<UIDrawable> for ownership tracking
static std::shared_ptr<UIDrawable> extractDrawableShared(PyObject* self, PyObjectsEnum objtype) {
switch (objtype) {
case PyObjectsEnum::UIFRAME:
return ((PyUIFrameObject*)self)->data;
case PyObjectsEnum::UICAPTION:
return ((PyUICaptionObject*)self)->data;
case PyObjectsEnum::UISPRITE:
return ((PyUISpriteObject*)self)->data;
case PyObjectsEnum::UILINE:
return ((PyUILineObject*)self)->data;
case PyObjectsEnum::UICIRCLE:
return ((PyUICircleObject*)self)->data;
case PyObjectsEnum::UIARC:
return ((PyUIArcObject*)self)->data;
case PyObjectsEnum::UIGRIDVIEW:
return ((PyUIGridViewObject*)self)->data;
default:
return nullptr;
}
}
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
UIDrawable::UIDrawable(const UIDrawable& other)
: z_index(other.z_index),
name(other.name),
position(other.position),
rotation(other.rotation),
origin(other.origin),
rotate_with_camera(other.rotate_with_camera),
visible(other.visible),
opacity(other.opacity),
hovered(false), // Don't copy hover state
serial_number(0), // Don't copy serial number
use_render_texture(other.use_render_texture),
render_dirty(true) // Force redraw after copy
{
// Deep copy click_callable if it exists
if (other.click_callable) {
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
}
// #140, #230 - Deep copy enter/exit callables (now PyHoverCallable)
if (other.on_enter_callable) {
on_enter_callable = std::make_unique<PyHoverCallable>(*other.on_enter_callable);
}
if (other.on_exit_callable) {
on_exit_callable = std::make_unique<PyHoverCallable>(*other.on_exit_callable);
}
// #141, #230 - Deep copy move callable (now PyHoverCallable)
if (other.on_move_callable) {
on_move_callable = std::make_unique<PyHoverCallable>(*other.on_move_callable);
}
// Deep copy render texture if needed
if (other.render_texture && other.use_render_texture) {
auto size = other.render_texture->getSize();
enableRenderTexture(size.x, size.y);
}
}
UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
if (this != &other) {
// Copy basic members
z_index = other.z_index;
name = other.name;
position = other.position;
rotation = other.rotation;
origin = other.origin;
rotate_with_camera = other.rotate_with_camera;
visible = other.visible;
opacity = other.opacity;
hovered = false; // Don't copy hover state
use_render_texture = other.use_render_texture;
render_dirty = true; // Force redraw after copy
// Deep copy click_callable
if (other.click_callable) {
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
} else {
click_callable.reset();
}
// #140, #230 - Deep copy enter/exit callables (now PyHoverCallable)
if (other.on_enter_callable) {
on_enter_callable = std::make_unique<PyHoverCallable>(*other.on_enter_callable);
} else {
on_enter_callable.reset();
}
if (other.on_exit_callable) {
on_exit_callable = std::make_unique<PyHoverCallable>(*other.on_exit_callable);
} else {
on_exit_callable.reset();
}
// #141, #230 - Deep copy move callable (now PyHoverCallable)
if (other.on_move_callable) {
on_move_callable = std::make_unique<PyHoverCallable>(*other.on_move_callable);
} else {
on_move_callable.reset();
}
// Deep copy render texture if needed
if (other.render_texture && other.use_render_texture) {
auto size = other.render_texture->getSize();
enableRenderTexture(size.x, size.y);
} else {
render_texture.reset();
use_render_texture = false;
}
}
return *this;
}
UIDrawable::UIDrawable(UIDrawable&& other) noexcept
: z_index(other.z_index),
name(std::move(other.name)),
position(other.position),
rotation(other.rotation),
origin(other.origin),
rotate_with_camera(other.rotate_with_camera),
visible(other.visible),
opacity(other.opacity),
hovered(other.hovered),
serial_number(other.serial_number),
click_callable(std::move(other.click_callable)),
on_enter_callable(std::move(other.on_enter_callable)), // #140
on_exit_callable(std::move(other.on_exit_callable)), // #140
on_move_callable(std::move(other.on_move_callable)), // #141
render_texture(std::move(other.render_texture)),
render_sprite(std::move(other.render_sprite)),
use_render_texture(other.use_render_texture),
render_dirty(other.render_dirty)
{
// Clear the moved-from object's serial number to avoid cache issues
other.serial_number = 0;
other.hovered = false; // #140
}
UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept {
if (this != &other) {
// Clear our own cache entry if we have one
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
// Move basic members
z_index = other.z_index;
name = std::move(other.name);
position = other.position;
rotation = other.rotation;
origin = other.origin;
rotate_with_camera = other.rotate_with_camera;
visible = other.visible;
opacity = other.opacity;
hovered = other.hovered; // #140
serial_number = other.serial_number;
use_render_texture = other.use_render_texture;
render_dirty = other.render_dirty;
// Move unique_ptr members
click_callable = std::move(other.click_callable);
on_enter_callable = std::move(other.on_enter_callable); // #140
on_exit_callable = std::move(other.on_exit_callable); // #140
on_move_callable = std::move(other.on_move_callable); // #141
render_texture = std::move(other.render_texture);
render_sprite = std::move(other.render_sprite);
// Clear the moved-from object's serial number
other.serial_number = 0;
other.hovered = false; // #140
}
return *this;
}
UIDrawable::~UIDrawable() {
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
void UIDrawable::click_unregister()
{
click_callable.reset();
}
void UIDrawable::render()
{
render(sf::Vector2f(), Resources::game->getRenderTarget());
}
PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure)); // trust me bro, it's an Enum
PyObject* ptr;
switch (objtype)
{
case PyObjectsEnum::UIFRAME:
if (((PyUIFrameObject*)self)->data->click_callable)
ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UICAPTION:
if (((PyUICaptionObject*)self)->data->click_callable)
ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UISPRITE:
if (((PyUISpriteObject*)self)->data->click_callable)
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UILINE:
if (((PyUILineObject*)self)->data->click_callable)
ptr = ((PyUILineObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UICIRCLE:
if (((PyUICircleObject*)self)->data->click_callable)
ptr = ((PyUICircleObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UIARC:
if (((PyUIArcObject*)self)->data->click_callable)
ptr = ((PyUIArcObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UIGRIDVIEW:
if (((PyUIGridViewObject*)self)->data->click_callable)
ptr = ((PyUIGridViewObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
default:
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
return NULL;
}
if (ptr && ptr != Py_None) {
Py_INCREF(ptr); // Return new reference, not borrowed
return ptr;
}
Py_RETURN_NONE;
}
int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure)); // trust me bro, it's an Enum
UIDrawable* target;
switch (objtype)
{
case PyObjectsEnum::UIFRAME:
target = (((PyUIFrameObject*)self)->data.get());
break;
case PyObjectsEnum::UICAPTION:
target = (((PyUICaptionObject*)self)->data.get());
break;
case PyObjectsEnum::UISPRITE:
target = (((PyUISpriteObject*)self)->data.get());
break;
case PyObjectsEnum::UILINE:
target = (((PyUILineObject*)self)->data.get());
break;
case PyObjectsEnum::UICIRCLE:
target = (((PyUICircleObject*)self)->data.get());
break;
case PyObjectsEnum::UIARC:
target = (((PyUIArcObject*)self)->data.get());
break;
case PyObjectsEnum::UIGRIDVIEW:
target = (((PyUIGridViewObject*)self)->data.get());
break;
default:
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _set_click");
return -1;
}
if (value == Py_None)
{
target->click_unregister();
} else {
target->click_register(value);
}
return 0;
}
void UIDrawable::click_register(PyObject* callable)
{
click_callable = std::make_unique<PyClickCallable>(callable);
}
// #140, #230 - Mouse enter/exit callback registration (now PyHoverCallable)
void UIDrawable::on_enter_register(PyObject* callable)
{
on_enter_callable = std::make_unique<PyHoverCallable>(callable);
}
void UIDrawable::on_enter_unregister()
{
on_enter_callable.reset();
}
void UIDrawable::on_exit_register(PyObject* callable)
{
on_exit_callable = std::make_unique<PyHoverCallable>(callable);
}
void UIDrawable::on_exit_unregister()
{
on_exit_callable.reset();
}
// #141, #230 - Mouse move callback registration (now PyHoverCallable)
void UIDrawable::on_move_register(PyObject* callable)
{
on_move_callable = std::make_unique<PyHoverCallable>(callable);
}
void UIDrawable::on_move_unregister()
{
on_move_callable.reset();
}
PyObject* UIDrawable::get_int(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
return PyLong_FromLong(drawable->z_index);
}
int UIDrawable::set_int(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
return -1;
}
long z = PyLong_AsLong(value);
if (z == -1 && PyErr_Occurred()) {
return -1;
}
// Clamp to int range
if (z < INT_MIN) z = INT_MIN;
if (z > INT_MAX) z = INT_MAX;
int old_z_index = drawable->z_index;
drawable->z_index = static_cast<int>(z);
// Notify of z_index change
if (old_z_index != drawable->z_index) {
drawable->notifyZIndexChanged();
}
return 0;
}
void UIDrawable::notifyZIndexChanged() {
// Mark the current scene as needing sort
// This works for elements in the scene's ui_elements collection
McRFPy_API::markSceneNeedsSort();
// TODO: In the future, we could add parent tracking to handle Frame children
// For now, Frame children will need manual sorting or collection modification
// to trigger a resort
}
PyObject* UIDrawable::get_name(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
return PyUnicode_FromString(drawable->name.c_str());
}
int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
if (value == NULL || value == Py_None) {
drawable->name = "";
return 0;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "name must be a string");
return -1;
}
const char* name_str = PyUnicode_AsUTF8(value);
if (!name_str) {
return -1;
}
drawable->name = name_str;
return 0;
}
void UIDrawable::enableRenderTexture(unsigned int width, unsigned int height) {
// Create or recreate RenderTexture if size changed
if (!render_texture || render_texture->getSize().x != width || render_texture->getSize().y != height) {
render_texture = std::make_unique<sf::RenderTexture>();
if (!render_texture->create(width, height)) {
std::cerr << "[McRogueFace] Warning: Failed to create RenderTexture ("
<< width << "x" << height << ")" << std::endl;
render_texture.reset();
use_render_texture = false;
return;
}
render_sprite.setTexture(render_texture->getTexture());
}
use_render_texture = true;
render_dirty = true;
}
void UIDrawable::disableRenderTexture() {
if (!use_render_texture) return;
render_texture.reset();
render_sprite = sf::Sprite(); // Clear stale texture reference
use_render_texture = false;
render_dirty = true;
}
void UIDrawable::updateRenderTexture() {
if (!use_render_texture || !render_texture) {
return;
}
// Clear the RenderTexture
render_texture->clear(sf::Color::Transparent);
// Render content to RenderTexture
// This will be overridden by derived classes
// For now, just display the texture
render_texture->display();
// Update the sprite
render_sprite.setTexture(render_texture->getTexture());
}
PyObject* UIDrawable::get_float_member(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure) >> 8);
int member = reinterpret_cast<intptr_t>(closure) & 0xFF;
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
switch (member) {
case 0: // x
return PyFloat_FromDouble(drawable->position.x);
case 1: // y
return PyFloat_FromDouble(drawable->position.y);
case 2: // w (width) - delegate to get_bounds
return PyFloat_FromDouble(drawable->get_bounds().width);
case 3: // h (height) - delegate to get_bounds
return PyFloat_FromDouble(drawable->get_bounds().height);
default:
PyErr_SetString(PyExc_AttributeError, "Invalid float member");
return NULL;
}
}
int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure) >> 8);
int member = reinterpret_cast<intptr_t>(closure) & 0xFF;
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
float val = 0.0f;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = static_cast<float>(PyLong_AsLong(value));
} else {
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
return -1;
}
switch (member) {
case 0: // x
drawable->position.x = val;
drawable->onPositionChanged();
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
break;
case 1: // y
drawable->position.y = val;
drawable->onPositionChanged();
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
break;
case 2: // w
case 3: // h
{
sf::FloatRect bounds = drawable->get_bounds();
if (member == 2) {
drawable->resize(val, bounds.height);
} else {
drawable->resize(bounds.width, val);
}
drawable->markDirty(); // #290: size change invalidates own + parent cache
}
break;
default:
PyErr_SetString(PyExc_AttributeError, "Invalid float member");
return -1;
}
return 0;
}
PyObject* UIDrawable::get_pos(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
// #331: direct allocation fast path — no module import or type call per read
return PyVector(drawable->position).pyObject();
}
int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
// Accept tuple or Vector
float x, y;
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
PyObject* x_obj = PyTuple_GetItem(value, 0);
PyObject* y_obj = PyTuple_GetItem(value, 1);
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
} else {
PyErr_SetString(PyExc_TypeError, "Position x must be a number");
return -1;
}
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
} else {
PyErr_SetString(PyExc_TypeError, "Position y must be a number");
return -1;
}
} else {
// Try to get as Vector
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyVectorType)) {
PyVectorObject* vec = (PyVectorObject*)value;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "Position must be a tuple (x, y) or Vector");
return -1;
}
}
drawable->position = sf::Vector2f(x, y);
drawable->onPositionChanged();
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
return 0;
}
// Rotation property getter/setter
PyObject* UIDrawable::get_rotation(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
return PyFloat_FromDouble(drawable->rotation);
}
int UIDrawable::set_rotation(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
float val = 0.0f;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = static_cast<float>(PyLong_AsLong(value));
} else {
PyErr_SetString(PyExc_TypeError, "rotation must be a number (int or float)");
return -1;
}
drawable->rotation = val;
drawable->markDirty();
return 0;
}
// Origin property getter/setter
PyObject* UIDrawable::get_origin(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
// #331: direct allocation fast path — no module import or type call per read
return PyVector(drawable->origin).pyObject();
}
int UIDrawable::set_origin(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
// Accept tuple or Vector
float x, y;
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
PyObject* x_obj = PyTuple_GetItem(value, 0);
PyObject* y_obj = PyTuple_GetItem(value, 1);
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
} else {
PyErr_SetString(PyExc_TypeError, "origin x must be a number");
return -1;
}
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
} else {
PyErr_SetString(PyExc_TypeError, "origin y must be a number");
return -1;
}
} else {
// Try to get as Vector
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyVectorType)) {
PyVectorObject* vec = (PyVectorObject*)value;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "origin must be a tuple (x, y) or Vector");
return -1;
}
}
drawable->origin = sf::Vector2f(x, y);
drawable->markDirty();
return 0;
}
// rotate_with_camera property getter/setter
PyObject* UIDrawable::get_rotate_with_camera(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
return PyBool_FromLong(drawable->rotate_with_camera);
}
int UIDrawable::set_rotate_with_camera(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "rotate_with_camera must be a boolean");
return -1;
}
drawable->rotate_with_camera = PyObject_IsTrue(value);
drawable->markDirty();
return 0;
}
// #221 - Grid coordinate properties (only valid when the parent is a Grid, i.e. a UIGridView)
PyObject* UIDrawable::get_grid_pos(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
// Check if parent is a Grid (UIGridView) -- #361: GridData is not a drawable
// and can never be a parent.
auto parent_ptr = drawable->getParent();
if (!parent_ptr) {
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
return NULL;
}
UIGridView* grid = dynamic_cast<UIGridView*>(parent_ptr.get());
if (!grid) {
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
return NULL;
}
// Calculate grid position from pixel position
sf::Vector2f cell_size = grid->getEffectiveCellSize();
float grid_x = drawable->position.x / cell_size.x;
float grid_y = drawable->position.y / cell_size.y;
// Return as Vector
PyObject* vector_type = (PyObject*)&mcrfpydef::PyVectorType;
PyObject* args = Py_BuildValue("(ff)", grid_x, grid_y);
PyObject* result = PyObject_CallObject(vector_type, args);
Py_DECREF(args);
return result;
}
int UIDrawable::set_grid_pos(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
// Check if parent is a Grid (UIGridView) -- #361: GridData is not a drawable
// and can never be a parent.
auto parent_ptr = drawable->getParent();
if (!parent_ptr) {
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
return -1;
}
UIGridView* grid = dynamic_cast<UIGridView*>(parent_ptr.get());
if (!grid) {
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
return -1;
}
// Parse the grid position value
float grid_x, grid_y;
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
PyObject* x_obj = PyTuple_GetItem(value, 0);
PyObject* y_obj = PyTuple_GetItem(value, 1);
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
grid_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
} else {
PyErr_SetString(PyExc_TypeError, "grid_pos x must be a number");
return -1;
}
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
grid_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
} else {
PyErr_SetString(PyExc_TypeError, "grid_pos y must be a number");
return -1;
}
} else if (PyObject_HasAttrString(value, "x") && PyObject_HasAttrString(value, "y")) {
// Vector-like object
PyObject* x_attr = PyObject_GetAttrString(value, "x");
PyObject* y_attr = PyObject_GetAttrString(value, "y");
if (x_attr && (PyFloat_Check(x_attr) || PyLong_Check(x_attr))) {
grid_x = PyFloat_Check(x_attr) ? PyFloat_AsDouble(x_attr) : static_cast<float>(PyLong_AsLong(x_attr));
} else {
Py_XDECREF(x_attr);
Py_XDECREF(y_attr);
PyErr_SetString(PyExc_TypeError, "grid_pos x must be a number");
return -1;
}
if (y_attr && (PyFloat_Check(y_attr) || PyLong_Check(y_attr))) {
grid_y = PyFloat_Check(y_attr) ? PyFloat_AsDouble(y_attr) : static_cast<float>(PyLong_AsLong(y_attr));
} else {
Py_XDECREF(x_attr);
Py_XDECREF(y_attr);
PyErr_SetString(PyExc_TypeError, "grid_pos y must be a number");
return -1;
}
Py_DECREF(x_attr);
Py_DECREF(y_attr);
} else {
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y) or Vector");
return -1;
}
// Convert grid position to pixel position
sf::Vector2f cell_size = grid->getEffectiveCellSize();
drawable->position.x = grid_x * cell_size.x;
drawable->position.y = grid_y * cell_size.y;
drawable->onPositionChanged();
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
return 0;
}
PyObject* UIDrawable::get_grid_size(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
// Check if parent is a Grid (UIGridView) -- #361: GridData is not a drawable
// and can never be a parent.
auto parent_ptr = drawable->getParent();
if (!parent_ptr) {
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
return NULL;
}
UIGridView* grid = dynamic_cast<UIGridView*>(parent_ptr.get());
if (!grid) {
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
return NULL;
}
// Calculate grid size from pixel size
sf::FloatRect bounds = drawable->get_bounds();
sf::Vector2f cell_size = grid->getEffectiveCellSize();
float grid_w = bounds.width / cell_size.x;
float grid_h = bounds.height / cell_size.y;
// Return as Vector
PyObject* vector_type = (PyObject*)&mcrfpydef::PyVectorType;
PyObject* args = Py_BuildValue("(ff)", grid_w, grid_h);
PyObject* result = PyObject_CallObject(vector_type, args);
Py_DECREF(args);
return result;
}
int UIDrawable::set_grid_size(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
// Check if parent is a Grid (UIGridView) -- #361: GridData is not a drawable
// and can never be a parent.
auto parent_ptr = drawable->getParent();
if (!parent_ptr) {
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
return -1;
}
UIGridView* grid = dynamic_cast<UIGridView*>(parent_ptr.get());
if (!grid) {
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
return -1;
}
// Parse the grid size value
float grid_w, grid_h;
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
PyObject* w_obj = PyTuple_GetItem(value, 0);
PyObject* h_obj = PyTuple_GetItem(value, 1);
if (PyFloat_Check(w_obj) || PyLong_Check(w_obj)) {
grid_w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : static_cast<float>(PyLong_AsLong(w_obj));
} else {
PyErr_SetString(PyExc_TypeError, "grid_size width must be a number");
return -1;
}
if (PyFloat_Check(h_obj) || PyLong_Check(h_obj)) {
grid_h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : static_cast<float>(PyLong_AsLong(h_obj));
} else {
PyErr_SetString(PyExc_TypeError, "grid_size height must be a number");
return -1;
}
} else if (PyObject_HasAttrString(value, "x") && PyObject_HasAttrString(value, "y")) {
// Vector-like object
PyObject* x_attr = PyObject_GetAttrString(value, "x");
PyObject* y_attr = PyObject_GetAttrString(value, "y");
if (x_attr && (PyFloat_Check(x_attr) || PyLong_Check(x_attr))) {
grid_w = PyFloat_Check(x_attr) ? PyFloat_AsDouble(x_attr) : static_cast<float>(PyLong_AsLong(x_attr));
} else {
Py_XDECREF(x_attr);
Py_XDECREF(y_attr);
PyErr_SetString(PyExc_TypeError, "grid_size width must be a number");
return -1;
}
if (y_attr && (PyFloat_Check(y_attr) || PyLong_Check(y_attr))) {
grid_h = PyFloat_Check(y_attr) ? PyFloat_AsDouble(y_attr) : static_cast<float>(PyLong_AsLong(y_attr));
} else {
Py_XDECREF(x_attr);
Py_XDECREF(y_attr);
PyErr_SetString(PyExc_TypeError, "grid_size height must be a number");
return -1;
}
Py_DECREF(x_attr);
Py_DECREF(y_attr);
} else {
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (w, h) or Vector");
return -1;
}
// Convert grid size to pixel size and resize
sf::Vector2f cell_size = grid->getEffectiveCellSize();
drawable->resize(grid_w * cell_size.x, grid_h * cell_size.y);
return 0;
}
// #122 - Parent-child hierarchy implementation
void UIDrawable::setParent(std::shared_ptr<UIDrawable> new_parent) {
parent = new_parent;
parent_scene.clear(); // #183: Clear scene parent when setting drawable parent
// Apply alignment when parent is set (if alignment is configured)
if (new_parent && align_type != AlignmentType::NONE) {
applyAlignment();
}
}
void UIDrawable::setParentScene(const std::string& scene_name) {
parent.reset(); // #183: Clear drawable parent when setting scene parent
parent_scene = scene_name;
// Apply alignment when scene parent is set (if alignment is configured)
if (!scene_name.empty() && align_type != AlignmentType::NONE) {
applyAlignment();
}
}
std::shared_ptr<UIDrawable> UIDrawable::getParent() const {
return parent.lock();
}
void UIDrawable::removeFromParent() {
// #183: Handle scene parent removal
if (!parent_scene.empty()) {
auto ui = Resources::game->scene_ui(parent_scene);
if (ui) {
for (auto it = ui->begin(); it != ui->end(); ++it) {
if (it->get() == this) {
ui->erase(it);
break;
}
}
}
parent_scene.clear();
return;
}
// Handle drawable parent removal
auto p = parent.lock();
if (!p) return;
// Check if parent is a UIFrame or UIGrid (both have children vector)
if (p->derived_type() == PyObjectsEnum::UIFRAME) {
auto frame = std::static_pointer_cast<UIFrame>(p);
auto& children = *frame->children;
// Find and remove this drawable from parent's children
// We need to find ourselves - but we don't have shared_from_this
// Instead, compare raw pointers
for (auto it = children.begin(); it != children.end(); ++it) {
if (it->get() == this) {
children.erase(it);
break;
}
}
frame->children_need_sort = true;
}
// #364: a grid child's parent is the UIGridView. The internal UIGrid (_GridData)
// holds no drawables, so it can never be a parent and needs no arm here.
else if (p->derived_type() == PyObjectsEnum::UIGRIDVIEW) {
auto view = std::static_pointer_cast<UIGridView>(p);
if (view->children) {
auto& children = *view->children;
for (auto it = children.begin(); it != children.end(); ++it) {
if (it->get() == this) {
children.erase(it);
break;
}
}
view->children_need_sort = true;
}
}
parent.reset();
}
// #102 - Global position calculation
sf::Vector2f UIDrawable::get_global_position() const {
sf::Vector2f global_pos = position;
auto p = parent.lock();
while (p) {
global_pos += p->position;
p = p->parent.lock();
}
return global_pos;
}
// #138 - Global bounds (bounds in screen coordinates)
sf::FloatRect UIDrawable::get_global_bounds() const {
sf::FloatRect local_bounds = get_bounds();
sf::Vector2f global_pos = get_global_position();
// Return bounds offset to global position
return sf::FloatRect(global_pos.x, global_pos.y, local_bounds.width, local_bounds.height);
}
// #138 - Hit testing
bool UIDrawable::contains_point(float x, float y) const {
sf::FloatRect global_bounds = get_global_bounds();
return global_bounds.contains(x, y);
}
// #144: Content dirty - texture needs rebuild
//
// #368 - The walk up the parent chain is UNCONDITIONAL. It used to be gated on
// `(!was_dirty || !p->render_dirty)`, which is sound only if render_dirty is reliably
// cleared once a drawable has been drawn. It is not: clearDirty() is called by the two
// classes that actually *cache* a raster (UIFrame's render-texture path, UIGridView),
// and by nobody else. For every ordinary Frame/Caption/Sprite/Line/Circle/Arc,
// render_dirty is a write-once latch -- true from the first mutation until death.
//
// With the flag stuck true, `was_dirty` was always true and the parent's render_dirty
// was always true, so the guard was always false and content invalidation propagated
// NOWHERE. A caching ancestor (Frame(cache_subtree=True), or a GridView) went on
// re-blitting a stale composite: text never updated, colors never changed. Only
// position changes survived, because markCompositeDirty (below) already walked
// unconditionally.
//
// Restoring the guard would mean making every drawable clear the flag honestly, and
// then trusting that every future drawable remembers to. The invariant here is worth
// more than the walk it saves: parent chains are shallow, and this is exactly the cost
// markCompositeDirty has always paid on every move without anyone noticing.
void UIDrawable::markContentDirty() {
render_dirty = true;
composite_dirty = true; // If content changed, composite also needs update
// Propagate to parent - parent's composite is dirty (child content changed)
auto p = parent.lock();
if (p) {
p->markContentDirty(); // Parent also needs to rebuild to include our changes
}
}
// #144: Composite dirty - position changed, texture still valid
void UIDrawable::markCompositeDirty() {
// Don't set render_dirty - our cached texture is still valid
// Only mark composite_dirty so parent knows to re-blit us
// Propagate to parent - parent needs to re-composite
auto p = parent.lock();
if (p) {
p->composite_dirty = true;
p->render_dirty = true; // Parent needs to re-render (re-composite children)
p->markCompositeDirty(); // Continue propagating up
}
}
// Legacy method - calls markContentDirty for backwards compatibility
void UIDrawable::markDirty() {
markContentDirty();
}
// #106 - Shader support
void UIDrawable::markShaderDynamic() {
shader_dynamic = true;
// Propagate to parent to invalidate caches
auto p = parent.lock();
if (p) {
p->markShaderDynamic();
}
}
// #106: Shader uniform property helpers for animation support
bool UIDrawable::setShaderProperty(const std::string& name, float value) {
// Check if name starts with "shader."
if (name.compare(0, 7, "shader.") != 0) {
return false;
}
// Extract the uniform name after "shader."
std::string uniform_name = name.substr(7);
if (uniform_name.empty()) {
return false;
}
// Initialize uniforms collection if needed
if (!uniforms) {
uniforms = std::make_unique<UniformCollection>();
}
// Set the uniform value
uniforms->setFloat(uniform_name, value);
markDirty();
return true;
}
bool UIDrawable::getShaderProperty(const std::string& name, float& value) const {
// Check if name starts with "shader."
if (name.compare(0, 7, "shader.") != 0) {
return false;
}
// Extract the uniform name after "shader."
std::string uniform_name = name.substr(7);
if (uniform_name.empty() || !uniforms) {
return false;
}
// Try to get the value from uniforms
const auto* entry = uniforms->getEntry(uniform_name);
if (!entry) {
return false;
}
// UniformEntry is variant<UniformValue, shared_ptr<PropertyBinding>, shared_ptr<CallableBinding>>
// UniformValue is variant<float, vec2, vec3, vec4>
// So we need to check for UniformValue first, then extract the float from it
// Try to extract static UniformValue from the entry
if (const auto* uval = std::get_if<UniformValue>(entry)) {
// Now try to extract float from UniformValue
if (const float* fval = std::get_if<float>(uval)) {
value = *fval;
return true;
}
// Could be vec2/vec3/vec4 - not a float, return false
return false;
}
// For bindings, evaluate and return
if (const auto* prop_binding = std::get_if<std::shared_ptr<PropertyBinding>>(entry)) {
auto opt_val = (*prop_binding)->evaluate();
if (opt_val) {
value = *opt_val;
return true;
}
} else if (const auto* call_binding = std::get_if<std::shared_ptr<CallableBinding>>(entry)) {
auto opt_val = (*call_binding)->evaluate();
if (opt_val) {
value = *opt_val;
return true;
}
}
return false;
}
bool UIDrawable::hasShaderProperty(const std::string& name) const {
// Check if name starts with "shader."
if (name.compare(0, 7, "shader.") != 0) {
return false;
}
// Shader uniforms are always valid property names (they'll be created on set)
return true;
}
// Python API for shader property
PyObject* UIDrawable::get_shader(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
if (!drawable->shader) {
Py_RETURN_NONE;
}
// Return the shader object (increment reference)
Py_INCREF(drawable->shader.get());
return (PyObject*)drawable->shader.get();
}
int UIDrawable::set_shader(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
if (value == Py_None) {
// Clear shader
drawable->shader.reset();
drawable->shader_dynamic = false;
drawable->markDirty();
return 0;
}
// Check if it's a Shader object
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyShaderType)) {
PyErr_SetString(PyExc_TypeError, "shader must be a Shader object or None");
return -1;
}
PyShaderObject* shader_obj = (PyShaderObject*)value;
if (!shader_obj->shader) {
PyErr_SetString(PyExc_ValueError, "Shader is not valid (compilation failed?)");
return -1;
}
// Store the shader
drawable->shader = std::shared_ptr<PyShaderObject>(shader_obj, [](PyShaderObject* p) {
// Custom deleter that doesn't delete the Python object
// The Python reference counting handles that
});
Py_INCREF(shader_obj); // Keep the Python object alive
// Create uniforms collection if needed
if (!drawable->uniforms) {
drawable->uniforms = std::make_unique<UniformCollection>();
}
// Set dynamic flag if shader is dynamic
if (shader_obj->dynamic) {
drawable->markShaderDynamic();
}
// Enable RenderTexture for shader rendering (if not already enabled)
auto bounds = drawable->get_bounds();
if (bounds.width > 0 && bounds.height > 0) {
drawable->enableRenderTexture(
static_cast<unsigned int>(bounds.width),
static_cast<unsigned int>(bounds.height)
);
}
drawable->markDirty();
return 0;
}
PyObject* UIDrawable::get_uniforms(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
// Create uniforms collection if needed
if (!drawable->uniforms) {
drawable->uniforms = std::make_unique<UniformCollection>();
}
// Create and return a Python wrapper for the collection
PyUniformCollectionObject* collection = (PyUniformCollectionObject*)
mcrfpydef::PyUniformCollectionType.tp_alloc(&mcrfpydef::PyUniformCollectionType, 0);
if (!collection) return NULL;
collection->collection = drawable->uniforms.get();
collection->weakreflist = NULL;
collection->owner = extractDrawableShared(self, objtype);
return (PyObject*)collection;
}
// Python API - get parent drawable
PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
// #183: Check for scene parent first
if (!drawable->parent_scene.empty()) {
PyObject* scene = PySceneClass::get_scene_by_name(drawable->parent_scene);
if (scene) {
return scene; // Already has new reference from get_scene_by_name
}
// Scene not found in python_scenes (shouldn't happen, but fall through to None)
}
auto parent_ptr = drawable->getParent();
if (!parent_ptr) {
Py_RETURN_NONE;
}
// Convert parent to Python object using the cache/conversion system
// Re-use the pattern from UICollection
PyTypeObject* type = nullptr;
PyObject* obj = nullptr;
switch (parent_ptr->derived_type()) {
case PyObjectsEnum::UIFRAME:
{
type = &mcrfpydef::PyUIFrameType;
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIFrame>(parent_ptr);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UICAPTION:
{
type = &mcrfpydef::PyUICaptionType;
auto pyObj = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UICaption>(parent_ptr);
pyObj->font = nullptr;
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UISPRITE:
{
type = &mcrfpydef::PyUISpriteType;
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UISprite>(parent_ptr);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UIGRIDVIEW:
{
type = &mcrfpydef::PyUIGridViewType;
auto pyObj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIGridView>(parent_ptr);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
default:
Py_RETURN_NONE;
}
return obj;
}
// Python API - set parent drawable (or None to remove from parent)
int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
std::shared_ptr<UIDrawable> drawable = nullptr;
// Get the shared_ptr for self
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data;
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data;
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data;
break;
case PyObjectsEnum::UILINE:
drawable = ((PyUILineObject*)self)->data;
break;
case PyObjectsEnum::UICIRCLE:
drawable = ((PyUICircleObject*)self)->data;
break;
case PyObjectsEnum::UIARC:
drawable = ((PyUIArcObject*)self)->data;
break;
case PyObjectsEnum::UIGRIDVIEW:
drawable = ((PyUIGridViewObject*)self)->data;
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return -1;
}
// Handle None - remove from parent
if (value == Py_None) {
drawable->removeFromParent();
return 0;
}
// Value must be a Frame, Grid, or Scene (things with children collections).
// #364: the internal _GridData (PyGridDataType) is NOT among them -- it holds
// entities and cells, never drawables -- so it falls through to the TypeError.
bool is_frame = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIFrameType);
bool is_gridview = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType);
bool is_scene = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PySceneType);
if (!is_frame && !is_gridview && !is_scene) {
PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Grid, Scene, or None");
return -1;
}
// Handle Scene parent specially - add to scene's children
if (is_scene) {
PySceneObject* scene_obj = (PySceneObject*)value;
std::string scene_name = scene_obj->name;
// Remove from old parent first
drawable->removeFromParent();
// Get the scene's UI elements and add
auto ui = Resources::game->scene_ui(scene_name);
if (ui) {
// Check if already in this scene (prevent duplicates)
bool already_present = false;
for (const auto& child : *ui) {
if (child.get() == drawable.get()) {
already_present = true;
break;
}
}
if (!already_present) {
ui->push_back(drawable);
drawable->setParentScene(scene_name);
}
}
return 0;
}
// Remove from old parent first
drawable->removeFromParent();
// Get the new parent's children collection and append
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>>* children_ptr = nullptr;
std::shared_ptr<UIDrawable> new_parent = nullptr;
if (is_frame) {
auto frame = ((PyUIFrameObject*)value)->data;
children_ptr = &frame->children;
new_parent = frame;
} else if (is_gridview) {
// #364: overlay children belong to the view itself, not to the grid data.
auto view = ((PyUIGridViewObject*)value)->data;
children_ptr = &view->children;
new_parent = view;
}
if (children_ptr && *children_ptr) {
// Check if already in this parent's collection (prevent duplicates)
bool already_present = false;
for (const auto& child : **children_ptr) {
if (child.get() == drawable.get()) {
already_present = true;
break;
}
}
if (!already_present) {
// Add to new parent's children
(*children_ptr)->push_back(drawable);
drawable->setParent(new_parent);
}
}
return 0;
}
// Python API - get global position (read-only)
PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
// #331: direct allocation fast path — no module import or type call per read
return PyVector(drawable->get_global_position()).pyObject();
}
// #138, #188 - Python API for bounds property - returns (pos, size) as pair of Vectors
PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
sf::FloatRect bounds = drawable->get_bounds();
// Get Vector type
PyObject* vector_type = (PyObject*)&mcrfpydef::PyVectorType;
// Create pos vector
PyObject* pos_args = Py_BuildValue("(ff)", bounds.left, bounds.top);
PyObject* pos = PyObject_CallObject(vector_type, pos_args);
Py_DECREF(pos_args);
if (!pos) {
return NULL;
}
// Create size vector
PyObject* size_args = Py_BuildValue("(ff)", bounds.width, bounds.height);
PyObject* size = PyObject_CallObject(vector_type, size_args);
Py_DECREF(size_args);
if (!size) {
Py_DECREF(pos);
return NULL;
}
// Return tuple of two vectors (N steals reference)
return Py_BuildValue("(NN)", pos, size);
}
// #138, #188 - Python API for global_bounds property - returns (pos, size) as pair of Vectors
PyObject* UIDrawable::get_global_bounds_py(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
sf::FloatRect bounds = drawable->get_global_bounds();
// Get Vector type
PyObject* vector_type = (PyObject*)&mcrfpydef::PyVectorType;
// Create pos vector
PyObject* pos_args = Py_BuildValue("(ff)", bounds.left, bounds.top);
PyObject* pos = PyObject_CallObject(vector_type, pos_args);
Py_DECREF(pos_args);
if (!pos) {
return NULL;
}
// Create size vector
PyObject* size_args = Py_BuildValue("(ff)", bounds.width, bounds.height);
PyObject* size = PyObject_CallObject(vector_type, size_args);
Py_DECREF(size_args);
if (!size) {
Py_DECREF(pos);
return NULL;
}
// Return tuple of two vectors (N steals reference)
return Py_BuildValue("(NN)", pos, size);
}
// #140 - Python API for on_enter property
PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
PyObject* ptr = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
if (((PyUIFrameObject*)self)->data->on_enter_callable)
ptr = ((PyUIFrameObject*)self)->data->on_enter_callable->borrow();
break;
case PyObjectsEnum::UICAPTION:
if (((PyUICaptionObject*)self)->data->on_enter_callable)
ptr = ((PyUICaptionObject*)self)->data->on_enter_callable->borrow();
break;
case PyObjectsEnum::UISPRITE:
if (((PyUISpriteObject*)self)->data->on_enter_callable)
ptr = ((PyUISpriteObject*)self)->data->on_enter_callable->borrow();
break;
case PyObjectsEnum::UILINE:
if (((PyUILineObject*)self)->data->on_enter_callable)
ptr = ((PyUILineObject*)self)->data->on_enter_callable->borrow();
break;
case PyObjectsEnum::UICIRCLE:
if (((PyUICircleObject*)self)->data->on_enter_callable)
ptr = ((PyUICircleObject*)self)->data->on_enter_callable->borrow();
break;
case PyObjectsEnum::UIARC:
if (((PyUIArcObject*)self)->data->on_enter_callable)
ptr = ((PyUIArcObject*)self)->data->on_enter_callable->borrow();
break;
case PyObjectsEnum::UIGRIDVIEW:
if (((PyUIGridViewObject*)self)->data->on_enter_callable)
ptr = ((PyUIGridViewObject*)self)->data->on_enter_callable->borrow();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter");
return NULL;
}
if (ptr && ptr != Py_None) {
Py_INCREF(ptr); // Return new reference, not borrowed
return ptr;
}
Py_RETURN_NONE;
}
int UIDrawable::set_on_enter(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* target = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
target = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
target = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
target = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UILINE:
target = ((PyUILineObject*)self)->data.get();
break;
case PyObjectsEnum::UICIRCLE:
target = ((PyUICircleObject*)self)->data.get();
break;
case PyObjectsEnum::UIARC:
target = ((PyUIArcObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRIDVIEW:
target = ((PyUIGridViewObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter");
return -1;
}
if (value == Py_None) {
target->on_enter_unregister();
} else {
target->on_enter_register(value);
}
return 0;
}
// #140 - Python API for on_exit property
PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
PyObject* ptr = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
if (((PyUIFrameObject*)self)->data->on_exit_callable)
ptr = ((PyUIFrameObject*)self)->data->on_exit_callable->borrow();
break;
case PyObjectsEnum::UICAPTION:
if (((PyUICaptionObject*)self)->data->on_exit_callable)
ptr = ((PyUICaptionObject*)self)->data->on_exit_callable->borrow();
break;
case PyObjectsEnum::UISPRITE:
if (((PyUISpriteObject*)self)->data->on_exit_callable)
ptr = ((PyUISpriteObject*)self)->data->on_exit_callable->borrow();
break;
case PyObjectsEnum::UILINE:
if (((PyUILineObject*)self)->data->on_exit_callable)
ptr = ((PyUILineObject*)self)->data->on_exit_callable->borrow();
break;
case PyObjectsEnum::UICIRCLE:
if (((PyUICircleObject*)self)->data->on_exit_callable)
ptr = ((PyUICircleObject*)self)->data->on_exit_callable->borrow();
break;
case PyObjectsEnum::UIARC:
if (((PyUIArcObject*)self)->data->on_exit_callable)
ptr = ((PyUIArcObject*)self)->data->on_exit_callable->borrow();
break;
case PyObjectsEnum::UIGRIDVIEW:
if (((PyUIGridViewObject*)self)->data->on_exit_callable)
ptr = ((PyUIGridViewObject*)self)->data->on_exit_callable->borrow();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit");
return NULL;
}
if (ptr && ptr != Py_None) {
Py_INCREF(ptr); // Return new reference, not borrowed
return ptr;
}
Py_RETURN_NONE;
}
int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* target = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
target = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
target = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
target = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UILINE:
target = ((PyUILineObject*)self)->data.get();
break;
case PyObjectsEnum::UICIRCLE:
target = ((PyUICircleObject*)self)->data.get();
break;
case PyObjectsEnum::UIARC:
target = ((PyUIArcObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRIDVIEW:
target = ((PyUIGridViewObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit");
return -1;
}
if (value == Py_None) {
target->on_exit_unregister();
} else {
target->on_exit_register(value);
}
return 0;
}
// #140 - Python API for hovered property (read-only)
PyObject* UIDrawable::get_hovered(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
return PyBool_FromLong(drawable->hovered);
}
// #141 - Python API for on_move property
PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
PyObject* ptr = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
if (((PyUIFrameObject*)self)->data->on_move_callable)
ptr = ((PyUIFrameObject*)self)->data->on_move_callable->borrow();
break;
case PyObjectsEnum::UICAPTION:
if (((PyUICaptionObject*)self)->data->on_move_callable)
ptr = ((PyUICaptionObject*)self)->data->on_move_callable->borrow();
break;
case PyObjectsEnum::UISPRITE:
if (((PyUISpriteObject*)self)->data->on_move_callable)
ptr = ((PyUISpriteObject*)self)->data->on_move_callable->borrow();
break;
case PyObjectsEnum::UILINE:
if (((PyUILineObject*)self)->data->on_move_callable)
ptr = ((PyUILineObject*)self)->data->on_move_callable->borrow();
break;
case PyObjectsEnum::UICIRCLE:
if (((PyUICircleObject*)self)->data->on_move_callable)
ptr = ((PyUICircleObject*)self)->data->on_move_callable->borrow();
break;
case PyObjectsEnum::UIARC:
if (((PyUIArcObject*)self)->data->on_move_callable)
ptr = ((PyUIArcObject*)self)->data->on_move_callable->borrow();
break;
case PyObjectsEnum::UIGRIDVIEW:
if (((PyUIGridViewObject*)self)->data->on_move_callable)
ptr = ((PyUIGridViewObject*)self)->data->on_move_callable->borrow();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move");
return NULL;
}
if (ptr && ptr != Py_None) {
Py_INCREF(ptr); // Return new reference, not borrowed
return ptr;
}
Py_RETURN_NONE;
}
int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* target = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
target = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
target = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
target = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UILINE:
target = ((PyUILineObject*)self)->data.get();
break;
case PyObjectsEnum::UICIRCLE:
target = ((PyUICircleObject*)self)->data.get();
break;
case PyObjectsEnum::UIARC:
target = ((PyUIArcObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRIDVIEW:
target = ((PyUIGridViewObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move");
return -1;
}
if (value == Py_None) {
target->on_move_unregister();
} else {
target->on_move_register(value);
}
return 0;
}
// Animation shorthand helper - creates and starts an animation on a UIDrawable
// This is a free function (not a member) to avoid incomplete type issues in UIBase.h template
PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", "conflict_mode", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
PyObject* easing_arg = Py_None;
int delta = 0;
int loop_val = 0;
PyObject* callback = nullptr;
const char* conflict_mode_str = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppOs", const_cast<char**>(keywords),
&property_name, &target_value, &duration,
&easing_arg, &delta, &loop_val, &callback, &conflict_mode_str)) {
return NULL;
}
// Validate property exists on this drawable
if (!self->hasProperty(property_name)) {
PyErr_Format(PyExc_ValueError,
"Property '%s' is not valid for animation on this object. "
"Check spelling or use a supported property name.",
property_name);
return NULL;
}
// Validate callback is callable if provided
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return NULL;
}
// Convert None to nullptr for C++
if (callback == Py_None) {
callback = nullptr;
}
// Convert Python target value to AnimationValue
AnimationValue animValue;
if (PyFloat_Check(target_value)) {
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
}
else if (PyLong_Check(target_value)) {
animValue = static_cast<int>(PyLong_AsLong(target_value));
}
else if (PyList_Check(target_value)) {
// List of integers for sprite animation
std::vector<int> indices;
Py_ssize_t size = PyList_Size(target_value);
for (Py_ssize_t i = 0; i < size; i++) {
PyObject* item = PyList_GetItem(target_value, i);
if (PyLong_Check(item)) {
indices.push_back(PyLong_AsLong(item));
} else {
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
return NULL;
}
}
animValue = indices;
}
else if (PyTuple_Check(target_value)) {
Py_ssize_t size = PyTuple_Size(target_value);
if (size == 2) {
// Vector2f
float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0));
float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1));
if (PyErr_Occurred()) return NULL;
animValue = sf::Vector2f(x, y);
}
else if (size == 3 || size == 4) {
// Color (RGB or RGBA)
int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0));
int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1));
int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2));
int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255;
if (PyErr_Occurred()) return NULL;
animValue = sf::Color(r, g, b, a);
}
else {
PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)");
return NULL;
}
}
else if (PyUnicode_Check(target_value)) {
// String for text animation
const char* str = PyUnicode_AsUTF8(target_value);
animValue = std::string(str);
}
else {
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
return NULL;
}
// Get easing function from argument
EasingFunction easingFunc;
if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) {
return NULL; // Error already set by from_arg
}
// Parse conflict mode
AnimationConflictMode conflict_mode = AnimationConflictMode::REPLACE;
if (conflict_mode_str) {
if (strcmp(conflict_mode_str, "replace") == 0) {
conflict_mode = AnimationConflictMode::REPLACE;
} else if (strcmp(conflict_mode_str, "queue") == 0) {
conflict_mode = AnimationConflictMode::QUEUE;
} else if (strcmp(conflict_mode_str, "error") == 0) {
conflict_mode = AnimationConflictMode::RAISE_ERROR;
} else {
PyErr_Format(PyExc_ValueError,
"Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", conflict_mode_str);
return NULL;
}
}
// Create the Animation
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
// Start on this drawable
animation->start(self);
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(animation, conflict_mode);
// Check if ERROR mode raised an exception
if (PyErr_Occurred()) {
return NULL;
}
// Create and return a PyAnimation wrapper
PyTypeObject* animType = &mcrfpydef::PyAnimationType;
PyAnimationObject* pyAnim = (PyAnimationObject*)animType->tp_alloc(animType, 0);
if (!pyAnim) {
return NULL;
}
pyAnim->data = animation;
return (PyObject*)pyAnim;
}
// ============================================================================
// Callback Cache Support (#184) - Python subclass method resolution
// ============================================================================
// Key for storing callback generation on Python type objects
static const char* CALLBACK_GEN_ATTR = "_mcrf_callback_gen";
uint32_t UIDrawable::getCallbackGeneration(PyObject* type) {
if (!type) return 0;
PyObject* gen = PyObject_GetAttrString(type, CALLBACK_GEN_ATTR);
if (gen) {
uint32_t result = static_cast<uint32_t>(PyLong_AsUnsignedLong(gen));
Py_DECREF(gen);
return result;
}
// No generation set yet - initialize to 0
PyErr_Clear();
return 0;
}
void UIDrawable::incrementCallbackGeneration(PyObject* type) {
if (!type) return;
uint32_t current = getCallbackGeneration(type);
PyObject* new_gen = PyLong_FromUnsignedLong(current + 1);
if (new_gen) {
PyObject_SetAttrString(type, CALLBACK_GEN_ATTR, new_gen);
Py_DECREF(new_gen);
}
PyErr_Clear(); // Clear any errors from SetAttr
}
bool UIDrawable::isCallbackCacheValid(PyObject* type) const {
if (!callback_cache.valid) return false;
return callback_cache.generation == getCallbackGeneration(type);
}
void UIDrawable::refreshCallbackCache(PyObject* pyObj) {
if (!pyObj) return;
PyObject* type = (PyObject*)Py_TYPE(pyObj);
// Update generation
callback_cache.generation = getCallbackGeneration(type);
callback_cache.valid = true;
// Check for each callback method
// We check the object (not just the class) to handle instance attributes too
// on_click
PyObject* attr = PyObject_GetAttrString(pyObj, "on_click");
callback_cache.has_on_click = (attr && PyCallable_Check(attr) && attr != Py_None);
Py_XDECREF(attr);
PyErr_Clear();
// on_enter
attr = PyObject_GetAttrString(pyObj, "on_enter");
callback_cache.has_on_enter = (attr && PyCallable_Check(attr) && attr != Py_None);
Py_XDECREF(attr);
PyErr_Clear();
// on_exit
attr = PyObject_GetAttrString(pyObj, "on_exit");
callback_cache.has_on_exit = (attr && PyCallable_Check(attr) && attr != Py_None);
Py_XDECREF(attr);
PyErr_Clear();
// on_move
attr = PyObject_GetAttrString(pyObj, "on_move");
callback_cache.has_on_move = (attr && PyCallable_Check(attr) && attr != Py_None);
Py_XDECREF(attr);
PyErr_Clear();
}
// ============================================================================
// Alignment System Implementation
// ============================================================================
void UIDrawable::applyAlignment() {
if (align_type == AlignmentType::NONE) return;
float pw, ph; // Parent width/height
auto p = parent.lock();
if (p) {
// Parent is another UIDrawable (Frame, Grid, etc.)
sf::FloatRect parent_bounds = p->get_bounds();
pw = parent_bounds.width;
ph = parent_bounds.height;
} else if (!parent_scene.empty()) {
// Parent is a Scene - use window's game resolution
GameEngine* game = McRFPy_API::game;
if (!game) return;
sf::Vector2u resolution = game->getGameResolution();
pw = static_cast<float>(resolution.x);
ph = static_cast<float>(resolution.y);
} else {
return; // No parent at all = can't align
}
sf::FloatRect self_bounds = get_bounds();
float cw = self_bounds.width, ch = self_bounds.height;
// Use specific margins if set (>= 0), otherwise inherit from general margin
// -1.0 means "inherit", any value >= 0 is an explicit override
float mx = (align_horiz_margin >= 0.0f) ? align_horiz_margin : align_margin;
float my = (align_vert_margin >= 0.0f) ? align_vert_margin : align_margin;
float x = 0, y = 0;
switch (align_type) {
case AlignmentType::TOP_LEFT:
x = mx;
y = my;
break;
case AlignmentType::TOP_CENTER:
x = (pw - cw) / 2.0f;
y = my;
break;
case AlignmentType::TOP_RIGHT:
x = pw - cw - mx;
y = my;
break;
case AlignmentType::CENTER_LEFT:
x = mx;
y = (ph - ch) / 2.0f;
break;
case AlignmentType::CENTER:
x = (pw - cw) / 2.0f;
y = (ph - ch) / 2.0f;
break;
case AlignmentType::CENTER_RIGHT:
x = pw - cw - mx;
y = (ph - ch) / 2.0f;
break;
case AlignmentType::BOTTOM_LEFT:
x = mx;
y = ph - ch - my;
break;
case AlignmentType::BOTTOM_CENTER:
x = (pw - cw) / 2.0f;
y = ph - ch - my;
break;
case AlignmentType::BOTTOM_RIGHT:
x = pw - cw - mx;
y = ph - ch - my;
break;
default:
return;
}
// For most drawables, position IS the bounding box top-left corner
// But for Circle and Arc, position is the center, so we need to adjust
float offset_x = 0.0f;
float offset_y = 0.0f;
// Check if this is a Circle or Arc (where position = center)
auto dtype = derived_type();
if (dtype == PyObjectsEnum::UICIRCLE || dtype == PyObjectsEnum::UIARC) {
// For these, position is the center, bounds.topLeft is position - radius
// So offset = position - bounds.topLeft = (radius, radius)
offset_x = position.x - self_bounds.left;
offset_y = position.y - self_bounds.top;
}
position = sf::Vector2f(x + offset_x, y + offset_y);
onPositionChanged();
markCompositeDirty();
}
void UIDrawable::setAlignment(AlignmentType align) {
align_type = align;
if (align != AlignmentType::NONE) {
applyAlignment();
}
}
void UIDrawable::realign() {
// Reapply alignment - useful for responsive layouts
if (align_type != AlignmentType::NONE) {
applyAlignment();
}
}
PyObject* UIDrawable::py_realign(PyObject* self, PyObject* args) {
PyObjectsEnum objtype = PyObjectsEnum::UIFRAME; // Default, will be set by type check
// Determine the type from the Python object
if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIFrameType)) objtype = PyObjectsEnum::UIFRAME;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICaptionType)) objtype = PyObjectsEnum::UICAPTION;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUISpriteType)) objtype = PyObjectsEnum::UISPRITE;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIGridViewType)) objtype = PyObjectsEnum::UIGRIDVIEW;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUILineType)) objtype = PyObjectsEnum::UILINE;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICircleType)) objtype = PyObjectsEnum::UICIRCLE;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIArcType)) objtype = PyObjectsEnum::UIARC;
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
drawable->realign();
Py_RETURN_NONE;
}
bool UIDrawable::validateMargins(AlignmentType align, float margin, float horiz_margin, float vert_margin, bool set_error) {
// Calculate effective margins (-1 means inherit from general margin)
float eff_horiz = (horiz_margin >= 0.0f) ? horiz_margin : margin;
float eff_vert = (vert_margin >= 0.0f) ? vert_margin : margin;
// CENTER alignment doesn't support any margins
if (align == AlignmentType::CENTER) {
if (margin != 0.0f || eff_horiz != 0.0f || eff_vert != 0.0f) {
if (set_error) {
PyErr_SetString(PyExc_ValueError,
"CENTER alignment does not support margins");
}
return false;
}
}
// Horizontally centered alignments don't support horiz_margin override
// (margin is applied vertically only)
if (align == AlignmentType::TOP_CENTER || align == AlignmentType::BOTTOM_CENTER) {
// If horiz_margin is explicitly set (not -1), it must be 0 or error
if (horiz_margin >= 0.0f && horiz_margin != 0.0f) {
if (set_error) {
PyErr_SetString(PyExc_ValueError,
"TOP_CENTER and BOTTOM_CENTER alignments do not support horiz_margin");
}
return false;
}
}
// Vertically centered alignments don't support vert_margin override
// (margin is applied horizontally only)
if (align == AlignmentType::CENTER_LEFT || align == AlignmentType::CENTER_RIGHT) {
// If vert_margin is explicitly set (not -1), it must be 0 or error
if (vert_margin >= 0.0f && vert_margin != 0.0f) {
if (set_error) {
PyErr_SetString(PyExc_ValueError,
"CENTER_LEFT and CENTER_RIGHT alignments do not support vert_margin");
}
return false;
}
}
return true;
}
// Python API: get align property
PyObject* UIDrawable::get_align(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
if (drawable->align_type == AlignmentType::NONE) {
Py_RETURN_NONE;
}
// Return Alignment enum member
if (!PyAlignment::alignment_enum_class) {
PyErr_SetString(PyExc_RuntimeError, "Alignment enum not initialized");
return NULL;
}
PyObject* value = PyLong_FromLong(static_cast<int>(drawable->align_type));
if (!value) return NULL;
PyObject* result = PyObject_CallFunctionObjArgs(PyAlignment::alignment_enum_class, value, NULL);
Py_DECREF(value);
return result;
}
// Python API: set align property
int UIDrawable::set_align(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
if (value == Py_None) {
drawable->align_type = AlignmentType::NONE;
return 0;
}
AlignmentType align;
if (!PyAlignment::from_arg(value, &align)) {
return -1;
}
// Validate margins for new alignment
if (!validateMargins(align, drawable->align_margin, drawable->align_horiz_margin, drawable->align_vert_margin)) {
return -1;
}
drawable->setAlignment(align);
return 0;
}
// Python API: get margin property
PyObject* UIDrawable::get_margin(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
return PyFloat_FromDouble(drawable->align_margin);
}
// Python API: set margin property
int UIDrawable::set_margin(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
float margin = 0.0f;
if (PyFloat_Check(value)) {
margin = static_cast<float>(PyFloat_AsDouble(value));
} else if (PyLong_Check(value)) {
margin = static_cast<float>(PyLong_AsLong(value));
} else {
PyErr_SetString(PyExc_TypeError, "margin must be a number");
return -1;
}
// Validate margins for current alignment
if (drawable->align_type != AlignmentType::NONE) {
if (!validateMargins(drawable->align_type, margin, drawable->align_horiz_margin, drawable->align_vert_margin)) {
return -1;
}
}
drawable->align_margin = margin;
if (drawable->align_type != AlignmentType::NONE) {
drawable->applyAlignment();
}
return 0;
}
// Python API: get horiz_margin property
PyObject* UIDrawable::get_horiz_margin(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
return PyFloat_FromDouble(drawable->align_horiz_margin);
}
// Python API: set horiz_margin property
int UIDrawable::set_horiz_margin(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
float horiz_margin = 0.0f;
if (PyFloat_Check(value)) {
horiz_margin = static_cast<float>(PyFloat_AsDouble(value));
} else if (PyLong_Check(value)) {
horiz_margin = static_cast<float>(PyLong_AsLong(value));
} else {
PyErr_SetString(PyExc_TypeError, "horiz_margin must be a number");
return -1;
}
// Validate margins for current alignment
if (drawable->align_type != AlignmentType::NONE) {
if (!validateMargins(drawable->align_type, drawable->align_margin, horiz_margin, drawable->align_vert_margin)) {
return -1;
}
}
drawable->align_horiz_margin = horiz_margin;
if (drawable->align_type != AlignmentType::NONE) {
drawable->applyAlignment();
}
return 0;
}
// Python API: get vert_margin property
PyObject* UIDrawable::get_vert_margin(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return NULL;
return PyFloat_FromDouble(drawable->align_vert_margin);
}
// Python API: set vert_margin property
int UIDrawable::set_vert_margin(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
float vert_margin = 0.0f;
if (PyFloat_Check(value)) {
vert_margin = static_cast<float>(PyFloat_AsDouble(value));
} else if (PyLong_Check(value)) {
vert_margin = static_cast<float>(PyLong_AsLong(value));
} else {
PyErr_SetString(PyExc_TypeError, "vert_margin must be a number");
return -1;
}
// Validate margins for current alignment
if (drawable->align_type != AlignmentType::NONE) {
if (!validateMargins(drawable->align_type, drawable->align_margin, drawable->align_horiz_margin, vert_margin)) {
return -1;
}
}
drawable->align_vert_margin = vert_margin;
if (drawable->align_type != AlignmentType::NONE) {
drawable->applyAlignment();
}
return 0;
}