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

commit2cd791410d105c1b27ed15b74088875e5c15445c
parentfb04a13533
authorLucas Galante <[email protected]>
date2026-07-12 17:24
refactor(widget)!: context-menu actions become one enum method (6bd phase 1)

The 13 per-action methods (cut_selection..collapse_all_nodes) on
Element, the Input capability hooks, and the Adapted forwards all
collapse into `context_action(ContextAction) -> bool`. The Input
default keeps whole-value Cut/Copy/Paste through the value-string
pair (any widget remains a valid right-click target); TextBox,
TreeList, and Breadcrumb narrow to one hook each (TreeList's seven
action bodies move verbatim to inherent methods via an impl split —
no code movement). core.rs dispatch maps the menu strings (incl. the
load-bearing "Cear") to enum values. Element trait: 91 -> 79 methods.

Verified: 168 tests; workspace builds; live probes — tree Copy Key
("input.accel_speed") / Copy Value ("0.5") / Collapse (group folds),
TextBox Paste ("hello" lands in the search box with just_changed).

 src/widget/container/breadcrumb.rs |   6 ++-
 src/widget/container/treelist.rs   |  21 +++++++-
 src/widget/core.rs                 |  60 +++++++--------------
 src/widget/input/text_box.rs       |  57 +++++++++++---------
 src/widget/mod.rs                  |  53 +++++++++----------
 src/widget/model.rs                | 105 ++++++++++---------------------------
 6 files changed, 126 insertions(+), 176 deletions(-)

diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index 372c133..9c293e4 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -185,10 +185,14 @@ impl Input for Breadcrumb {
     }
 
 
-    fn copy_path(&self) {
+    fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
+        if action != crate::widget::ContextAction::CopyPath {
+            return false;
+        }
         let idx = self.right_clicked_seg.unwrap_or(self.path.len());
         let path_str = self.path_to_seg(idx);
         crate::widget::clipboard::copy_to_clipboard(&path_str);
+        true
     }
 }
 
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 06d33bb..e12add1 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -1117,8 +1117,25 @@ impl Input for TreeList {
         }
     }
 
