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

commit166ac401cfbbd0e276e42a001db29d137776e1ef
parenta2f8f3e9aa
authorLucas Galante <[email protected]>
date2026-07-03 17:27
Add right-click Copy Path context menu to Breadcrumb widget

 src/context.rs                     | 15 ++++++++++-
 src/widget/container/breadcrumb.rs | 51 +++++++++++++++++++++++++++++++++++---
 src/widget/core.rs                 |  3 +++
 src/widget/mod.rs                  |  1 +
 4 files changed, 66 insertions(+), 4 deletions(-)

diff --git a/src/context.rs b/src/context.rs
index 5d4afd2..bcd344e 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -777,7 +777,18 @@ impl UiContext {
             return;
         }
         let name = unsafe { (*target).type_name() };
-        let label = unsafe { (*target).label() };
+        let label = if name == "Breadcrumb" {
+            unsafe {
+                if let Some(bc) = (*target).as_any().downcast_ref::<crate::widget::container::Breadcrumb>() {
+                    let idx = bc.right_clicked_seg.unwrap_or(bc.path.len());
+                    Some(bc.path_to_seg(idx))
+                } else {
+                    None
+                }
+            }
+        } else {
+            unsafe { (*target).label() }
+        };
         let header = if let Some(lbl) = label {
             format!("[{}]: {}", name, lbl)
         } else {
@@ -813,6 +824,8 @@ impl UiContext {
             if is_search {
                 options.push("Cear".to_string());
             }
+        } else if name == "Breadcrumb" {
+            options.push("Copy Path".to_string());
         } else {
             options.extend(vec!["Copy".to_string(), "Paste".to_string()]);
         }
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index 05caff9..e554a6d 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -6,9 +6,10 @@ use crate::widget::display::TextLabel;
 pub struct Breadcrumb {
     pub base: Widget,
     hovered: bool,
-    path: Vec<String>,
+    pub path: Vec<String>,
     hovered_seg: Option<usize>,
     clicked_seg: Option<usize>,
+    pub right_clicked_seg: Option<usize>,
     pub network_opacity: f32,
 }
 
@@ -19,7 +20,21 @@ impl Breadcrumb {
 
     pub fn new() -> Self {
         Self { base: Widget::new(), hovered: false,
-               path: Vec::new(), hovered_seg: None, clicked_seg: None, network_opacity: 1.0 }
+               path: Vec::new(), hovered_seg: None, clicked_seg: None, right_clicked_seg: None, network_opacity: 1.0 }
+    }
+
+    pub fn path_to_seg(&self, idx: usize) -> String {
+        let mut path_str = "/".to_string();
+        for (i, s) in self.path.iter().enumerate() {
+            if i + 1 > idx {
+                break;
+            }
+            if path_str != "/" {
+                path_str.push('/');
+            }
+            path_str.push_str(s);
+        }
+        path_str
     }
 
     fn virtual_segs(&self) -> Vec<String> {
@@ -64,7 +79,12 @@ impl Element for Breadcrumb {
         was != self.hovered || old != self.hovered_seg
     }
 
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, _py: f32, _ctx: &mut UiContext) -> bool {
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if button == MouseButton::Right && state == ElementState::Pressed {
+            self.right_clicked_seg = self.seg_at(px);
+            ctx.handle_right_click(self.as_ptr_mut(), px, py);
+            return true;
+        }
         if button != MouseButton::Left || state != ElementState::Pressed { return false; }
         if let Some(i) = self.seg_at(px) {
             if i < self.path.len() {
@@ -75,6 +95,12 @@ impl Element for Breadcrumb {
         false
     }
 
+    fn copy_path(&self) {
+        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);
+    }
+
     fn as_path_controller(&self) -> Option<&dyn PathController> { Some(self) }
     fn as_path_controller_mut(&mut self) -> Option<&mut dyn PathController> { Some(self) }
 
@@ -178,5 +204,24 @@ mod tests {
         assert!(!breadcrumb.mouse_input(crate::widget::MouseButton::Left, crate::widget::ElementState::Pressed, 100.0, 25.0, &mut ui_ctx));
         assert_eq!(breadcrumb.path_click(), None);
     }
+
+    #[test]
+    fn test_breadcrumb_right_clicks() {
+        let mut breadcrumb = Breadcrumb::new();
+        breadcrumb.set_path(&["home".to_string(), "lsgalante".to_string()]);
+        breadcrumb.set_rect(10.0, 20.0, 300.0, 24.0);
+
+        let mut ui_ctx = UiContext::new();
+
+        // Right click segment 1 (home/)
+        let handled = breadcrumb.mouse_input(crate::widget::MouseButton::Right, crate::widget::ElementState::Pressed, 50.0, 25.0, &mut ui_ctx);
+        assert!(handled);
+        assert_eq!(breadcrumb.right_clicked_seg, Some(1));
+
+        // Test path_to_seg
+        assert_eq!(breadcrumb.path_to_seg(0), "/");
+        assert_eq!(breadcrumb.path_to_seg(1), "/home");
+        assert_eq!(breadcrumb.path_to_seg(2), "/home/lsgalante");
+    }
 }
 
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 9e852db..ef84086 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -570,6 +570,9 @@ pub mod context_menu {
                                         "Collapse All" => {
                                             target.collapse_all_nodes();
                                         }
+                                        "Copy Path" => {
+                                            target.copy_path();
+                                        }
                                         _ => {}
                                     }
                                 }
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 0eab028..16a46fd 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -300,6 +300,7 @@ pub trait Element {
     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) {}