[Bugfix] Caption Python property setters don't call markDirty() #289

Closed
opened 2026-03-08 16:35:11 +00:00 by john · 0 comments
Owner

Bug

Caption has two code paths for setting properties. The animation/C++ path (setProperty()) correctly calls markDirty(). The Python tp_getset setter path does not.

Affected setters (UICaption.cpp)

Setter Line What it does Calls markDirty?
set_text() 323-332 text.setString(...) No
set_color_member() 253-303 text.setFillColor(...) / text.setOutlineColor(...) No
set_float_member() 186-212 text.setOutlineThickness(...) / text.setCharacterSize(...) No
set_vec_member() 219-222 text.setPosition(...) No

Meanwhile, the setProperty() path (line 580+) calls markDirty() for every single property. This is the path used by cap.animate(...).

Contrast with UIFrame

UIFrame::set_color_member() (line 371-422) does call self->data->markDirty(). So frame.fill_color = Color(...) propagates dirty flags correctly, but caption.fill_color = Color(...) does not. Same property name, sibling widget types, different correctness.

Contrast with UISprite, UICircle, UILine, UIArc

These types use the unified setProperty() dispatch through UIDrawable::set_float_member() base class for x/y, but their type-specific setters (in their respective setProperty() methods) all call markDirty(). Caption's tp_getset setters are the outlier — they bypass setProperty() entirely and go straight to SFML calls.

Reproduction

box = mcrfpy.Frame(pos=(10,10), size=(400,300),
                    clip_children=True)
scene.children.append(box)

cap = mcrfpy.Caption(text="Hello", pos=(10,10))
box.children.append(cap)
# First render: "Hello" visible

cap.text = "World"      # No markDirty() — parent cache stale
cap.fill_color = mcrfpy.Color(255, 0, 0)  # Same problem

Note on x/y setters

Caption's x and y properties use UIDrawable::set_float_member() (the base class), which calls drawable->onPositionChanged(). Caption's onPositionChanged() (line 162-166) syncs the SFML text position but does not call markDirty() or markCompositeDirty(). This means moving a Caption within a cached Frame also doesn't invalidate the cache.

Fix

Add self->data->markDirty() (or the appropriate markContentDirty() / markCompositeDirty()) to each setter. For position changes, markCompositeDirty() is sufficient (texture content unchanged, just position). For content changes (text, color, font_size, outline), use markDirty() / markContentDirty().

Impact

Same as #288 — only manifests with clip_children=True or cache_subtree=True on a parent Frame. Combined with #288, these two bugs make Frame render caching appear completely broken for any dynamic content.

## Bug Caption has **two code paths** for setting properties. The animation/C++ path (`setProperty()`) correctly calls `markDirty()`. The Python `tp_getset` setter path does not. ### Affected setters (UICaption.cpp) | Setter | Line | What it does | Calls markDirty? | |--------|------|-------------|-----------------| | `set_text()` | 323-332 | `text.setString(...)` | **No** | | `set_color_member()` | 253-303 | `text.setFillColor(...)` / `text.setOutlineColor(...)` | **No** | | `set_float_member()` | 186-212 | `text.setOutlineThickness(...)` / `text.setCharacterSize(...)` | **No** | | `set_vec_member()` | 219-222 | `text.setPosition(...)` | **No** | Meanwhile, the `setProperty()` path (line 580+) calls `markDirty()` for every single property. This is the path used by `cap.animate(...)`. ### Contrast with UIFrame `UIFrame::set_color_member()` (line 371-422) **does** call `self->data->markDirty()`. So `frame.fill_color = Color(...)` propagates dirty flags correctly, but `caption.fill_color = Color(...)` does not. Same property name, sibling widget types, different correctness. ### Contrast with UISprite, UICircle, UILine, UIArc These types use the unified `setProperty()` dispatch through `UIDrawable::set_float_member()` base class for x/y, but their type-specific setters (in their respective `setProperty()` methods) all call `markDirty()`. Caption's tp_getset setters are the outlier — they bypass `setProperty()` entirely and go straight to SFML calls. ### Reproduction ```python box = mcrfpy.Frame(pos=(10,10), size=(400,300), clip_children=True) scene.children.append(box) cap = mcrfpy.Caption(text="Hello", pos=(10,10)) box.children.append(cap) # First render: "Hello" visible cap.text = "World" # No markDirty() — parent cache stale cap.fill_color = mcrfpy.Color(255, 0, 0) # Same problem ``` ### Note on x/y setters Caption's `x` and `y` properties use `UIDrawable::set_float_member()` (the base class), which calls `drawable->onPositionChanged()`. Caption's `onPositionChanged()` (line 162-166) syncs the SFML text position but does **not** call `markDirty()` or `markCompositeDirty()`. This means moving a Caption within a cached Frame also doesn't invalidate the cache. ### Fix Add `self->data->markDirty()` (or the appropriate `markContentDirty()` / `markCompositeDirty()`) to each setter. For position changes, `markCompositeDirty()` is sufficient (texture content unchanged, just position). For content changes (text, color, font_size, outline), use `markDirty()` / `markContentDirty()`. ### Impact Same as #288 — only manifests with `clip_children=True` or `cache_subtree=True` on a parent Frame. Combined with #288, these two bugs make Frame render caching appear completely broken for any dynamic content.
john closed this issue 2026-04-10 05:09:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
john/McRogueFace#289
No description provided.