-    // The tree context-menu actions, dispatched by the global context menu through the
-    // adapter's Element forwards.
+    fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
+        use crate::widget::ContextAction as CA;
+        match action {
+            CA::CopyKey => self.copy_key(),
+            CA::CopyValue => self.copy_value(),
+            CA::DeleteKey => self.delete_key(),
+            CA::ExpandNode => self.expand_node(),
+            CA::CollapseNode => self.collapse_node(),
+            CA::ExpandAll => self.expand_all_nodes(),
+            CA::CollapseAll => self.collapse_all_nodes(),
+            _ => return false,
+        }
+        true
+    }
+}
+
+impl TreeList {
+    // The tree context-menu actions, dispatched by the global context menu through
+    // `Input::context_action`.
     fn copy_key(&self) {
         if let Some(idx) = self.selected_key_idx {
             if idx < self.flat_keys.len() {
diff --git a/src/widget/core.rs b/src/widget/core.rs
index e438b97..45208cb 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -492,47 +492,25 @@ pub mod context_menu {
                             if let Some(target_ptr) = ctx.tree.get_ptr(target_id) {
                                 unsafe {
                                     let target = &mut *target_ptr;
-                                    match opt.as_str() {
-                                        "Cut" => {
-                                            let _ = target.cut_selection();
-                                        }
-                                        "Copy" => {
-                                            target.copy_selection();
-                                        }
-                                        "Paste" => {
-                                            let _ = target.paste_from_clipboard();
-                                        }
-                                        "Select All" => {
-                                            target.select_all();
-                                        }
-                                        "Cear" => {
-                                            target.clear_text();
-                                        }
-                                        "Copy Key" => {
-                                            target.copy_key();
-                                        }
-                                        "Copy Value" => {
-                                            target.copy_value();
-                                        }
-                                        "Delete" => {
-                                            target.delete_key();
-                                        }
-                                        "Expand" => {
-                                            target.expand_node();
-                                        }
-                                        "Collapse" => {
-                                            target.collapse_node();
-                                        }
-                                        "Expand All" => {
-                                            target.expand_all_nodes();
-                                        }
-                                        "Collapse All" => {
-                                            target.collapse_all_nodes();
-                                        }
-                                        "Copy Path" => {
-                                            target.copy_path();
-                                        }
-                                        _ => {}
+                                    use crate::widget::ContextAction as CA;
+                                    let action = match opt.as_str() {
+                                        "Cut" => Some(CA::Cut),
+                                        "Copy" => Some(CA::Copy),
+                                        "Paste" => Some(CA::Paste),
+                                        "Select All" => Some(CA::SelectAll),
+                                        "Cear" => Some(CA::ClearText),
+                                        "Copy Key" => Some(CA::CopyKey),
+                                        "Copy Value" => Some(CA::CopyValue),
+                                        "Delete" => Some(CA::DeleteKey),
+                                        "Expand" => Some(CA::ExpandNode),
+                                        "Collapse" => Some(CA::CollapseNode),
+                                        "Expand All" => Some(CA::ExpandAll),
+                                        "Collapse All" => Some(CA::CollapseAll),
+                                        "Copy Path" => Some(CA::CopyPath),
+                                        _ => None,
+                                    };
+                                    if let Some(action) = action {
+                                        let _ = target.context_action(action);
                                     }
                                 }
                             }
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index e3a4ca4..145ed06 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1406,32 +1406,37 @@ impl Input for TextBox {
         self.set_value(val)
     }
 
-    fn cut_selection(&mut self) -> bool {
-        let res = self.cut_selection();
-        if res {
-            self.just_changed = true;
-        }
-        res
-    }
-
-    fn copy_selection(&self) {
-        self.copy_selection();
-    }
-
-    fn paste_from_clipboard(&mut self) -> bool {
-        let res = self.paste_from_clipboard();
-        if res {
-            self.just_changed = true;
+    fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
+        use crate::widget::ContextAction as CA;
+        match action {
+            CA::Cut => {
+                let res = self.cut_selection();
+                if res {
+                    self.just_changed = true;
+                }
+                res
+            }
+            CA::Copy => {
+                self.copy_selection();
+                true
+            }
+            CA::Paste => {
+                let res = self.paste_from_clipboard();
+                if res {
+                    self.just_changed = true;
+                }
+                res
+            }
+            CA::SelectAll => {
+                self.select_all();
+                true
+            }
+            CA::ClearText => {
+                self.set_value("");
+                true
+            }
+            _ => false,
         }
-        res
-    }
-
-    fn select_all(&mut self) {
-        self.select_all();
-    }
-
-    fn clear_text(&mut self) {
-        self.set_value("");
     }
 
     fn draggable(&self, _rect: Rect) -> bool {
@@ -1691,7 +1696,7 @@ mod tests {
         assert!(opts.contains(&"Cear".to_string()));
 
         // Simulate choosing the "Cear" option
-        Element::clear_text(&mut tb);
+        Element::context_action(&mut tb, crate::widget::ContextAction::ClearText);
         assert_eq!(tb.text, "");
         assert_eq!(tb.edit_buffer, "");
     }
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index c54e63a..38ab595 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -75,6 +75,25 @@ pub enum Justification {
     Right,
 }
 
+/// A context-menu action dispatched on the menu's target widget (6bd phase 1: one enum
+/// replaces the 13 per-action `Element` methods). `ClearText` is the search-box "Cear" item.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum ContextAction {
+    Cut,
+    Copy,
+    Paste,
+    SelectAll,
+    ClearText,
+    CopyKey,
+    CopyValue,
+    CopyPath,
+    DeleteKey,
+    ExpandNode,
+    CollapseNode,
+    ExpandAll,
+    CollapseAll,
+}
+
 use crate::colors;
 use std::sync::atomic::AtomicUsize;
 use std::collections::HashMap;
@@ -285,36 +304,12 @@ pub trait Element {
     fn set_value_string(&mut self, _val: &str) -> bool { false }
     fn take_change(&mut self) -> bool { false }
 
-    fn cut_selection(&mut self) -> bool {
-        if let Some(val) = self.get_value_string() {
-            clipboard::copy_to_clipboard(&val);
-            self.set_value_string("")
-        } else {
-            false
-        }
-    }
-    fn copy_selection(&self) {
-        if let Some(val) = self.get_value_string() {
-            clipboard::copy_to_clipboard(&val);
-        }
-    }
-    fn paste_from_clipboard(&mut self) -> bool {
-        if let Some(text) = clipboard::read_from_clipboard() {
-            self.set_value_string(&text)
-        } else {
-            false
-        }
+    /// Dispatch a context-menu action on this widget. Returns whether it was applied.
+    /// Default inert; the adapter forwards to `Input::context_action` (whose default gives
+    /// every widget whole-value Cut/Copy/Paste through the value-string pair).
+    fn context_action(&mut self, _action: ContextAction) -> bool {
+        false
     }
-    fn select_all(&mut self) {}
-    fn clear_text(&mut self) {}
-    fn copy_key(&self) {}
-    fn copy_value(&self) {}
-    fn copy_path(&self) {}
-    fn delete_key(&mut self) {}
-    fn expand_node(&mut self) {}
-    fn collapse_node(&mut self) {}
-    fn expand_all_nodes(&mut self) {}
-    fn collapse_all_nodes(&mut self) {}
 
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
         if let Some(b) = self.base_mut() {
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 0b87c04..37885f2 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -491,28 +491,34 @@ pub trait Input {
     // before these hooks existed keep their exact behavior; TextBox overrides with real
     // selection-aware implementations.
 
-    fn cut_selection(&mut self) -> bool {
-        if let Some(val) = self.value_string() {
-            crate::widget::clipboard::copy_to_clipboard(&val);
-            self.set_value_string("")
-        } else {
-            false
-        }
-    }
-    fn copy_selection(&self) {
-        if let Some(val) = self.value_string() {
-            crate::widget::clipboard::copy_to_clipboard(&val);
-        }
-    }
-    fn paste_from_clipboard(&mut self) -> bool {
-        if let Some(text) = crate::widget::clipboard::read_from_clipboard() {
-            self.set_value_string(&text)
-        } else {
-            false
+    fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
+        match action {
+            crate::widget::ContextAction::Cut => {
+                if let Some(val) = self.value_string() {
+                    crate::widget::clipboard::copy_to_clipboard(&val);
+                    self.set_value_string("")
+                } else {
+                    false
+                }
+            }
+            crate::widget::ContextAction::Copy => {
+                if let Some(val) = self.value_string() {
+                    crate::widget::clipboard::copy_to_clipboard(&val);
+                    true
+                } else {
+                    false
+                }
+            }
+            crate::widget::ContextAction::Paste => {
+                if let Some(text) = crate::widget::clipboard::read_from_clipboard() {
+                    self.set_value_string(&text)
+                } else {
+                    false
+                }
+            }
+            _ => false,
         }
     }
-    fn select_all(&mut self) {}
-    fn clear_text(&mut self) {}
 
     /// Whether direct `focus()`/`unfocus()` calls flip the base `focused` flag. Legacy widgets
     /// differ: most set it in their `focus` overrides, but TextBox never did — its detached
@@ -588,22 +594,6 @@ pub trait Input {
     // concrete `Adapted<W>` (or a `&dyn XController` held directly), per RFC §3.5.
 
 
-    /// Copy this widget's path/content to the clipboard — the context menu's "Copy Path" action
-    /// calls `Element::copy_path` on its target (Breadcrumb is the only implementor).
-    fn copy_path(&self) {}
-
-    /// The tree context-menu actions ("Copy Key" / "Copy Value" / "Delete" / "Expand" /
-    /// "Collapse" / "Expand All" / "Collapse All") — the global context menu dispatches them
-    /// on its `dyn Element` target; `Adapted` forwards here. TreeList is the only implementor
-    /// (transitional, dies with the `Element` deletion like the controller hooks above).
-    fn copy_key(&self) {}
-    fn copy_value(&self) {}
-    fn delete_key(&mut self) {}
-    fn expand_node(&mut self) {}
-    fn collapse_node(&mut self) {}
-    fn expand_all_nodes(&mut self) {}
-    fn collapse_all_nodes(&mut self) {}
-
     /// Keyboard modifier state pushed in by hosts before dispatch (legacy
     /// `Element::set_modifiers`).
     fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
@@ -1348,20 +1338,8 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn set_selected(&mut self, selected: bool) {
         Input::set_selected(&mut self.inner, selected)
     }
-    fn cut_selection(&mut self) -> bool {
-        Input::cut_selection(&mut self.inner)
-    }
-    fn copy_selection(&self) {
-        Input::copy_selection(&self.inner)
-    }
-    fn paste_from_clipboard(&mut self) -> bool {
-        Input::paste_from_clipboard(&mut self.inner)
-    }
-    fn select_all(&mut self) {
-        Input::select_all(&mut self.inner)
-    }
-    fn clear_text(&mut self) {
-        Input::clear_text(&mut self.inner)
+    fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
+        Input::context_action(&mut self.inner, action)
     }
     /// Row-rect assignment (row-layout hosts): apply the widget's clamp
     /// ([`Layout::adjust_row_rect`] — TextBox's `width`/`max_width`), then the base write the
@@ -1422,33 +1400,6 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
         Input::set_drag_bounds(&mut self.inner, bx, by, bw, bh)
     }
 
-    // --- Controller downcasts -> the `Input` capability hooks ---
-    fn copy_path(&self) {
-        Input::copy_path(&self.inner)
-    }
-
-    fn copy_key(&self) {
-        Input::copy_key(&self.inner)
-    }
-    fn copy_value(&self) {
-        Input::copy_value(&self.inner)
-    }
-    fn delete_key(&mut self) {
-        Input::delete_key(&mut self.inner)
-    }
-    fn expand_node(&mut self) {
-        Input::expand_node(&mut self.inner)
-    }
-    fn collapse_node(&mut self) {
-        Input::collapse_node(&mut self.inner)
-    }
-    fn expand_all_nodes(&mut self) {
-        Input::expand_all_nodes(&mut self.inner)
-    }
-    fn collapse_all_nodes(&mut self) {
-        Input::collapse_all_nodes(&mut self.inner)
-    }
-
     // --- Legacy direct-dispatch entry points. Hosts (treelist's add-key button, parameters_bg's
     // checkboxes, app pages) call these ON the widget instead of routing an Event through
     // `propagate_event`; without these overrides they'd hit the inert Element defaults and the