git.lucas.co / cce-fonts
font browser

commit1125f45136f56b15db6ecd85add52c9c4d4e8c17
parent46faaba6d7
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-07 22:07
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 | 41 +++++++++++++++++++++--------------------
 1 file changed, 21 insertions(+), 20 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 5b5268c..48af2e0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
 mod pages;
 
 use wayland_client::QueueHandle;
+use cce_ui::widget::Owned;
 use cce_ui::cosmic_text::FontSystem;
 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
 use cce_ui::widget::{
@@ -213,23 +214,23 @@ struct TypefaceApp {
     keys: FontsKeys,
 
     // Browse panel
-    search_box: cce_ui::widget::Adapted<TextBox>,
+    search_box: Owned<cce_ui::widget::Adapted<TextBox>>,
     list_region: ScrollRegion,
     list_item_h: f32,
-    font_buttons: Vec<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
+    font_buttons: Vec<Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>>,
 
     // Preview panel
-    style_dropdown: cce_ui::widget::Adapted<Dropdown>,
-    size_spinbox: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
-    preview_box: cce_ui::widget::Adapted<TextBox>,
+    style_dropdown: Owned<cce_ui::widget::Adapted<Dropdown>>,
+    size_spinbox: Owned<cce_ui::widget::Adapted<cce_ui::widget::Spinbox>>,
+    preview_box: Owned<cce_ui::widget::Adapted<TextBox>>,
 
     // Details panel
-    btn_open_folder: cce_ui::widget::Adapted<cce_ui::widget::Button>,
-    btn_remove_font: cce_ui::widget::Adapted<cce_ui::widget::Button>,
+    btn_open_folder: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
+    btn_remove_font: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
 
     // Selection mode buttons
-    select_cancel_btn: cce_ui::widget::Adapted<cce_ui::widget::Button>,
-    select_confirm_btn: cce_ui::widget::Adapted<cce_ui::widget::Button>,
+    select_cancel_btn: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
+    select_confirm_btn: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
     select_mode: bool,
     last_click_idx: Option<usize>,
     /// When that click landed. A wall clock, not a `dt` countdown: `dt` is
@@ -406,7 +407,7 @@ impl TypefaceApp {
         for id in stale {
             self.ui_context.unregister_widget(id);
         }
-        self.font_buttons = self.filtered.iter().map(|f| Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(f)).collect();
+        self.font_buttons = self.filtered.iter().map(|f| Owned::new(Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(f))).collect();
         // ...and register the replacements now. `dispatch_widgets()` reports every row whose
         // rect passes the parked-sentinel gate, and fresh rows are (0,0,0,0) so they pass it
         // immediately — but `register_widgets()` only runs in the next layout pass, so the
@@ -705,18 +706,18 @@ impl Application for TypefaceApp {
         let all_fonts = previewable_fonts(font_system.db());
         let mut app = Self {
             keys: FontsKeys::load(),
-            search_box,
+            search_box: Owned::new(search_box),
             list_region: ScrollRegion::new(0.0, 4.0)
                 .with_sink_behind(true),
             list_item_h,
             font_buttons: Vec::new(),
-            style_dropdown,
-            size_spinbox,
-            preview_box,
-            btn_open_folder,
-            btn_remove_font,
-            select_cancel_btn,
-            select_confirm_btn,
+            style_dropdown: Owned::new(style_dropdown),
+            size_spinbox: Owned::new(size_spinbox),
+            preview_box: Owned::new(preview_box),
+            btn_open_folder: Owned::new(btn_open_folder),
+            btn_remove_font: Owned::new(btn_remove_font),
+            select_cancel_btn: Owned::new(select_cancel_btn),
+            select_confirm_btn: Owned::new(select_confirm_btn),
             select_mode,
             last_click_idx: None,
             last_click_at: None,
@@ -749,7 +750,7 @@ impl Application for TypefaceApp {
         
         app.families = app.extract_families(&app.all_fonts);
         app.filtered = app.filter_families(&app.families, "");
-        app.font_buttons = app.filtered.iter().map(|f| Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(f)).collect();
+        app.font_buttons = app.filtered.iter().map(|f| Owned::new(Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(f))).collect();
         
         // Parse preselected family and size from CLI args (passed by FontSelector)
         let mut preselected_family = None;
@@ -848,7 +849,7 @@ impl Application for TypefaceApp {
                 for id in stale {
                     self.ui_context.unregister_widget(id);
                 }
-                self.font_buttons = self.filtered.iter().map(|f| Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(f)).collect();
+                self.font_buttons = self.filtered.iter().map(|f| Owned::new(Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(f))).collect();
                 // Register the replacements now rather than waiting for the next layout pass —
                 // see refresh(). This is the path that produced the stale-root spam.
                 for btn in self.font_buttons.iter_mut() {