Alignment: reactive or automatically calculated repositioning of UIDrawables on their parent
This commit is contained in:
parent
73230989ad
commit
4bf590749c
23 changed files with 1350 additions and 397 deletions
|
|
@ -13,6 +13,7 @@
|
|||
#include "PyFOV.h"
|
||||
#include "PyTransition.h"
|
||||
#include "PyEasing.h"
|
||||
#include "PyAlignment.h"
|
||||
#include "PyKey.h"
|
||||
#include "PyMouseButton.h"
|
||||
#include "PyInputState.h"
|
||||
|
|
@ -603,6 +604,13 @@ PyObject* PyInit_mcrfpy()
|
|||
PyErr_Clear();
|
||||
}
|
||||
|
||||
// Add Alignment enum class for automatic child positioning
|
||||
PyObject* alignment_class = PyAlignment::create_enum_class(m);
|
||||
if (!alignment_class) {
|
||||
// If enum creation fails, continue without it (non-fatal)
|
||||
PyErr_Clear();
|
||||
}
|
||||
|
||||
// Add automation submodule
|
||||
PyObject* automation_module = McRFPy_Automation::init_automation_module();
|
||||
if (automation_module != NULL) {
|
||||
|
|
|
|||
232
src/PyAlignment.cpp
Normal file
232
src/PyAlignment.cpp
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
#include "PyAlignment.h"
|
||||
#include "McRFPy_API.h"
|
||||
|
||||
// Static storage for cached enum class reference
|
||||
PyObject* PyAlignment::alignment_enum_class = nullptr;
|
||||
|
||||
// Alignment table - maps enum value to name
|
||||
struct AlignmentEntry {
|
||||
const char* name;
|
||||
int value;
|
||||
AlignmentType type;
|
||||
};
|
||||
|
||||
static const AlignmentEntry alignment_table[] = {
|
||||
{"TOP_LEFT", 0, AlignmentType::TOP_LEFT},
|
||||
{"TOP_CENTER", 1, AlignmentType::TOP_CENTER},
|
||||
{"TOP_RIGHT", 2, AlignmentType::TOP_RIGHT},
|
||||
{"CENTER_LEFT", 3, AlignmentType::CENTER_LEFT},
|
||||
{"CENTER", 4, AlignmentType::CENTER},
|
||||
{"CENTER_RIGHT", 5, AlignmentType::CENTER_RIGHT},
|
||||
{"BOTTOM_LEFT", 6, AlignmentType::BOTTOM_LEFT},
|
||||
{"BOTTOM_CENTER", 7, AlignmentType::BOTTOM_CENTER},
|
||||
{"BOTTOM_RIGHT", 8, AlignmentType::BOTTOM_RIGHT},
|
||||
};
|
||||
|
||||
// Legacy camelCase names (for backwards compatibility if desired)
|
||||
static const char* legacy_names[] = {
|
||||
"topLeft", "topCenter", "topRight",
|
||||
"centerLeft", "center", "centerRight",
|
||||
"bottomLeft", "bottomCenter", "bottomRight"
|
||||
};
|
||||
|
||||
static const int NUM_ALIGNMENT_ENTRIES = sizeof(alignment_table) / sizeof(alignment_table[0]);
|
||||
|
||||
const char* PyAlignment::alignment_name(AlignmentType value) {
|
||||
int idx = static_cast<int>(value);
|
||||
if (idx >= 0 && idx < NUM_ALIGNMENT_ENTRIES) {
|
||||
return alignment_table[idx].name;
|
||||
}
|
||||
return "NONE";
|
||||
}
|
||||
|
||||
PyObject* PyAlignment::create_enum_class(PyObject* module) {
|
||||
// Import IntEnum from enum module
|
||||
PyObject* enum_module = PyImport_ImportModule("enum");
|
||||
if (!enum_module) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum");
|
||||
Py_DECREF(enum_module);
|
||||
if (!int_enum) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create dict of enum members
|
||||
PyObject* members = PyDict_New();
|
||||
if (!members) {
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add all alignment members
|
||||
for (int i = 0; i < NUM_ALIGNMENT_ENTRIES; i++) {
|
||||
PyObject* value = PyLong_FromLong(alignment_table[i].value);
|
||||
if (!value) {
|
||||
Py_DECREF(members);
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
if (PyDict_SetItemString(members, alignment_table[i].name, value) < 0) {
|
||||
Py_DECREF(value);
|
||||
Py_DECREF(members);
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(value);
|
||||
}
|
||||
|
||||
// Call IntEnum("Alignment", members) to create the enum class
|
||||
PyObject* name = PyUnicode_FromString("Alignment");
|
||||
if (!name) {
|
||||
Py_DECREF(members);
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// IntEnum(name, members) using functional API
|
||||
PyObject* args = PyTuple_Pack(2, name, members);
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(members);
|
||||
if (!args) {
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* alignment_class = PyObject_Call(int_enum, args, NULL);
|
||||
Py_DECREF(args);
|
||||
Py_DECREF(int_enum);
|
||||
|
||||
if (!alignment_class) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Cache the reference for fast type checking
|
||||
alignment_enum_class = alignment_class;
|
||||
Py_INCREF(alignment_enum_class);
|
||||
|
||||
// Add docstring to the enum class
|
||||
static const char* alignment_doc =
|
||||
"Alignment enum for positioning UI elements relative to parent bounds.\n\n"
|
||||
"Values:\n"
|
||||
" TOP_LEFT, TOP_CENTER, TOP_RIGHT\n"
|
||||
" CENTER_LEFT, CENTER, CENTER_RIGHT\n"
|
||||
" BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT\n\n"
|
||||
"Margin Validation Rules:\n"
|
||||
" Margins define distance from parent edge when aligned.\n\n"
|
||||
" - CENTER: No margins allowed (raises ValueError if margin != 0)\n"
|
||||
" - TOP_CENTER, BOTTOM_CENTER: Only vert_margin applies (horiz_margin raises ValueError)\n"
|
||||
" - CENTER_LEFT, CENTER_RIGHT: Only horiz_margin applies (vert_margin raises ValueError)\n"
|
||||
" - Corner alignments (TOP_LEFT, etc.): All margins valid\n\n"
|
||||
"Properties:\n"
|
||||
" align: Alignment value or None to disable\n"
|
||||
" margin: General margin for all applicable edges\n"
|
||||
" horiz_margin: Override for horizontal edge (0 = use general margin)\n"
|
||||
" vert_margin: Override for vertical edge (0 = use general margin)\n\n"
|
||||
"Example:\n"
|
||||
" # Center a panel in the scene\n"
|
||||
" panel = Frame(size=(200, 100), align=Alignment.CENTER)\n"
|
||||
" scene.children.append(panel)\n\n"
|
||||
" # Place button in bottom-right with 10px margin\n"
|
||||
" button = Frame(size=(80, 30), align=Alignment.BOTTOM_RIGHT, margin=10)\n"
|
||||
" panel.children.append(button)";
|
||||
PyObject* doc = PyUnicode_FromString(alignment_doc);
|
||||
if (doc) {
|
||||
PyObject_SetAttrString(alignment_class, "__doc__", doc);
|
||||
Py_DECREF(doc);
|
||||
}
|
||||
|
||||
// Add to module
|
||||
if (PyModule_AddObject(module, "Alignment", alignment_class) < 0) {
|
||||
Py_DECREF(alignment_class);
|
||||
alignment_enum_class = nullptr;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return alignment_class;
|
||||
}
|
||||
|
||||
int PyAlignment::from_arg(PyObject* arg, AlignmentType* out_align, bool* was_none) {
|
||||
if (was_none) *was_none = false;
|
||||
|
||||
// Accept None -> NONE alignment (no alignment)
|
||||
if (arg == Py_None) {
|
||||
if (was_none) *was_none = true;
|
||||
*out_align = AlignmentType::NONE;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Accept Alignment enum member (check if it's an instance of our enum)
|
||||
if (alignment_enum_class && PyObject_IsInstance(arg, alignment_enum_class)) {
|
||||
// IntEnum members have a 'value' attribute
|
||||
PyObject* value = PyObject_GetAttrString(arg, "value");
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
long val = PyLong_AsLong(value);
|
||||
Py_DECREF(value);
|
||||
if (val == -1 && PyErr_Occurred()) {
|
||||
return 0;
|
||||
}
|
||||
if (val >= 0 && val < NUM_ALIGNMENT_ENTRIES) {
|
||||
*out_align = alignment_table[val].type;
|
||||
return 1;
|
||||
}
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Invalid Alignment value: %ld. Must be 0-%d.", val, NUM_ALIGNMENT_ENTRIES - 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Accept int (for direct enum value access)
|
||||
if (PyLong_Check(arg)) {
|
||||
long val = PyLong_AsLong(arg);
|
||||
if (val == -1 && PyErr_Occurred()) {
|
||||
return 0;
|
||||
}
|
||||
if (val >= 0 && val < NUM_ALIGNMENT_ENTRIES) {
|
||||
*out_align = alignment_table[val].type;
|
||||
return 1;
|
||||
}
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Invalid alignment value: %ld. Must be 0-%d or use mcrfpy.Alignment enum.",
|
||||
val, NUM_ALIGNMENT_ENTRIES - 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Accept string (for backwards compatibility)
|
||||
if (PyUnicode_Check(arg)) {
|
||||
const char* name = PyUnicode_AsUTF8(arg);
|
||||
if (!name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check legacy camelCase names first
|
||||
for (int i = 0; i < NUM_ALIGNMENT_ENTRIES; i++) {
|
||||
if (strcmp(name, legacy_names[i]) == 0) {
|
||||
*out_align = alignment_table[i].type;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check enum-style names (TOP_LEFT, CENTER, etc.)
|
||||
for (int i = 0; i < NUM_ALIGNMENT_ENTRIES; i++) {
|
||||
if (strcmp(name, alignment_table[i].name) == 0) {
|
||||
*out_align = alignment_table[i].type;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Build error message with available options
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Unknown alignment: '%s'. Use mcrfpy.Alignment enum (e.g., Alignment.CENTER) "
|
||||
"or string names: 'topLeft', 'topCenter', 'topRight', 'centerLeft', 'center', "
|
||||
"'centerRight', 'bottomLeft', 'bottomCenter', 'bottomRight'.",
|
||||
name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"Alignment must be mcrfpy.Alignment enum member, string, int, or None");
|
||||
return 0;
|
||||
}
|
||||
42
src/PyAlignment.h
Normal file
42
src/PyAlignment.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
|
||||
// Alignment type enum - used internally in C++
|
||||
enum class AlignmentType {
|
||||
NONE = -1, // No alignment (static positioning)
|
||||
TOP_LEFT = 0,
|
||||
TOP_CENTER = 1,
|
||||
TOP_RIGHT = 2,
|
||||
CENTER_LEFT = 3,
|
||||
CENTER = 4,
|
||||
CENTER_RIGHT = 5,
|
||||
BOTTOM_LEFT = 6,
|
||||
BOTTOM_CENTER = 7,
|
||||
BOTTOM_RIGHT = 8
|
||||
};
|
||||
|
||||
// Module-level Alignment enum class (created at runtime using Python's IntEnum)
|
||||
// Stored as a module attribute: mcrfpy.Alignment
|
||||
|
||||
class PyAlignment {
|
||||
public:
|
||||
// Create the Alignment enum class and add to module
|
||||
// Returns the enum class (new reference), or NULL on error
|
||||
static PyObject* create_enum_class(PyObject* module);
|
||||
|
||||
// Helper to extract alignment from Python arg
|
||||
// Accepts Alignment enum, string, int, or None
|
||||
// Returns 1 on success, 0 on error (with exception set)
|
||||
// If arg is None, sets *out_align to NONE and sets *was_none to true
|
||||
static int from_arg(PyObject* arg, AlignmentType* out_align, bool* was_none = nullptr);
|
||||
|
||||
// Convert alignment enum value to string name
|
||||
static const char* alignment_name(AlignmentType value);
|
||||
|
||||
// Cached reference to the Alignment enum class for fast type checking
|
||||
static PyObject* alignment_enum_class;
|
||||
|
||||
// Number of alignment options (excluding NONE)
|
||||
static const int NUM_ALIGNMENTS = 9;
|
||||
};
|
||||
|
|
@ -500,6 +500,30 @@ PyGetSetDef PySceneClass::getsetters[] = {
|
|||
{NULL}
|
||||
};
|
||||
|
||||
// Scene.realign() - recalculate alignment for all children
|
||||
static PyObject* PySceneClass_realign(PySceneObject* self, PyObject* args)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene || !scene->ui_elements) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Iterate through all UI elements and realign those with alignment set
|
||||
for (auto& drawable : *scene->ui_elements) {
|
||||
if (drawable && drawable->align_type != AlignmentType::NONE) {
|
||||
drawable->applyAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Methods
|
||||
PyMethodDef PySceneClass::methods[] = {
|
||||
{"activate", (PyCFunction)activate, METH_VARARGS | METH_KEYWORDS,
|
||||
|
|
@ -521,6 +545,13 @@ PyMethodDef PySceneClass::methods[] = {
|
|||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Alternative to setting on_key property. Handler is called for both key press and release events.")
|
||||
)},
|
||||
{"realign", (PyCFunction)PySceneClass_realign, METH_NOARGS,
|
||||
MCRF_METHOD(SceneClass, realign,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Recalculate alignment for all children with alignment set."),
|
||||
MCRF_NOTE("Call this after window resize or when game_resolution changes. "
|
||||
"For responsive layouts, connect this to on_resize callback.")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "UIArc.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyAlignment.h"
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
|
|
@ -206,6 +207,12 @@ void UIArc::resize(float w, float h) {
|
|||
vertices_dirty = true;
|
||||
}
|
||||
|
||||
void UIArc::onPositionChanged() {
|
||||
// Sync center from position (for alignment system)
|
||||
center = position;
|
||||
vertices_dirty = true;
|
||||
}
|
||||
|
||||
// Property setters
|
||||
bool UIArc::setProperty(const std::string& name, float value) {
|
||||
if (name == "radius") {
|
||||
|
|
@ -443,6 +450,7 @@ PyGetSetDef UIArc::getsetters[] = {
|
|||
"Position as a Vector (same as center).", (void*)PyObjectsEnum::UIARC},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIARC),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIARC),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -481,17 +489,23 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
|
|||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
PyObject* align_obj = nullptr; // Alignment enum or None
|
||||
float margin = 0.0f;
|
||||
float horiz_margin = -1.0f;
|
||||
float vert_margin = -1.0f;
|
||||
|
||||
static const char* kwlist[] = {
|
||||
"center", "radius", "start_angle", "end_angle", "color", "thickness",
|
||||
"on_click", "visible", "opacity", "z_index", "name",
|
||||
"align", "margin", "horiz_margin", "vert_margin",
|
||||
nullptr
|
||||
};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifiz", const_cast<char**>(kwlist),
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfff", const_cast<char**>(kwlist),
|
||||
¢er_obj, &radius, &start_angle, &end_angle,
|
||||
&color_obj, &thickness,
|
||||
&click_handler, &visible, &opacity, &z_index, &name)) {
|
||||
&click_handler, &visible, &opacity, &z_index, &name,
|
||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -546,6 +560,9 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
|
|||
self->data->name = name;
|
||||
}
|
||||
|
||||
// Process alignment arguments
|
||||
UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin);
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
|
|
|
|||
11
src/UIArc.h
11
src/UIArc.h
|
|
@ -76,6 +76,7 @@ public:
|
|||
sf::FloatRect get_bounds() const override;
|
||||
void move(float dx, float dy) override;
|
||||
void resize(float w, float h) override;
|
||||
void onPositionChanged() override;
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
|
|
@ -140,7 +141,11 @@ namespace mcrfpydef {
|
|||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" align (Alignment): Alignment relative to parent. Default: None\n"
|
||||
" margin (float): Margin from parent edge when aligned. Default: 0\n"
|
||||
" horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n"
|
||||
" vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n"
|
||||
"Attributes:\n"
|
||||
" center (Vector): Center position\n"
|
||||
" radius (float): Arc radius\n"
|
||||
|
|
@ -152,6 +157,10 @@ namespace mcrfpydef {
|
|||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" align (Alignment): Alignment relative to parent (or None)\n"
|
||||
" margin (float): General margin for alignment\n"
|
||||
" horiz_margin (float): Horizontal margin override\n"
|
||||
" vert_margin (float): Vertical margin override\n"
|
||||
),
|
||||
.tp_methods = UIArc_methods,
|
||||
.tp_getset = UIArc::getsetters,
|
||||
|
|
|
|||
52
src/UIBase.h
52
src/UIBase.h
|
|
@ -123,11 +123,38 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
|
|||
MCRF_RETURNS("Animation object for monitoring progress") \
|
||||
MCRF_RAISES("ValueError", "If property name is not valid for this drawable type") \
|
||||
MCRF_NOTE("This is a convenience method that creates an Animation, starts it, and adds it to the AnimationManager.") \
|
||||
)}, \
|
||||
{"realign", (PyCFunction)UIDrawable::py_realign, METH_NOARGS, \
|
||||
MCRF_METHOD(Drawable, realign, \
|
||||
MCRF_SIG("()", "None"), \
|
||||
MCRF_DESC("Reapply alignment relative to parent, useful for responsive layouts."), \
|
||||
MCRF_NOTE("Call this to recalculate position after parent changes size. " \
|
||||
"For elements with align=None, this has no effect.") \
|
||||
)}
|
||||
|
||||
// Legacy macro for backwards compatibility - same as UIDRAWABLE_METHODS
|
||||
#define UIDRAWABLE_METHODS_FULL UIDRAWABLE_METHODS
|
||||
|
||||
// Macro for handling alignment in constructors
|
||||
// Usage: UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin)
|
||||
// Returns -1 on error (suitable for use in tp_init functions)
|
||||
#define UIDRAWABLE_PROCESS_ALIGNMENT(self_data, align_obj, margin, horiz_margin, vert_margin) \
|
||||
do { \
|
||||
if ((align_obj) && (align_obj) != Py_None) { \
|
||||
AlignmentType _align; \
|
||||
if (!PyAlignment::from_arg(align_obj, &_align)) { \
|
||||
return -1; \
|
||||
} \
|
||||
if (!UIDrawable::validateMargins(_align, margin, horiz_margin, vert_margin)) { \
|
||||
return -1; \
|
||||
} \
|
||||
(self_data)->align_type = _align; \
|
||||
(self_data)->align_margin = margin; \
|
||||
(self_data)->align_horiz_margin = horiz_margin; \
|
||||
(self_data)->align_vert_margin = vert_margin; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// Property getters/setters for visible and opacity
|
||||
template<typename T>
|
||||
static PyObject* UIDrawable_get_visible(T* self, void* closure)
|
||||
|
|
@ -230,4 +257,29 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
|
|||
"Performance note: Called frequently during movement - keep handlers fast." \
|
||||
), (void*)type_enum}
|
||||
|
||||
// Alignment system - automatic positioning relative to parent bounds
|
||||
#define UIDRAWABLE_ALIGNMENT_GETSETTERS(type_enum) \
|
||||
{"align", (getter)UIDrawable::get_align, (setter)UIDrawable::set_align, \
|
||||
MCRF_PROPERTY(align, \
|
||||
"Alignment relative to parent bounds (Alignment enum or None). " \
|
||||
"When set, position is automatically calculated when parent is assigned or resized. " \
|
||||
"Set to None to disable alignment and use manual positioning." \
|
||||
), (void*)type_enum}, \
|
||||
{"margin", (getter)UIDrawable::get_margin, (setter)UIDrawable::set_margin, \
|
||||
MCRF_PROPERTY(margin, \
|
||||
"General margin from edge when aligned (float). " \
|
||||
"Applied to both horizontal and vertical edges unless overridden. " \
|
||||
"Invalid for CENTER alignment (raises ValueError)." \
|
||||
), (void*)type_enum}, \
|
||||
{"horiz_margin", (getter)UIDrawable::get_horiz_margin, (setter)UIDrawable::set_horiz_margin, \
|
||||
MCRF_PROPERTY(horiz_margin, \
|
||||
"Horizontal margin override (float, 0 = use general margin). " \
|
||||
"Invalid for vertically-centered alignments (TOP_CENTER, BOTTOM_CENTER, CENTER)." \
|
||||
), (void*)type_enum}, \
|
||||
{"vert_margin", (getter)UIDrawable::get_vert_margin, (setter)UIDrawable::set_vert_margin, \
|
||||
MCRF_PROPERTY(vert_margin, \
|
||||
"Vertical margin override (float, 0 = use general margin). " \
|
||||
"Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER)." \
|
||||
), (void*)type_enum}
|
||||
|
||||
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "PyVector.h"
|
||||
#include "PyFont.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyAlignment.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -310,6 +311,7 @@ PyGetSetDef UICaption::getsetters[] = {
|
|||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -350,21 +352,27 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
|
||||
PyObject* align_obj = nullptr; // Alignment enum or None
|
||||
float margin = 0.0f;
|
||||
float horiz_margin = -1.0f;
|
||||
float vert_margin = -1.0f;
|
||||
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "font", "text", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"fill_color", "outline_color", "outline", "font_size", "on_click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y",
|
||||
"align", "margin", "horiz_margin", "vert_margin",
|
||||
nullptr
|
||||
};
|
||||
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast<char**>(kwlist),
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizffOfff", const_cast<char**>(kwlist),
|
||||
&pos_obj, &font, &text, // Positional
|
||||
&fill_color, &outline_color, &outline, &font_size, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y)) {
|
||||
&visible, &opacity, &z_index, &name, &x, &y,
|
||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -463,7 +471,10 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
|
||||
// Process alignment arguments
|
||||
UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin);
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
|
|
@ -472,7 +483,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,11 @@ namespace mcrfpydef {
|
|||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n\n"
|
||||
" y (float): Y position override. Default: 0\n"
|
||||
" align (Alignment): Alignment relative to parent. Default: None\n"
|
||||
" margin (float): Margin from parent edge when aligned. Default: 0\n"
|
||||
" horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n"
|
||||
" vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n"
|
||||
"Attributes:\n"
|
||||
" text (str): The displayed text content\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
|
|
@ -105,7 +109,11 @@ namespace mcrfpydef {
|
|||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" w, h (float): Read-only computed size based on text and font"),
|
||||
" w, h (float): Read-only computed size based on text and font\n"
|
||||
" align (Alignment): Alignment relative to parent (or None)\n"
|
||||
" margin (float): General margin for alignment\n"
|
||||
" horiz_margin (float): Horizontal margin override\n"
|
||||
" vert_margin (float): Vertical margin override"),
|
||||
.tp_methods = UICaption_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
.tp_getset = UICaption::getsetters,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "PyVector.h"
|
||||
#include "PyColor.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyAlignment.h"
|
||||
#include <cmath>
|
||||
|
||||
UICircle::UICircle()
|
||||
|
|
@ -395,6 +396,7 @@ PyGetSetDef UICircle::getsetters[] = {
|
|||
"Position as a Vector (same as center).", (void*)PyObjectsEnum::UICIRCLE},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -418,7 +420,8 @@ PyObject* UICircle::repr(PyUICircleObject* self) {
|
|||
int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {
|
||||
"radius", "center", "fill_color", "outline_color", "outline",
|
||||
"on_click", "visible", "opacity", "z_index", "name", NULL
|
||||
"on_click", "visible", "opacity", "z_index", "name",
|
||||
"align", "margin", "horiz_margin", "vert_margin", NULL
|
||||
};
|
||||
|
||||
float radius = 10.0f;
|
||||
|
|
@ -433,10 +436,15 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
|
|||
float opacity_val = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = NULL;
|
||||
PyObject* align_obj = NULL; // Alignment enum or None
|
||||
float margin = 0.0f;
|
||||
float horiz_margin = -1.0f;
|
||||
float vert_margin = -1.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfis", (char**)kwlist,
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfisOfff", (char**)kwlist,
|
||||
&radius, ¢er_obj, &fill_color_obj, &outline_color_obj, &outline,
|
||||
&click_obj, &visible, &opacity_val, &z_index, &name)) {
|
||||
&click_obj, &visible, &opacity_val, &z_index, &name,
|
||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -512,6 +520,9 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
|
|||
self->data->name = name;
|
||||
}
|
||||
|
||||
// Process alignment arguments
|
||||
UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin);
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
|
|
|
|||
|
|
@ -129,7 +129,11 @@ namespace mcrfpydef {
|
|||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" align (Alignment): Alignment relative to parent. Default: None\n"
|
||||
" margin (float): Margin from parent edge when aligned. Default: 0\n"
|
||||
" horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n"
|
||||
" vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n"
|
||||
"Attributes:\n"
|
||||
" radius (float): Circle radius\n"
|
||||
" center (Vector): Center position\n"
|
||||
|
|
@ -140,6 +144,10 @@ namespace mcrfpydef {
|
|||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" align (Alignment): Alignment relative to parent (or None)\n"
|
||||
" margin (float): General margin for alignment\n"
|
||||
" horiz_margin (float): Horizontal margin override\n"
|
||||
" vert_margin (float): Vertical margin override\n"
|
||||
),
|
||||
.tp_methods = UICircle_methods,
|
||||
.tp_getset = UICircle::getsetters,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,30 @@
|
|||
#include "PyEasing.h"
|
||||
#include "PySceneObject.h" // #183: For scene parent lookup
|
||||
|
||||
// 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::UIGRID:
|
||||
return ((PyUIGridObject*)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();
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
|
||||
|
||||
UIDrawable::UIDrawable(const UIDrawable& other)
|
||||
|
|
@ -306,68 +330,16 @@ void UIDrawable::on_move_unregister()
|
|||
|
||||
PyObject* UIDrawable::get_int(PyObject* self, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return -1;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
|
||||
|
|
@ -406,68 +378,16 @@ void UIDrawable::notifyZIndexChanged() {
|
|||
|
||||
PyObject* UIDrawable::get_name(PyObject* self, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return -1;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
if (value == NULL || value == Py_None) {
|
||||
drawable->name = "";
|
||||
|
|
@ -524,34 +444,8 @@ void UIDrawable::updateRenderTexture() {
|
|||
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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
switch (member) {
|
||||
case 0: // x
|
||||
|
|
@ -571,34 +465,8 @@ PyObject* UIDrawable::get_float_member(PyObject* self, void* closure) {
|
|||
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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return -1;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
float val = 0.0f;
|
||||
if (PyFloat_Check(value)) {
|
||||
|
|
@ -640,34 +508,8 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure)
|
|||
|
||||
PyObject* UIDrawable::get_pos(PyObject* self, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
// Create a Python Vector object from position
|
||||
PyObject* module = PyImport_ImportModule("mcrfpy");
|
||||
|
|
@ -687,34 +529,8 @@ PyObject* UIDrawable::get_pos(PyObject* self, void* closure) {
|
|||
|
||||
int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return -1;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
// Accept tuple or Vector
|
||||
float x, y;
|
||||
|
|
@ -766,11 +582,21 @@ int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
|
|||
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 {
|
||||
|
|
@ -893,34 +719,8 @@ void UIDrawable::markDirty() {
|
|||
// 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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
// #183: Check for scene parent first
|
||||
if (!drawable->parent_scene.empty()) {
|
||||
|
|
@ -1126,34 +926,8 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
|
|||
// 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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
sf::Vector2f global_pos = drawable->get_global_position();
|
||||
|
||||
|
|
@ -1176,34 +950,8 @@ PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) {
|
|||
// #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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
sf::FloatRect bounds = drawable->get_bounds();
|
||||
|
||||
|
|
@ -1237,34 +985,8 @@ PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) {
|
|||
// #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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
sf::FloatRect bounds = drawable->get_global_bounds();
|
||||
|
||||
|
|
@ -1464,34 +1186,8 @@ int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) {
|
|||
// #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 = nullptr;
|
||||
|
||||
switch (objtype) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICAPTION:
|
||||
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
drawable = ((PyUICircleObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIARC:
|
||||
drawable = ((PyUIArcObject*)self)->data.get();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||
return NULL;
|
||||
}
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
return PyBool_FromLong(drawable->hovered);
|
||||
}
|
||||
|
|
@ -1804,3 +1500,358 @@ void UIDrawable::refreshCallbackCache(PyObject* pyObj) {
|
|||
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
|
||||
PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
|
||||
PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
|
||||
PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
|
||||
PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
||||
PyObject* line_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line");
|
||||
PyObject* circle_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle");
|
||||
PyObject* arc_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc");
|
||||
|
||||
if (PyObject_IsInstance(self, frame_type)) objtype = PyObjectsEnum::UIFRAME;
|
||||
else if (PyObject_IsInstance(self, caption_type)) objtype = PyObjectsEnum::UICAPTION;
|
||||
else if (PyObject_IsInstance(self, sprite_type)) objtype = PyObjectsEnum::UISPRITE;
|
||||
else if (PyObject_IsInstance(self, grid_type)) objtype = PyObjectsEnum::UIGRID;
|
||||
else if (PyObject_IsInstance(self, line_type)) objtype = PyObjectsEnum::UILINE;
|
||||
else if (PyObject_IsInstance(self, circle_type)) objtype = PyObjectsEnum::UICIRCLE;
|
||||
else if (PyObject_IsInstance(self, arc_type)) objtype = PyObjectsEnum::UIARC;
|
||||
|
||||
Py_XDECREF(frame_type);
|
||||
Py_XDECREF(caption_type);
|
||||
Py_XDECREF(sprite_type);
|
||||
Py_XDECREF(grid_type);
|
||||
Py_XDECREF(line_type);
|
||||
Py_XDECREF(circle_type);
|
||||
Py_XDECREF(arc_type);
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFont.h"
|
||||
#include "PyAlignment.h"
|
||||
|
||||
#include "Resources.h"
|
||||
#include "UIBase.h"
|
||||
|
|
@ -133,7 +134,39 @@ public:
|
|||
// Python API for hit testing (#138)
|
||||
static PyObject* get_bounds_py(PyObject* self, void* closure);
|
||||
static PyObject* get_global_bounds_py(PyObject* self, void* closure);
|
||||
|
||||
|
||||
// Alignment system - position children relative to parent bounds
|
||||
AlignmentType align_type = AlignmentType::NONE;
|
||||
float align_margin = 0.0f; // General margin for all edges
|
||||
float align_horiz_margin = -1.0f; // Horizontal margin override (-1 = use align_margin)
|
||||
float align_vert_margin = -1.0f; // Vertical margin override (-1 = use align_margin)
|
||||
|
||||
// Apply alignment: recalculate position from parent bounds
|
||||
void applyAlignment();
|
||||
|
||||
// User-callable realignment: reapply alignment to self (useful for responsive layouts)
|
||||
void realign();
|
||||
|
||||
// Python API: realign method
|
||||
static PyObject* py_realign(PyObject* self, PyObject* args);
|
||||
|
||||
// Setters that trigger realignment
|
||||
void setAlignment(AlignmentType align);
|
||||
AlignmentType getAlignment() const { return align_type; }
|
||||
|
||||
// Python API for alignment properties
|
||||
static PyObject* get_align(PyObject* self, void* closure);
|
||||
static int set_align(PyObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_margin(PyObject* self, void* closure);
|
||||
static int set_margin(PyObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_horiz_margin(PyObject* self, void* closure);
|
||||
static int set_horiz_margin(PyObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_vert_margin(PyObject* self, void* closure);
|
||||
static int set_vert_margin(PyObject* self, PyObject* value, void* closure);
|
||||
|
||||
// Validate margin settings (raises ValueError for invalid combinations)
|
||||
static bool validateMargins(AlignmentType align, float margin, float horiz_margin, float vert_margin, bool set_error = true);
|
||||
|
||||
// New properties for Phase 1
|
||||
bool visible = true; // #87 - visibility flag
|
||||
float opacity = 1.0f; // #88 - opacity (0.0 = transparent, 1.0 = opaque)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyAlignment.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||
|
|
@ -84,6 +85,15 @@ void UIFrame::move(float dx, float dy)
|
|||
void UIFrame::resize(float w, float h)
|
||||
{
|
||||
box.setSize(sf::Vector2f(w, h));
|
||||
|
||||
// Notify aligned children to recalculate their positions
|
||||
if (children) {
|
||||
for (auto& child : *children) {
|
||||
if (child->getAlignment() != AlignmentType::NONE) {
|
||||
child->applyAlignment();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UIFrame::onPositionChanged()
|
||||
|
|
@ -458,6 +468,7 @@ PyGetSetDef UIFrame::getsetters[] = {
|
|||
{"cache_subtree", (getter)UIFrame::get_cache_subtree, (setter)UIFrame::set_cache_subtree, "#144: Cache subtree rendering to texture for performance", NULL},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -504,6 +515,10 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
int clip_children = 0;
|
||||
int cache_subtree = 0; // #144: texture caching
|
||||
PyObject* align_obj = nullptr; // Alignment enum or None
|
||||
float margin = 0.0f;
|
||||
float horiz_margin = -1.0f;
|
||||
float vert_margin = -1.0f;
|
||||
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
|
|
@ -511,14 +526,16 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
// Keyword-only args
|
||||
"fill_color", "outline_color", "outline", "children", "on_click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree",
|
||||
"align", "margin", "horiz_margin", "vert_margin",
|
||||
nullptr
|
||||
};
|
||||
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffii", const_cast<char**>(kwlist),
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfff", const_cast<char**>(kwlist),
|
||||
&pos_obj, &size_obj, // Positional
|
||||
&fill_color, &outline_color, &outline, &children_arg, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree)) {
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree,
|
||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -617,6 +634,9 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Process alignment arguments
|
||||
UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin);
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
|
|
@ -673,6 +693,7 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
Py_DECREF(grid_type);
|
||||
|
||||
if (drawable) {
|
||||
drawable->setParent(self->data); // Set parent before adding (enables alignment)
|
||||
self->data->children->push_back(drawable);
|
||||
self->data->children_need_sort = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,11 @@ namespace mcrfpydef {
|
|||
" w (float): Width override. Default: 0\n"
|
||||
" h (float): Height override. Default: 0\n"
|
||||
" clip_children (bool): Whether to clip children to frame bounds. Default: False\n"
|
||||
" cache_subtree (bool): Cache rendering to texture for performance. Default: False\n\n"
|
||||
" cache_subtree (bool): Cache rendering to texture for performance. Default: False\n"
|
||||
" align (Alignment): Alignment relative to parent. Default: None (manual positioning)\n"
|
||||
" margin (float): Margin from parent edge when aligned. Default: 0\n"
|
||||
" horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n"
|
||||
" vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" w, h (float): Size in pixels\n"
|
||||
|
|
@ -129,7 +133,11 @@ namespace mcrfpydef {
|
|||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" clip_children (bool): Whether to clip children to frame bounds\n"
|
||||
" cache_subtree (bool): Cache subtree rendering to texture"),
|
||||
" cache_subtree (bool): Cache subtree rendering to texture\n"
|
||||
" align (Alignment): Alignment relative to parent (or None)\n"
|
||||
" margin (float): General margin for alignment\n"
|
||||
" horiz_margin (float): Horizontal margin override\n"
|
||||
" vert_margin (float): Vertical margin override"),
|
||||
.tp_methods = UIFrame_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
.tp_getset = UIFrame::getsetters,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyAlignment.h"
|
||||
#include "PyTypeCache.h" // Thread-safe cached Python types
|
||||
#include "UIEntity.h"
|
||||
#include "Profiler.h"
|
||||
|
|
@ -496,6 +497,15 @@ void UIGrid::resize(float w, float h)
|
|||
renderTexture.create(static_cast<unsigned int>(w), static_cast<unsigned int>(h));
|
||||
output.setTexture(renderTexture.getTexture());
|
||||
}
|
||||
|
||||
// Notify aligned children to recalculate their positions
|
||||
if (children) {
|
||||
for (auto& child : *children) {
|
||||
if (child->getAlignment() != AlignmentType::NONE) {
|
||||
child->applyAlignment();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UIGrid::onPositionChanged()
|
||||
|
|
@ -674,6 +684,10 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
int grid_w = 2, grid_h = 2; // Default to 2x2 grid
|
||||
PyObject* align_obj = nullptr; // Alignment enum or None
|
||||
float margin = 0.0f;
|
||||
float horiz_margin = -1.0f;
|
||||
float vert_margin = -1.0f;
|
||||
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
|
|
@ -682,15 +696,17 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
"fill_color", "on_click", "center_x", "center_y", "zoom",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_w", "grid_h",
|
||||
"layers", // #150 - layers dict parameter
|
||||
"align", "margin", "horiz_margin", "vert_margin",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffiiO", const_cast<char**>(kwlist),
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffiiOOfff", const_cast<char**>(kwlist),
|
||||
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
|
||||
&fill_color, &click_handler, ¢er_x, ¢er_y, &zoom,
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_w, &grid_h,
|
||||
&layers_obj)) {
|
||||
&layers_obj,
|
||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -813,7 +829,10 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
|
||||
// Process alignment arguments
|
||||
UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin);
|
||||
|
||||
// Handle fill_color
|
||||
if (fill_color && fill_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||
|
|
@ -2188,6 +2207,7 @@ PyGetSetDef UIGrid::getsetters[] = {
|
|||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
// #142 - Grid cell mouse events
|
||||
{"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter,
|
||||
"Callback when mouse enters a grid cell. Called with (cell_pos: Vector).", NULL},
|
||||
|
|
|
|||
12
src/UIGrid.h
12
src/UIGrid.h
|
|
@ -258,7 +258,11 @@ namespace mcrfpydef {
|
|||
" w (float): Width override. Default: auto-calculated\n"
|
||||
" h (float): Height override. Default: auto-calculated\n"
|
||||
" grid_w (int): Grid width override. Default: 2\n"
|
||||
" grid_h (int): Grid height override. Default: 2\n\n"
|
||||
" grid_h (int): Grid height override. Default: 2\n"
|
||||
" align (Alignment): Alignment relative to parent. Default: None\n"
|
||||
" margin (float): Margin from parent edge when aligned. Default: 0\n"
|
||||
" horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n"
|
||||
" vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" w, h (float): Size in pixels\n"
|
||||
|
|
@ -277,7 +281,11 @@ namespace mcrfpydef {
|
|||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name"),
|
||||
" name (str): Element name\n"
|
||||
" align (Alignment): Alignment relative to parent (or None)\n"
|
||||
" margin (float): General margin for alignment\n"
|
||||
" horiz_margin (float): Horizontal margin override\n"
|
||||
" vert_margin (float): Vertical margin override"),
|
||||
.tp_methods = UIGrid_all_methods,
|
||||
//.tp_members = UIGrid::members,
|
||||
.tp_getset = UIGrid::getsetters,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "PyVector.h"
|
||||
#include "PyColor.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyAlignment.h"
|
||||
#include <cmath>
|
||||
|
||||
UILine::UILine()
|
||||
|
|
@ -465,6 +466,7 @@ PyGetSetDef UILine::getsetters[] = {
|
|||
(void*)PyObjectsEnum::UILINE},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UILINE),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UILINE),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -497,16 +499,22 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
|
|||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
PyObject* align_obj = nullptr; // Alignment enum or None
|
||||
float margin = 0.0f;
|
||||
float horiz_margin = -1.0f;
|
||||
float vert_margin = -1.0f;
|
||||
|
||||
static const char* kwlist[] = {
|
||||
"start", "end", "thickness", "color",
|
||||
"on_click", "visible", "opacity", "z_index", "name",
|
||||
"align", "margin", "horiz_margin", "vert_margin",
|
||||
nullptr
|
||||
};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifiz", const_cast<char**>(kwlist),
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfff", const_cast<char**>(kwlist),
|
||||
&start_obj, &end_obj, &thickness, &color_obj,
|
||||
&click_handler, &visible, &opacity, &z_index, &name)) {
|
||||
&click_handler, &visible, &opacity, &z_index, &name,
|
||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -565,6 +573,9 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
|
|||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Process alignment arguments
|
||||
UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin);
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
|
|
|
|||
10
src/UILine.h
10
src/UILine.h
|
|
@ -125,7 +125,11 @@ namespace mcrfpydef {
|
|||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" align (Alignment): Alignment relative to parent. Default: None\n"
|
||||
" margin (float): Margin from parent edge when aligned. Default: 0\n"
|
||||
" horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n"
|
||||
" vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n"
|
||||
"Attributes:\n"
|
||||
" start (Vector): Starting point\n"
|
||||
" end (Vector): Ending point\n"
|
||||
|
|
@ -135,6 +139,10 @@ namespace mcrfpydef {
|
|||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" align (Alignment): Alignment relative to parent (or None)\n"
|
||||
" margin (float): General margin for alignment\n"
|
||||
" horiz_margin (float): Horizontal margin override\n"
|
||||
" vert_margin (float): Vertical margin override\n"
|
||||
),
|
||||
.tp_methods = UILine_methods,
|
||||
.tp_getset = UILine::getsetters,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "PyVector.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "UIFrame.h" // #144: For snapshot= parameter
|
||||
#include "PyAlignment.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||
|
|
@ -355,6 +356,7 @@ PyGetSetDef UISprite::getsetters[] = {
|
|||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UISPRITE},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -388,6 +390,10 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
PyObject* snapshot = nullptr; // #144: snapshot parameter
|
||||
PyObject* align_obj = nullptr; // Alignment enum or None
|
||||
float margin = 0.0f;
|
||||
float horiz_margin = -1.0f;
|
||||
float vert_margin = -1.0f;
|
||||
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
|
|
@ -395,14 +401,16 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
// Keyword-only args
|
||||
"scale", "scale_x", "scale_y", "on_click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "snapshot",
|
||||
"align", "margin", "horiz_margin", "vert_margin",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffO", const_cast<char**>(kwlist),
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfff", const_cast<char**>(kwlist),
|
||||
&pos_obj, &texture, &sprite_index, // Positional
|
||||
&scale, &scale_x, &scale_y, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &snapshot)) {
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &snapshot,
|
||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -512,6 +520,9 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Process alignment arguments
|
||||
UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin);
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
|
|
|
|||
|
|
@ -121,7 +121,11 @@ namespace mcrfpydef {
|
|||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n\n"
|
||||
" y (float): Y position override. Default: 0\n"
|
||||
" align (Alignment): Alignment relative to parent. Default: None\n"
|
||||
" margin (float): Margin from parent edge when aligned. Default: 0\n"
|
||||
" horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n"
|
||||
" vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
|
|
@ -134,7 +138,11 @@ namespace mcrfpydef {
|
|||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" w, h (float): Read-only computed size based on texture and scale"),
|
||||
" w, h (float): Read-only computed size based on texture and scale\n"
|
||||
" align (Alignment): Alignment relative to parent (or None)\n"
|
||||
" margin (float): General margin for alignment\n"
|
||||
" horiz_margin (float): Horizontal margin override\n"
|
||||
" vert_margin (float): Vertical margin override"),
|
||||
.tp_methods = UISprite_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
.tp_getset = UISprite::getsetters,
|
||||
|
|
|
|||
131
tests/unit/alignment_constructor_test.py
Normal file
131
tests/unit/alignment_constructor_test.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test alignment constructor arguments work correctly."""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test that alignment args work in constructors
|
||||
|
||||
print("Test 1: Frame with align constructor arg...")
|
||||
parent = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
child = mcrfpy.Frame(size=(100, 50), align=mcrfpy.Alignment.CENTER)
|
||||
parent.children.append(child)
|
||||
# Expected: (400-100)/2=150, (300-50)/2=125
|
||||
if abs(child.x - 150) < 0.1 and abs(child.y - 125) < 0.1:
|
||||
print(" PASS: Frame align constructor arg works")
|
||||
else:
|
||||
print(f" FAIL: Expected (150, 125), got ({child.x}, {child.y})")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 2: Frame with align and margin constructor args...")
|
||||
parent2 = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
child2 = mcrfpy.Frame(size=(50, 50), align=mcrfpy.Alignment.TOP_LEFT, margin=10)
|
||||
parent2.children.append(child2)
|
||||
if abs(child2.x - 10) < 0.1 and abs(child2.y - 10) < 0.1:
|
||||
print(" PASS: Frame margin constructor arg works")
|
||||
else:
|
||||
print(f" FAIL: Expected (10, 10), got ({child2.x}, {child2.y})")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 3: Caption with align constructor arg...")
|
||||
parent3 = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
cap = mcrfpy.Caption(text="Test", align=mcrfpy.Alignment.TOP_CENTER, margin=20)
|
||||
parent3.children.append(cap)
|
||||
# Should be centered horizontally, 20px from top
|
||||
if abs(cap.y - 20) < 0.1:
|
||||
print(" PASS: Caption align constructor arg works")
|
||||
else:
|
||||
print(f" FAIL: Expected y=20, got y={cap.y}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 4: Sprite with align constructor arg...")
|
||||
parent4 = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
spr = mcrfpy.Sprite(align=mcrfpy.Alignment.BOTTOM_LEFT, margin=5)
|
||||
parent4.children.append(spr)
|
||||
if abs(spr.x - 5) < 0.1:
|
||||
print(" PASS: Sprite align constructor arg works")
|
||||
else:
|
||||
print(f" FAIL: Expected x=5, got x={spr.x}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 5: Grid with align constructor arg...")
|
||||
parent5 = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), size=(200, 200), align=mcrfpy.Alignment.CENTER_RIGHT, margin=15)
|
||||
parent5.children.append(grid)
|
||||
# Expected x: 400-200-15=185
|
||||
if abs(grid.x - 185) < 0.1:
|
||||
print(" PASS: Grid align constructor arg works")
|
||||
else:
|
||||
print(f" FAIL: Expected x=185, got x={grid.x}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 6: Line with align constructor arg...")
|
||||
parent6 = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
line = mcrfpy.Line(start=(0, 0), end=(50, 0), align=mcrfpy.Alignment.TOP_LEFT, margin=25)
|
||||
parent6.children.append(line)
|
||||
# Line's position (pos) should be at margin
|
||||
if abs(line.pos.x - 25) < 0.1 and abs(line.pos.y - 25) < 0.1:
|
||||
print(" PASS: Line align constructor arg works")
|
||||
else:
|
||||
print(f" FAIL: Expected pos at (25, 25), got ({line.pos.x}, {line.pos.y})")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 7: Circle with align constructor arg...")
|
||||
parent7 = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
circ = mcrfpy.Circle(radius=30, align=mcrfpy.Alignment.CENTER)
|
||||
parent7.children.append(circ)
|
||||
# Circle is centered, center.x should be at parent center (400/2=200), center.y at (300/2=150)
|
||||
if abs(circ.center.x - 200) < 0.1 and abs(circ.center.y - 150) < 0.1:
|
||||
print(" PASS: Circle align constructor arg works")
|
||||
else:
|
||||
print(f" FAIL: Expected center at (200, 150), got ({circ.center.x}, {circ.center.y})")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 8: Arc with align constructor arg...")
|
||||
parent8 = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
arc = mcrfpy.Arc(radius=40, align=mcrfpy.Alignment.BOTTOM_CENTER, vert_margin=10)
|
||||
parent8.children.append(arc)
|
||||
# Arc is BOTTOM_CENTER aligned with 10px vert_margin
|
||||
# Arc bounds: width=2*radius=80, height=2*radius=80
|
||||
# center.x should be 400/2=200 (centered)
|
||||
# For bottom alignment: bottom of arc = 300-10 = 290, so center.y = 290 - 40 = 250
|
||||
if abs(arc.center.x - 200) < 1.0 and abs(arc.center.y - 250) < 1.0:
|
||||
print(" PASS: Arc align constructor arg works")
|
||||
else:
|
||||
print(f" FAIL: Expected center at (200, 250), got ({arc.center.x}, {arc.center.y})")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 9: Testing horiz_margin and vert_margin separately...")
|
||||
parent9 = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
||||
frame9 = mcrfpy.Frame(size=(100, 50), align=mcrfpy.Alignment.TOP_RIGHT, horiz_margin=30, vert_margin=20)
|
||||
parent9.children.append(frame9)
|
||||
# Expected: x = 400-100-30=270, y = 20
|
||||
if abs(frame9.x - 270) < 0.1 and abs(frame9.y - 20) < 0.1:
|
||||
print(" PASS: horiz_margin and vert_margin constructor args work")
|
||||
else:
|
||||
print(f" FAIL: Expected (270, 20), got ({frame9.x}, {frame9.y})")
|
||||
sys.exit(1)
|
||||
|
||||
print("Test 10: Nested children with alignment in constructor list...")
|
||||
outer = mcrfpy.Frame(
|
||||
pos=(100, 100),
|
||||
size=(400, 300),
|
||||
children=[
|
||||
mcrfpy.Frame(size=(200, 100), align=mcrfpy.Alignment.CENTER),
|
||||
mcrfpy.Caption(text="Title", align=mcrfpy.Alignment.TOP_CENTER, margin=10),
|
||||
]
|
||||
)
|
||||
# Check inner frame is centered
|
||||
inner = outer.children[0]
|
||||
# (400-200)/2=100, (300-100)/2=100
|
||||
if abs(inner.x - 100) < 0.1 and abs(inner.y - 100) < 0.1:
|
||||
print(" PASS: Nested children alignment works in constructor list")
|
||||
else:
|
||||
print(f" FAIL: Expected inner at (100, 100), got ({inner.x}, {inner.y})")
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
print("=" * 50)
|
||||
print("All alignment constructor tests PASSED!")
|
||||
print("=" * 50)
|
||||
sys.exit(0)
|
||||
214
tests/unit/alignment_test.py
Normal file
214
tests/unit/alignment_test.py
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
"""Test the alignment system for UIDrawable elements."""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test 1: Check Alignment enum exists and has expected values
|
||||
print("Test 1: Checking Alignment enum...")
|
||||
try:
|
||||
assert hasattr(mcrfpy, 'Alignment'), "Alignment enum not found"
|
||||
|
||||
# Check all alignment values exist
|
||||
expected_alignments = [
|
||||
'TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT',
|
||||
'CENTER_LEFT', 'CENTER', 'CENTER_RIGHT',
|
||||
'BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT'
|
||||
]
|
||||
for name in expected_alignments:
|
||||
assert hasattr(mcrfpy.Alignment, name), f"Alignment.{name} not found"
|
||||
print(" PASS: Alignment enum has all expected values")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 2: Check that align property exists on Frame
|
||||
print("Test 2: Checking align property on Frame...")
|
||||
try:
|
||||
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
||||
|
||||
# Default alignment should be None
|
||||
assert frame.align is None, f"Expected align=None, got {frame.align}"
|
||||
|
||||
# Set alignment
|
||||
frame.align = mcrfpy.Alignment.CENTER
|
||||
assert frame.align == mcrfpy.Alignment.CENTER, f"Expected CENTER, got {frame.align}"
|
||||
|
||||
# Set back to None
|
||||
frame.align = None
|
||||
assert frame.align is None, f"Expected None, got {frame.align}"
|
||||
print(" PASS: align property works on Frame")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 3: Check margin properties exist
|
||||
print("Test 3: Checking margin properties...")
|
||||
try:
|
||||
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||
|
||||
# Check default margins are 0
|
||||
assert frame.margin == 0, f"Expected margin=0, got {frame.margin}"
|
||||
assert frame.horiz_margin == 0, f"Expected horiz_margin=0, got {frame.horiz_margin}"
|
||||
assert frame.vert_margin == 0, f"Expected vert_margin=0, got {frame.vert_margin}"
|
||||
|
||||
# Set margins when no alignment
|
||||
frame.margin = 10.0
|
||||
assert frame.margin == 10.0, f"Expected margin=10, got {frame.margin}"
|
||||
print(" PASS: margin properties exist and can be set")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 4: Check alignment auto-positioning
|
||||
print("Test 4: Checking alignment auto-positioning...")
|
||||
try:
|
||||
# Create parent frame
|
||||
parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200))
|
||||
|
||||
# Create child with CENTER alignment
|
||||
child = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
|
||||
child.align = mcrfpy.Alignment.CENTER
|
||||
|
||||
# Add to parent - should trigger alignment
|
||||
parent.children.append(child)
|
||||
|
||||
# Child should be centered: (200-50)/2 = 75
|
||||
expected_x = 75.0
|
||||
expected_y = 75.0
|
||||
assert abs(child.x - expected_x) < 0.1, f"Expected x={expected_x}, got {child.x}"
|
||||
assert abs(child.y - expected_y) < 0.1, f"Expected y={expected_y}, got {child.y}"
|
||||
print(" PASS: CENTER alignment positions child correctly")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 5: Check TOP_LEFT with margin
|
||||
print("Test 5: Checking TOP_LEFT alignment with margin...")
|
||||
try:
|
||||
parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200))
|
||||
child = mcrfpy.Frame(pos=(999, 999), size=(50, 50)) # Start at wrong position
|
||||
child.align = mcrfpy.Alignment.TOP_LEFT
|
||||
child.margin = 10.0
|
||||
|
||||
parent.children.append(child)
|
||||
|
||||
# Child should be at (10, 10)
|
||||
assert abs(child.x - 10.0) < 0.1, f"Expected x=10, got {child.x}"
|
||||
assert abs(child.y - 10.0) < 0.1, f"Expected y=10, got {child.y}"
|
||||
print(" PASS: TOP_LEFT with margin positions correctly")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 6: Check BOTTOM_RIGHT alignment
|
||||
print("Test 6: Checking BOTTOM_RIGHT alignment...")
|
||||
try:
|
||||
parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200))
|
||||
child = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
|
||||
child.align = mcrfpy.Alignment.BOTTOM_RIGHT
|
||||
child.margin = 5.0
|
||||
|
||||
parent.children.append(child)
|
||||
|
||||
# Child should be at (200-50-5, 200-50-5) = (145, 145)
|
||||
expected_x = 145.0
|
||||
expected_y = 145.0
|
||||
assert abs(child.x - expected_x) < 0.1, f"Expected x={expected_x}, got {child.x}"
|
||||
assert abs(child.y - expected_y) < 0.1, f"Expected y={expected_y}, got {child.y}"
|
||||
print(" PASS: BOTTOM_RIGHT with margin positions correctly")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 7: Check resize propagation
|
||||
print("Test 7: Checking resize propagation to children...")
|
||||
try:
|
||||
parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200))
|
||||
child = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
|
||||
child.align = mcrfpy.Alignment.CENTER
|
||||
|
||||
parent.children.append(child)
|
||||
|
||||
# Initial position check
|
||||
assert abs(child.x - 75.0) < 0.1, f"Initial x should be 75, got {child.x}"
|
||||
|
||||
# Resize parent
|
||||
parent.w = 300
|
||||
parent.h = 300
|
||||
|
||||
# Child should be re-centered: (300-50)/2 = 125
|
||||
expected_x = 125.0
|
||||
expected_y = 125.0
|
||||
assert abs(child.x - expected_x) < 0.1, f"After resize, expected x={expected_x}, got {child.x}"
|
||||
assert abs(child.y - expected_y) < 0.1, f"After resize, expected y={expected_y}, got {child.y}"
|
||||
print(" PASS: Resize propagates to aligned children")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 8: Check that align=None freezes position
|
||||
print("Test 8: Checking that align=None freezes position...")
|
||||
try:
|
||||
parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200))
|
||||
child = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
|
||||
child.align = mcrfpy.Alignment.CENTER
|
||||
|
||||
parent.children.append(child)
|
||||
centered_x = child.x
|
||||
centered_y = child.y
|
||||
|
||||
# Disable alignment
|
||||
child.align = None
|
||||
|
||||
# Resize parent
|
||||
parent.w = 400
|
||||
parent.h = 400
|
||||
|
||||
# Position should NOT change
|
||||
assert abs(child.x - centered_x) < 0.1, f"Position should be frozen at {centered_x}, got {child.x}"
|
||||
assert abs(child.y - centered_y) < 0.1, f"Position should be frozen at {centered_y}, got {child.y}"
|
||||
print(" PASS: align=None freezes position")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 9: Check CENTER alignment rejects margins
|
||||
print("Test 9: Checking CENTER alignment rejects margins...")
|
||||
try:
|
||||
frame = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
|
||||
frame.align = mcrfpy.Alignment.CENTER
|
||||
|
||||
# Setting margin on CENTER should raise ValueError
|
||||
try:
|
||||
frame.margin = 10.0
|
||||
print(" FAIL: Expected ValueError for margin with CENTER alignment")
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
pass # Expected
|
||||
|
||||
print(" PASS: CENTER alignment correctly rejects margin")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 10: Check alignment on other drawable types
|
||||
print("Test 10: Checking alignment on Caption...")
|
||||
try:
|
||||
parent = mcrfpy.Frame(pos=(0, 0), size=(200, 100))
|
||||
caption = mcrfpy.Caption(text="Test", pos=(0, 0))
|
||||
caption.align = mcrfpy.Alignment.CENTER
|
||||
|
||||
parent.children.append(caption)
|
||||
|
||||
# Caption should be roughly centered (exact position depends on text size)
|
||||
# Just verify it was moved from (0,0)
|
||||
assert caption.x > 0 or caption.y > 0, "Caption should have been repositioned"
|
||||
print(" PASS: Caption supports alignment")
|
||||
except Exception as e:
|
||||
print(f" FAIL: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "=" * 40)
|
||||
print("All alignment tests PASSED!")
|
||||
print("=" * 40)
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue