feat(rendering): implement RenderTexture base infrastructure and UIFrame clipping (#6)

- Added RenderTexture support to UIDrawable base class
  - std::unique_ptr<sf::RenderTexture> for opt-in rendering
  - Dirty flag system for optimization
  - enableRenderTexture() and markDirty() methods

- Implemented clip_children property for UIFrame
  - Python-accessible boolean property
  - Automatic RenderTexture creation when enabled
  - Proper coordinate transformation for nested frames

- Updated UIFrame::render() for clipping support
  - Renders to RenderTexture when clip_children=true
  - Handles nested clipping correctly
  - Only re-renders when dirty flag is set

- Added comprehensive dirty flag propagation
  - All property setters mark frame as dirty
  - Size changes recreate RenderTexture
  - Animation system integration

- Created tests for clipping functionality
  - Basic clipping test with visual verification
  - Advanced nested clipping test
  - Dynamic resize handling test

This is Phase 1 of the RenderTexture overhaul, providing the foundation
for advanced rendering effects like blur, glow, and viewport rendering.
This commit is contained in:
John McCardle 2025-07-06 16:13:12 -04:00
commit 967ebcf478
7 changed files with 503 additions and 19 deletions

View file

@ -241,3 +241,36 @@ int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) {
drawable->name = name_str;
return 0;
}
void UIDrawable::enableRenderTexture(unsigned int width, unsigned int height) {
// Create or recreate RenderTexture if size changed
if (!render_texture || render_texture->getSize().x != width || render_texture->getSize().y != height) {
render_texture = std::make_unique<sf::RenderTexture>();
if (!render_texture->create(width, height)) {
render_texture.reset();
use_render_texture = false;
return;
}
render_sprite.setTexture(render_texture->getTexture());
}
use_render_texture = true;
render_dirty = true;
}
void UIDrawable::updateRenderTexture() {
if (!use_render_texture || !render_texture) {
return;
}
// Clear the RenderTexture
render_texture->clear(sf::Color::Transparent);
// Render content to RenderTexture
// This will be overridden by derived classes
// For now, just display the texture
render_texture->display();
// Update the sprite
render_sprite.setTexture(render_texture->getTexture());
}

View file

@ -77,6 +77,21 @@ public:
virtual bool getProperty(const std::string& name, sf::Color& value) const { return false; }
virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; }
virtual bool getProperty(const std::string& name, std::string& value) const { return false; }
protected:
// RenderTexture support (opt-in)
std::unique_ptr<sf::RenderTexture> render_texture;
sf::Sprite render_sprite;
bool use_render_texture = false;
bool render_dirty = true;
// Enable RenderTexture for this drawable
void enableRenderTexture(unsigned int width, unsigned int height);
void updateRenderTexture();
public:
// Mark this drawable as needing redraw
void markDirty() { render_dirty = true; }
};
typedef struct {

View file

@ -89,22 +89,70 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
// TODO: Apply opacity when SFML supports it on shapes
box.move(offset);
//Resources::game->getWindow().draw(box);
target.draw(box);
box.move(-offset);
// Check if we need to use RenderTexture for clipping
if (clip_children && !children->empty()) {
// Enable RenderTexture if not already enabled
if (!use_render_texture) {
auto size = box.getSize();
enableRenderTexture(static_cast<unsigned int>(size.x),
static_cast<unsigned int>(size.y));
}
// Update RenderTexture if dirty
if (use_render_texture && render_dirty) {
// Clear the RenderTexture
render_texture->clear(sf::Color::Transparent);
// Draw the frame box to RenderTexture
box.setPosition(0, 0); // Render at origin in texture
render_texture->draw(box);
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
// Render children to RenderTexture at local coordinates
for (auto drawable : *children) {
drawable->render(sf::Vector2f(0, 0), *render_texture);
}
// Finalize the RenderTexture
render_texture->display();
// Update sprite
render_sprite.setTexture(render_texture->getTexture());
render_dirty = false;
}
// Draw the RenderTexture sprite
if (use_render_texture) {
render_sprite.setPosition(offset + box.getPosition());
target.draw(render_sprite);
}
} else {
// Standard rendering without clipping
box.move(offset);
target.draw(box);
box.move(-offset);
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
for (auto drawable : *children) {
drawable->render(offset + box.getPosition(), target);
for (auto drawable : *children) {
drawable->render(offset + box.getPosition(), target);
}
}
}
@ -157,16 +205,36 @@ int UIFrame::set_float_member(PyUIFrameObject* self, PyObject* value, void* clos
PyErr_SetString(PyExc_TypeError, "Value must be an integer.");
return -1;
}
if (member_ptr == 0) //x
if (member_ptr == 0) { //x
self->data->box.setPosition(val, self->data->box.getPosition().y);
else if (member_ptr == 1) //y
self->data->markDirty();
}
else if (member_ptr == 1) { //y
self->data->box.setPosition(self->data->box.getPosition().x, val);
else if (member_ptr == 2) //w
self->data->markDirty();
}
else if (member_ptr == 2) { //w
self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y));
else if (member_ptr == 3) //h
if (self->data->use_render_texture) {
// Need to recreate RenderTexture with new size
self->data->enableRenderTexture(static_cast<unsigned int>(self->data->box.getSize().x),
static_cast<unsigned int>(self->data->box.getSize().y));
}
self->data->markDirty();
}
else if (member_ptr == 3) { //h
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
else if (member_ptr == 4) //outline
if (self->data->use_render_texture) {
// Need to recreate RenderTexture with new size
self->data->enableRenderTexture(static_cast<unsigned int>(self->data->box.getSize().x),
static_cast<unsigned int>(self->data->box.getSize().y));
}
self->data->markDirty();
}
else if (member_ptr == 4) { //outline
self->data->box.setOutlineThickness(val);
self->data->markDirty();
}
return 0;
}
@ -243,10 +311,12 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos
if (member_ptr == 0)
{
self->data->box.setFillColor(sf::Color(r, g, b, a));
self->data->markDirty();
}
else if (member_ptr == 1)
{
self->data->box.setOutlineColor(sf::Color(r, g, b, a));
self->data->markDirty();
}
else
{
@ -276,6 +346,28 @@ int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure)
return -1;
}
self->data->box.setPosition(vec->data);
self->data->markDirty();
return 0;
}
PyObject* UIFrame::get_clip_children(PyUIFrameObject* self, void* closure)
{
return PyBool_FromLong(self->data->clip_children);
}
int UIFrame::set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "clip_children must be a boolean");
return -1;
}
bool new_clip = PyObject_IsTrue(value);
if (new_clip != self->data->clip_children) {
self->data->clip_children = new_clip;
self->data->markDirty(); // Mark as needing redraw
}
return 0;
}
@ -301,6 +393,7 @@ PyGetSetDef UIFrame::getsetters[] = {
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIFRAME},
{"pos", (getter)UIFrame::get_pos, (setter)UIFrame::set_pos, "Position as a Vector", NULL},
{"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL},
UIDRAWABLE_GETSETTERS,
{NULL}
};
@ -461,58 +554,81 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
bool UIFrame::setProperty(const std::string& name, float value) {
if (name == "x") {
box.setPosition(sf::Vector2f(value, box.getPosition().y));
markDirty();
return true;
} else if (name == "y") {
box.setPosition(sf::Vector2f(box.getPosition().x, value));
markDirty();
return true;
} else if (name == "w") {
box.setSize(sf::Vector2f(value, box.getSize().y));
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(box.getSize().x),
static_cast<unsigned int>(box.getSize().y));
}
markDirty();
return true;
} else if (name == "h") {
box.setSize(sf::Vector2f(box.getSize().x, value));
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(box.getSize().x),
static_cast<unsigned int>(box.getSize().y));
}
markDirty();
return true;
} else if (name == "outline") {
box.setOutlineThickness(value);
markDirty();
return true;
} else if (name == "fill_color.r") {
auto color = box.getFillColor();
color.r = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.g") {
auto color = box.getFillColor();
color.g = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.b") {
auto color = box.getFillColor();
color.b = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.a") {
auto color = box.getFillColor();
color.a = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "outline_color.r") {
auto color = box.getOutlineColor();
color.r = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.g") {
auto color = box.getOutlineColor();
color.g = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.b") {
auto color = box.getOutlineColor();
color.b = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.a") {
auto color = box.getOutlineColor();
color.a = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
}
return false;
@ -521,9 +637,11 @@ bool UIFrame::setProperty(const std::string& name, float value) {
bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
box.setFillColor(value);
markDirty();
return true;
} else if (name == "outline_color") {
box.setOutlineColor(value);
markDirty();
return true;
}
return false;
@ -532,9 +650,16 @@ bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "position") {
box.setPosition(value);
markDirty();
return true;
} else if (name == "size") {
box.setSize(value);
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(value.x),
static_cast<unsigned int>(value.y));
}
markDirty();
return true;
}
return false;

View file

@ -29,6 +29,7 @@ public:
float outline;
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
bool clip_children = false; // Whether to clip children to frame bounds
void render(sf::Vector2f, sf::RenderTarget&) override final;
void move(sf::Vector2f);
PyObjectsEnum derived_type() override final;
@ -47,6 +48,8 @@ public:
static int set_color_member(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_pos(PyUIFrameObject* self, void* closure);
static int set_pos(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_clip_children(PyUIFrameObject* self, void* closure);
static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIFrameObject* self);
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);