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

commit07a992c40fe239b2d12226a6ca223478637ce8de
parent5a62ea1de2
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-07 22:08
refactor: hold widgets in Owned boxes (cce-ui Owned<W>)

The UiContext registry keeps raw pointers to registered widgets, and a
widget that moved while registered (its Vec reallocating, its struct
returned by value) left one pointing at the old address. Each widget
field is now an Owned<Adapted<..>> — a heap box whose allocation, not the
widget, carries the liveness token the registry watches — so the
registered address never moves and is dropped from the registry when the
box is freed. Field access is unchanged through Deref; construction
sites wrap in Owned::new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/main.rs | 65 +++++++++++++++++++++++++++++++------------------------------
 1 file changed, 33 insertions(+), 32 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 21ebfe3..21cc579 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,6 +3,7 @@
 mod ipc;
 mod wpe;
 mod accounts_file;
+use cce_ui::widget::Owned;
 use cce_ui::widget::ScrollRegion;
 use wayland_client::QueueHandle;
 use cce_ui::cosmic_text::FontSystem;
@@ -260,35 +261,35 @@ struct ClearEmailApp {
     /// The app menu trigger: the cce-icons `mail` glyph, plateless. Its
     /// rows fire through `context_menu_actions`, the same path as a card's
     /// right-click menu, so the bar owns no second menu implementation.
-    mail_button: cce_ui::widget::Adapted<cce_ui::widget::Button>,
+    mail_button: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
     /// Folder switcher: options[0] carries the live inbox unread count
     /// ("Inbox (6)"), refreshed each rebuild, so rows and trigger agree.
-    folder_dropdown: cce_ui::widget::Adapted<Dropdown>,
+    folder_dropdown: Owned<cce_ui::widget::Adapted<Dropdown>>,
     /// Account switcher beside the folder dropdown: options are the account
     /// emails plus a trailing "Manage Accounts…" pseudo-entry (management
     /// lives in cce-system-interface). Options refresh from accounts.json on
     /// every open — the job the retired Accounts page did on entry.
-    account_dropdown: cce_ui::widget::Adapted<Dropdown>,
+    account_dropdown: Owned<cce_ui::widget::Adapted<Dropdown>>,
 
     // Search and List View
-    search_box: cce_ui::widget::Adapted<TextBox>,
+    search_box: Owned<cce_ui::widget::Adapted<TextBox>>,
     email_list: ScrollRegion,
-    email_buttons: Vec<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
+    email_buttons: Vec<Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>>,
 
     // Details View (Reply/Delete/Mark Read/Unread live in the Message menu)
-    detail_body: cce_ui::widget::Adapted<TextBox>,
+    detail_body: Owned<cce_ui::widget::Adapted<TextBox>>,
 
     // Compose Dialog
-    compose_to: cce_ui::widget::Adapted<TextBox>,
-    compose_cc: cce_ui::widget::Adapted<TextBox>,
-    compose_bcc: cce_ui::widget::Adapted<TextBox>,
-    compose_subject: cce_ui::widget::Adapted<TextBox>,
-    compose_body: cce_ui::widget::Adapted<TextBox>,
+    compose_to: Owned<cce_ui::widget::Adapted<TextBox>>,
+    compose_cc: Owned<cce_ui::widget::Adapted<TextBox>>,
+    compose_bcc: Owned<cce_ui::widget::Adapted<TextBox>>,
+    compose_subject: Owned<cce_ui::widget::Adapted<TextBox>>,
+    compose_body: Owned<cce_ui::widget::Adapted<TextBox>>,
     /// Absolute paths queued for the next send; drawn as removable chips.
     compose_attachments: Vec<String>,
-    btn_compose_send: cce_ui::widget::Adapted<cce_ui::widget::Button>,
-    btn_compose_cancel: cce_ui::widget::Adapted<cce_ui::widget::Button>,
-    btn_compose_attach: cce_ui::widget::Adapted<cce_ui::widget::Button>,
+    btn_compose_send: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
+    btn_compose_cancel: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
+    btn_compose_attach: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
 
     // Accounts (switch via the bar dropdown — management lives in cce-system-interface)
     accounts: Vec<AccountInfo>,
@@ -350,9 +351,9 @@ struct ClearEmailApp {
     /// HTML-view toggles in the detail header band (text/HTML, remote
     /// images); laid out each frame by `layout_html_buttons`.
     #[cfg(feature = "wpe")]
-    btn_html_view: cce_ui::widget::Adapted<cce_ui::widget::Button>,
+    btn_html_view: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
     #[cfg(feature = "wpe")]
-    btn_html_images: cce_ui::widget::Adapted<cce_ui::widget::Button>,
+    btn_html_images: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
     /// Where the page was last drawn (logical px), for routing input to it.
     #[cfg(feature = "wpe")]
     webview_rect: (f32, f32, f32, f32),
@@ -4872,22 +4873,22 @@ impl Application for ClearEmailApp {
             backfill_remaining: std::collections::HashMap::new(),
             resync_pending: false,
             keys: EmailKeys::load(),
-            mail_button,
-            folder_dropdown,
-            account_dropdown,
-            search_box,
+            mail_button: Owned::new(mail_button),
+            folder_dropdown: Owned::new(folder_dropdown),
+            account_dropdown: Owned::new(account_dropdown),
+            search_box: Owned::new(search_box),
             email_list,
             email_buttons: Vec::new(),
-            detail_body,
-            compose_to,
-            compose_cc,
-            compose_bcc,
-            compose_subject,
-            compose_body,
+            detail_body: Owned::new(detail_body),
+            compose_to: Owned::new(compose_to),
+            compose_cc: Owned::new(compose_cc),
+            compose_bcc: Owned::new(compose_bcc),
+            compose_subject: Owned::new(compose_subject),
+            compose_body: Owned::new(compose_body),
             compose_attachments: Vec::new(),
-            btn_compose_send,
-            btn_compose_cancel,
-            btn_compose_attach,
+            btn_compose_send: Owned::new(btn_compose_send),
+            btn_compose_cancel: Owned::new(btn_compose_cancel),
+            btn_compose_attach: Owned::new(btn_compose_attach),
             accounts,
             selected_account_idx,
             emails,
@@ -4917,9 +4918,9 @@ impl Application for ClearEmailApp {
             #[cfg(feature = "wpe")]
             show_text: false,
             #[cfg(feature = "wpe")]
-            btn_html_view: Button::new(-9999.0, -9999.0, 0.0, 0.0).with_label("View: HTML"),
+            btn_html_view: Owned::new(Button::new(-9999.0, -9999.0, 0.0, 0.0).with_label("View: HTML")),
             #[cfg(feature = "wpe")]
-            btn_html_images: Button::new(-9999.0, -9999.0, 0.0, 0.0).with_label("Load Images"),
+            btn_html_images: Owned::new(Button::new(-9999.0, -9999.0, 0.0, 0.0).with_label("Load Images")),
             #[cfg(feature = "wpe")]
             webview_rect: (0.0, 0.0, 0.0, 0.0),
             #[cfg(feature = "wpe")]
@@ -5796,7 +5797,7 @@ impl Application for ClearEmailApp {
                     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))
+                    .map(|_| Owned::new(Button::new_list_row(0.0, 0.0, 0.0, 0.0)))
                     .collect();
                 // register_dispatch_roots() already ran this frame with the OLD
                 // buttons; the fresh ids must be registered now or every click on