git.lucas.co / cce-weather
weather: now, the next 24 hours and the week
git clone https://git.lucas.co/cce-weather.git

commitbfccf96a1c8113c9836ae2ca7a80900da2b7378e
parentf865c13c6d
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 | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 60ef131..e338350 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,6 +18,7 @@ use std::sync::Arc;
 use chrono::{Datelike, NaiveDate, NaiveDateTime, Timelike};
 use wayland_client::QueueHandle;
 
+use cce_ui::widget::Owned;
 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
 use cce_ui::layout::{
     align_text_y, bevel_width, control_gap, list_font_parsed, plate_corner_radius,
@@ -148,10 +149,10 @@ fn spawn_timer(sender: calloop::channel::Sender<Message>, last_fetch: Arc<Atomic
 struct WeatherApp {
     // Widgets: plain fields so their addresses are stable (the UiContext
     // registry holds pointers to them).
-    search: Adapted<TextBox>,
-    units_btn: Adapted<Button>,
-    refresh_btn: Adapted<Button>,
-    results_btns: [Adapted<Button>; MAX_RESULTS],
+    search: Owned<Adapted<TextBox>>,
+    units_btn: Owned<Adapted<Button>>,
+    refresh_btn: Owned<Adapted<Button>>,
+    results_btns: [Owned<Adapted<Button>>; MAX_RESULTS],
 
     // App state — the source of truth; widgets are re-asserted from it.
     location: Option<Location>,
@@ -650,10 +651,10 @@ impl Application for WeatherApp {
         spawn_timer(sender.clone(), last_fetch.clone(), config.refresh_minutes);
 
         let mut app = Self {
-            search: TextBox::new(String::new()).with_placeholder("Search for a city…"),
-            units_btn: Button::new(0.0, 0.0, 0.0, 0.0).with_label(units.toggled().temp_suffix()),
-            refresh_btn: Button::new(0.0, 0.0, 0.0, 0.0).with_icon_name("refresh", "Refresh"),
-            results_btns: std::array::from_fn(|_| Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label("")),
+            search: Owned::new(TextBox::new(String::new()).with_placeholder("Search for a city…")),
+            units_btn: Owned::new(Button::new(0.0, 0.0, 0.0, 0.0).with_label(units.toggled().temp_suffix())),
+            refresh_btn: Owned::new(Button::new(0.0, 0.0, 0.0, 0.0).with_icon_name("refresh", "Refresh")),
+            results_btns: std::array::from_fn(|_| Owned::new(Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(""))),
             chosen_location: state.location.clone(),
             chosen_units: state.units,
             location,