git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commitfe85b85552075f5db1c76e64d292d99bed5e112b
parentd99b8a5876
authorLucas Galante <[email protected]>
date2026-06-18 01:18
audio: box dynamic controls in Vec to avoid dangling pointer crash on reallocation

 src/main.rs         |  8 ++++----
 src/pages/audio.rs  | 28 ++++++++++++++--------------
 src/pages/layout.rs | 26 ++++++++++++++++++++++++++
 3 files changed, 44 insertions(+), 18 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index e77d5ee..d80d765 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1088,16 +1088,16 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
                 link_parent_child(page_root, &mut self.page_sec_containers[1], &mut self.ui_context);
                 
                 for sb in &mut self.app.audio.sink_spinboxes {
-                    link_parent_child(&mut self.page_sec_containers[0], sb, &mut self.ui_context);
+                    link_parent_child(&mut self.page_sec_containers[0], &mut **sb, &mut self.ui_context);
                 }
                 for sb in &mut self.app.audio.source_spinboxes {
-                    link_parent_child(&mut self.page_sec_containers[1], sb, &mut self.ui_context);
+                    link_parent_child(&mut self.page_sec_containers[1], &mut **sb, &mut self.ui_context);
                 }
                 for slider in &mut self.app.audio.sink_sliders {
-                    link_parent_child(&mut self.page_sec_containers[0], slider, &mut self.ui_context);
+                    link_parent_child(&mut self.page_sec_containers[0], &mut **slider, &mut self.ui_context);
                 }
                 for slider in &mut self.app.audio.source_sliders {
-                    link_parent_child(&mut self.page_sec_containers[1], slider, &mut self.ui_context);
+                    link_parent_child(&mut self.page_sec_containers[1], &mut **slider, &mut self.ui_context);
                 }
             }
             Page::Display => {
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index 0cd8f92..1364dad 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -25,10 +25,10 @@ pub struct AudioState {
     pub loaded: bool,
     pub sinks: Vec<AudioSink>,
     pub sources: Vec<AudioSource>,
-    pub sink_spinboxes: Vec<Spinbox>,
-    pub source_spinboxes: Vec<Spinbox>,
-    pub sink_sliders: Vec<Slider>,
-    pub source_sliders: Vec<Slider>,
+    pub sink_spinboxes: Vec<Box<Spinbox>>,
+    pub source_spinboxes: Vec<Box<Spinbox>>,
+    pub sink_sliders: Vec<Box<Slider>>,
+    pub source_sliders: Vec<Box<Slider>>,
 }
 
 #[derive(Debug, Clone)]
@@ -260,9 +260,9 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
                         state.sink_sliders[idx].set_value(sink.volume);
                         let slider_x = subsec.ax(bar_x);
                         
-                        let label_h = cce_ui::widget::label_offset(&state.sink_sliders[idx]);
+                        let label_h = cce_ui::widget::label_offset(&*state.sink_sliders[idx]);
                         let slider_h = cce_ui::layout::slider_height() + label_h;
-                        render_widget(subsec.pc, &mut state.sink_sliders[idx], slider_x, yt, bar_w, slider_h, ctx);
+                        render_widget(subsec.pc, &mut *state.sink_sliders[idx], slider_x, yt, bar_w, slider_h, ctx);
 
                         let row_y = subsec.ay() + slider_h + 8.0;
                         let sb_w = 100.0;
@@ -274,7 +274,7 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
                         state.sink_spinboxes[idx].value = (sink.volume * 100.0).round() as i32;
                         state.sink_spinboxes[idx].set_row_rect(row_rect_x, subsec.cw - 16.0);
                         let sb_x = subsec.ax(bar_x);
-                        render_widget(subsec.pc, &mut state.sink_spinboxes[idx], sb_x, row_y, sb_w, sb_h, ctx);
+                        render_widget(subsec.pc, &mut *state.sink_spinboxes[idx], sb_x, row_y, sb_w, sb_h, ctx);
 
                         let mute_label = if sink.muted { "Unmute" } else { "Mute" };
                         let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
@@ -328,9 +328,9 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
                         state.source_sliders[idx].set_value(src.volume);
                         let slider_x = subsec.ax(bar_x);
                         
-                        let label_h = cce_ui::widget::label_offset(&state.source_sliders[idx]);
+                        let label_h = cce_ui::widget::label_offset(&*state.source_sliders[idx]);
                         let slider_h = cce_ui::layout::slider_height() + label_h;
-                        render_widget(subsec.pc, &mut state.source_sliders[idx], slider_x, yt, bar_w, slider_h, ctx);
+                        render_widget(subsec.pc, &mut *state.source_sliders[idx], slider_x, yt, bar_w, slider_h, ctx);
 
                         let row_y = subsec.ay() + slider_h + 8.0;
                         let sb_w = 100.0;
@@ -342,7 +342,7 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
                         state.source_spinboxes[idx].value = (src.volume * 100.0).round() as i32;
                         state.source_spinboxes[idx].set_row_rect(row_rect_x, subsec.cw - 16.0);
                         let sb_x = subsec.ax(bar_x);
-                        render_widget(subsec.pc, &mut state.source_spinboxes[idx], sb_x, row_y, sb_w, sb_h, ctx);
+                        render_widget(subsec.pc, &mut *state.source_spinboxes[idx], sb_x, row_y, sb_w, sb_h, ctx);
 
                         let mute_label = if src.muted { "Unmute" } else { "Mute" };
                         let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
@@ -366,10 +366,10 @@ pub fn update(state: &mut AudioState, msg: AudioMessage) {
     match msg {
         AudioMessage::Refreshed(new) => {
             *state = new;
-            state.sink_spinboxes.resize_with(state.sinks.len(), || Spinbox::new(50, 0, 100, 1));
-            state.source_spinboxes.resize_with(state.sources.len(), || Spinbox::new(50, 0, 100, 1));
-            state.sink_sliders.resize_with(state.sinks.len(), || Slider::new().with_range(0.0, 1.0).with_scroll(true));
-            state.source_sliders.resize_with(state.sources.len(), || Slider::new().with_range(0.0, 1.0).with_scroll(true));
+            state.sink_spinboxes.resize_with(state.sinks.len(), || Box::new(Spinbox::new(50, 0, 100, 1)));
+            state.source_spinboxes.resize_with(state.sources.len(), || Box::new(Spinbox::new(50, 0, 100, 1)));
+            state.sink_sliders.resize_with(state.sinks.len(), || Box::new(Slider::new().with_range(0.0, 1.0).with_scroll(true)));
+            state.source_sliders.resize_with(state.sources.len(), || Box::new(Slider::new().with_range(0.0, 1.0).with_scroll(true)));
         }
         AudioMessage::SinkVolume(id, vol) => {
             if let Some(sink) = state.sinks.iter_mut().find(|s| s.id == id) {
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index a741b9b..8e166ff 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -921,4 +921,30 @@ mode = "popup"
         );
         assert!(res);
     }
+
+    #[test]
+    fn test_spinbox_right_click_crash() {
+        use cce_ui::widget::Element;
+        let mut state = LayoutState::default();
+        let mut ctx = cce_ui::context::UiContext::new();
+        let sb = &mut state.spinboxes[0];
+        sb.set_rect(0.0, 0.0, 100.0, 44.0);
+        let res = sb.mouse_input(
+            cce_ui::widget::MouseButton::Right,
+            cce_ui::widget::ElementState::Pressed,
+            50.0,
+            20.0,
+            &mut ctx,
+        );
+        assert!(res);
+        assert!(cce_ui::widget::context_menu::is_visible());
+
+        // Now replace the state simulating config reload/refresh
+        let new_state = LayoutState::default();
+        state = new_state;
+
+        // Assert that the context menu is hidden (cleared)
+        assert!(!cce_ui::widget::context_menu::is_visible());
+    }
 }
+