git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commite45cd469043761a26ca9b2bf2c9ee37066d2f6f9
parent2c59f3a2a2
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-25 12:25
fix(context_menu): a menu with a slider row no longer panics on paint

The slider rows' paint stamp is dropped at the end of paint, under the
CONTEXT_MENU borrow paint_with_labels holds, and its Drop clears widget
references in that same cell: borrow_mut panicked on every paint of such
a menu, killing the designer on any viewport right-click. An already
borrowed cell means the drop came from inside the menu's own code, which
cannot be dropping the menu's target, so there is nothing to clear.

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

 src/widget/core.rs | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/widget/core.rs b/src/widget/core.rs
index f6d4068..860b53b 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -924,7 +924,13 @@ pub mod context_menu {
     pub fn clear_if_matches(w: &dyn WidgetHost) {
         let id = w.base().id();
         CONTEXT_MENU.with(|m| {
-            let mut menu = m.borrow_mut();
+            // Already borrowed means the widget is being dropped from INSIDE
+            // the menu's own code — the slider rows' paint stamp, dropped at
+            // the end of `paint` under `paint_with_labels`' borrow. A widget
+            // the menu made for itself cannot be its target, so there is
+            // nothing to clear; `borrow_mut` here panicked on every paint of
+            // a menu with a slider row.
+            let Ok(mut menu) = m.try_borrow_mut() else { return };
             if menu.target == Some(id) {
                 menu.target = None;
                 menu.visible = false;
@@ -1120,6 +1126,22 @@ mod context_menu_slider_tests {
         m.row_y(idx) + ROW_H * 0.5
     }
 
+    /// Painted through the thread-local, as every host paints it: the slider
+    /// stamp is dropped while `CONTEXT_MENU` is borrowed, and its drop clears
+    /// widget references in that same cell. The tests above paint a bare
+    /// `ContextMenuState` and never held the borrow.
+    #[test]
+    fn a_slider_row_paints_through_the_shared_menu() {
+        use super::context_menu as cm;
+        cm::show(100.0, 50.0, vec!["Frame All".into(), "Opacity".into()], 0, WidgetId(7));
+        cm::set_row_slider(1, MenuSlider { value: 50.0, min: 0.0, max: 100.0, step: 5.0, decimals: 0, suffix: "%" });
+        let mut pc = crate::scene::paint::PaintCtx::new();
+        cm::paint_with_labels(&mut pc);
+        cm::paint(&mut pc);
+        assert!(cm::is_visible(), "painting leaves the menu up");
+        cm::hide();
+    }
+
     /// A notch over the slider row steps it by `step`, up is more, and the
     /// change is reported once; over an action row the wheel is not the
     /// menu's. A trackpad's fractions add up to whole steps.