page layout tool
git clone https://git.lucas.co/cce-layout-interface.git
fix: unregister the outgoing row buttons before rebuilding them
`rebuild_recent_buttons` and `rebuild_layers_tab_widgets` both `clear()` their Vec and
push freshly constructed buttons. Widget ids are globally monotonic and never reused,
so the replacements register under NEW ids while the dropped ones stayed in the
registry pointing at freed boxes — and this crate never calls `clear_hierarchy`, so
they accumulated for the life of the process.
That is a use-after-free rather than a leak: the engine calls
`close_popovers_missed_by_press` on every left press, which walks the whole registry
and dereferences each pointer.
Uses the new `UiContext::unregister_widget`. Row routing was already correct — the
`else { set_rect(-9999.0, ...) }` park in `rebuild_text_items` is the same-predicate
filter that keeps a scrolled-out row from stealing a visible row's click.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/main.rs | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/src/main.rs b/src/main.rs
index 2441be9..b1d27d3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -452,6 +452,14 @@ impl LayoutApp {
}
fn rebuild_recent_buttons(&mut self) {
+ // Widget ids are globally monotonic and never reused, so the buttons pushed below
+ // register under NEW ids; the dropped ones would stay in the registry pointing at
+ // freed memory, and the engine derefs the whole registry on every left press
+ // (`close_popovers_missed_by_press`). Drop their registrations first.
+ let stale: Vec<_> = self.recent_files_buttons.iter().map(|b| b.id()).collect();
+ for id in stale {
+ self.ui_context.unregister_widget(id);
+ }
self.recent_files_buttons.clear();
for file in &self.recent_files {
let label = file.file_name()
@@ -1184,6 +1192,12 @@ impl LayoutApp {
}
fn rebuild_layers_tab_widgets(&mut self) {
+ // See the note in rebuild_recent_buttons(): monotonic ids mean the outgoing buttons
+ // must be unregistered or the registry keeps dangling pointers into freed boxes.
+ let stale: Vec<_> = self.layer_buttons.iter().map(|b| b.id()).collect();
+ for id in stale {
+ self.ui_context.unregister_widget(id);
+ }
self.layer_buttons.clear();
for (idx, element) in self.elements.iter().enumerate() {
let label = match element {