git.lucas.co / cce-mail
mail client (IMAP/SMTP)
git clone https://git.lucas.co/cce-mail.git

commitc58048c53888448119d43f82f74950b049133157
parente9c7f8cca9
authorLucas Galante <[email protected]>
date2026-08-14 13:46
fix: unregister the outgoing email-row buttons before reallocating

`email_buttons` is replaced whenever the visible message count changes (folder
switch, sync, search). Widget ids are globally monotonic and never reused, so the
fresh buttons register under NEW ids while the old ids stayed in the registry
pointing into the freed Vec buffer — 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 to read `visible()` / `popover_rect()`.

Uses the new `UiContext::unregister_widget`. The row *routing* was already correct —
culled rows are parked at (-9999, -9999, 0, 0) and all three dispatch loops filter on
that sentinel — so this is only about the registry, not about which row gets a click.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/main.rs | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/main.rs b/src/main.rs
index 2466303..c59adb6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2043,6 +2043,15 @@ impl Application for ClearEmailApp {
             self.email_list.update_bounds(list_count, 55.0 + MENUBAR_H, h_f32 - 70.0 - MENUBAR_H);
 
             if self.email_buttons.len() != list_count {
+                // Widget ids are globally monotonic and never reused, so the fresh buttons
+                // register under NEW ids — the outgoing ones would linger in the registry
+                // pointing into this Vec's freed buffer, and the engine walks the whole
+                // registry and derefs it on every left press
+                // (`close_popovers_missed_by_press`). Drop them before the reallocation.
+                let stale: Vec<_> = self.email_buttons.iter().map(|b| b.id()).collect();
+                for id in stale {
+                    self.ui_context.unregister_widget(id);
+                }
                 self.email_buttons = (0..list_count)
                     .map(|_| Button::new_list_row(0.0, 0.0, 0.0, 0.0))
                     .collect();