git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commitb4bf46f1a0f68b383312e5c6caa52ac8d82706bd
parentb2e97f9ab8
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          | 13 +++++++------
 src/pages/browse.rs  | 15 ++++++++-------
 src/pages/mod.rs     | 13 +++++++------
 src/pages/network.rs |  9 +++++----
 src/pages/space.rs   |  5 +++--
 5 files changed, 30 insertions(+), 25 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index b279807..6d71275 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,5 @@
 use wayland_client::QueueHandle;
+use cce_ui::widget::Owned;
 use cce_ui::cosmic_text::FontSystem;
 
 use cce_ui::engine::{Application, LogicalPosition, LogicalSize, WindowSettings};
@@ -83,7 +84,7 @@ impl PromptKind {
 struct PromptDialog {
     kind: PromptKind,
     path: std::path::PathBuf,
-    textbox: cce_ui::widget::Adapted<cce_ui::widget::TextBox>,
+    textbox: Owned<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
 }
 
 fn prompt_rects(win_w: f32, win_h: f32) -> PromptRects {
@@ -600,11 +601,11 @@ struct FilesystemApp {
     width: u32,
     height: u32,
     scale_factor: f64,
-    page_buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, Message)>,
+    page_buttons: Vec<(Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>, Message)>,
     hovered_button: Option<usize>,
     cursor_x: f32,
     cursor_y: f32,
-    paginator: cce_ui::widget::Adapted<Paginator>,
+    paginator: Owned<cce_ui::widget::Adapted<Paginator>>,
     just_initialized: bool,
     ui_context: cce_ui::context::UiContext,
     watcher: Option<notify::RecommendedWatcher>,
@@ -1569,7 +1570,7 @@ impl Application for FilesystemApp {
             hovered_button: None,
             cursor_x: 0.0,
             cursor_y: 0.0,
-            paginator,
+            paginator: Owned::new(paginator),
             just_initialized: true,
             ui_context: cce_ui::context::UiContext::new(),
             watcher: None,
@@ -1796,7 +1797,7 @@ impl Application for FilesystemApp {
                     .with_placeholder("Program/Command");
                 tb.focus();
                 self.ui_context.set_focused(&mut tb);
-                self.prompt_dialog = Some(PromptDialog { kind: PromptKind::OpenWith, path, textbox: tb });
+                self.prompt_dialog = Some(PromptDialog { kind: PromptKind::OpenWith, path, textbox: Owned::new(tb) });
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
@@ -1816,7 +1817,7 @@ impl Application for FilesystemApp {
                 tb.all_selected = false;
                 tb.sync_editor_state();
                 self.ui_context.set_focused(&mut tb);
-                self.prompt_dialog = Some(PromptDialog { kind: PromptKind::Rename, path, textbox: tb });
+                self.prompt_dialog = Some(PromptDialog { kind: PromptKind::Rename, path, textbox: Owned::new(tb) });
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
diff --git a/src/pages/browse.rs b/src/pages/browse.rs
index e57026c..083abae 100644
--- a/src/pages/browse.rs
+++ b/src/pages/browse.rs
@@ -1,6 +1,7 @@
 use std::path::{Path, PathBuf};
 
 use crate::pages::PageContent;
+use cce_ui::widget::Owned;
 use cce_ui::widget::{Adapted, WidgetHost, Breadcrumb, PathController};
 use cce_ui::layout::{ColumnLayout, LayoutStrategy};
 
@@ -27,7 +28,7 @@ pub struct BrowseState {
     pub show_hidden: bool,
     pub list: crate::row_list::RowList,
     pub search_visible: bool,
-    pub search_box: cce_ui::widget::Adapted<cce_ui::widget::TextBox>,
+    pub search_box: Owned<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
     pub selected: Option<usize>,
     /// The selection the list was last auto-scrolled to. The layout pass runs
     /// every frame, and an unconditional scroll_into_view there UNDID every
@@ -37,8 +38,8 @@ pub struct BrowseState {
     pub autoscrolled_to: Option<usize>,
     /// A just-created folder to select once a refresh lists it.
     pub pending_select: Option<PathBuf>,
-    pub breadcrumb: Adapted<Breadcrumb>,
-    pub save_name_box: cce_ui::widget::Adapted<cce_ui::widget::TextBox>,
+    pub breadcrumb: Owned<Adapted<Breadcrumb>>,
+    pub save_name_box: Owned<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
 }
 
 impl Default for BrowseState {
@@ -54,14 +55,14 @@ impl Default for BrowseState {
             show_hidden: false,
             list: crate::row_list::RowList::new(cce_ui::layout::button_height(), 2.0),
             search_visible: false,
-            search_box: cce_ui::widget::TextBox::new(String::new())
+            search_box: Owned::new(cce_ui::widget::TextBox::new(String::new())
                 .with_placeholder("Search...")
-                .with_update_on_type(true),
+                .with_update_on_type(true)),
             selected: None,
             autoscrolled_to: None,
             pending_select: None,
-            breadcrumb,
-            save_name_box: cce_ui::widget::TextBox::new(String::new()).with_max_width(None),
+            breadcrumb: Owned::new(breadcrumb),
+            save_name_box: Owned::new(cce_ui::widget::TextBox::new(String::new()).with_max_width(None)),
         };
         state.update_breadcrumb();
         state
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 91c091e..7c783bd 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -3,6 +3,7 @@ pub mod preview;
 pub mod network;
 pub mod space;
 
+use cce_ui::widget::Owned;
 use cce_ui::layout::RenderTarget;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -54,7 +55,7 @@ pub fn breadcrumb_h() -> f32 {
 /// started their content 28px and 34px down against Browse's 24 + gap.
 pub fn breadcrumb_header(
     pc: &mut PageContent,
-    breadcrumb: &mut cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>,
+    breadcrumb: &mut Owned<cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>>,
     cx: f32,
     cy: f32,
     cw: f32,
@@ -100,7 +101,7 @@ pub fn breadcrumb_header(
 /// this carve to `window_pc` would buy nothing.
 pub fn breadcrumb_relief(
     pc: &mut PageContent,
-    breadcrumb: &cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>,
+    breadcrumb: &Owned<cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>>,
     rect: cce_ui::scene::layout::Rect,
 ) {
     let r = cce_ui::layout::dropdown_corner_radius();
@@ -172,7 +173,7 @@ impl Stroke {
 pub struct PageContent {
     pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
     pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
-    pub buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, crate::Message)>,
+    pub buttons: Vec<(Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>, crate::Message)>,
     /// Relief steps for the control_relief styling — (x, y, w, h, radius, depth,
     /// kind: [`RELIEF_RECESSED`]/[`RELIEF_RAISED`]/[`RELIEF_INSET`]). The flat rects
     /// own the faces; these are the edges-only walls emitted over them (the
@@ -422,7 +423,7 @@ impl PageContent {
             .with_bg(bg)
             .with_hover_bg(hover_bg)
             .with_label_color(label_color);
-        self.buttons.push((btn, action));
+        self.buttons.push((Owned::new(btn), action));
     }
 
     /// A button wearing the toolkit's own face — no per-call colors. The
@@ -437,7 +438,7 @@ impl PageContent {
             .with_label(label)
             .with_bg([0.0, 0.0, 0.0, 0.0])
             .with_hover_bg([1.0, 1.0, 1.0, 0.10]);
-        self.buttons.push((btn, action));
+        self.buttons.push((Owned::new(btn), action));
     }
 
     pub fn button_left(
@@ -458,7 +459,7 @@ impl PageContent {
             .with_hover_bg(hover_bg)
             .with_label_color(label_color)
             .with_left_align(true);
-        self.buttons.push((btn, action));
+        self.buttons.push((Owned::new(btn), action));
     }
 }
 
diff --git a/src/pages/network.rs b/src/pages/network.rs
index c9af9bc..4b9cfc8 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,11 +1,12 @@
 use std::path::{Path, PathBuf};
 use crate::pages::PageContent;
 use crate::pages::browse::{BrowseState, DirEntry};
+use cce_ui::widget::Owned;
 use cce_ui::widget::{Adapted, Graph, GraphNode, Breadcrumb, GraphController, PathController};
 
 pub struct NetworkState {
-    pub graph: Adapted<Graph>,
-    pub breadcrumb: Adapted<Breadcrumb>,
+    pub graph: Owned<Adapted<Graph>>,
+    pub breadcrumb: Owned<Adapted<Breadcrumb>>,
     pub last_dir: PathBuf,
     /// The graph pane `(x, y, w, h)` the nodes were last laid out for: a
     /// pane of another size is laid out again (`view`), so the columns fit
@@ -41,8 +42,8 @@ impl Default for NetworkState {
         breadcrumb.set_network_opacity(0.95);
 
         Self {
-            graph,
-            breadcrumb,
+            graph: Owned::new(graph),
+            breadcrumb: Owned::new(breadcrumb),
             last_dir: PathBuf::new(),
             node_glyphs: Vec::new(),
             laid_out_for: (0.0, 0.0, 0.0, 0.0),
diff --git a/src/pages/space.rs b/src/pages/space.rs
index 76f038e..12f47a6 100644
--- a/src/pages/space.rs
+++ b/src/pages/space.rs
@@ -19,6 +19,7 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use std::sync::atomic::{AtomicBool, Ordering};
 
+use cce_ui::widget::Owned;
 use cce_ui::widget::{Adapted, Breadcrumb, PathController};
 
 use crate::pages::PageContent;
@@ -405,7 +406,7 @@ pub fn update(state: &mut SpaceState, msg: SpaceMessage) {
 // ── Page state ──────────────────────────────────────────────────────
 
 pub struct SpaceState {
-    pub breadcrumb: Adapted<Breadcrumb>,
+    pub breadcrumb: Owned<Adapted<Breadcrumb>>,
     /// The directory the current `tree` describes. Empty until a scan lands.
     pub scanned_dir: PathBuf,
     pub tree: Option<TreeNode>,
@@ -439,7 +440,7 @@ impl Default for SpaceState {
         let mut breadcrumb = Breadcrumb::new();
         breadcrumb.set_network_opacity(0.95);
         Self {
-            breadcrumb,
+            breadcrumb: Owned::new(breadcrumb),
             scanned_dir: PathBuf::new(),
             tree: None,
             tiles: Vec::new(),