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

commitd18d72d16e1e2dc63f5a42617916fde21d76a449
parente173a5c19c
authorLucas Galante <[email protected]>
date2026-06-22 14:55
Add config metadata and display file/key info in widget context menu

 src/context.rs                     | 31 ++++++++++++++++++++++++-------
 src/widget/core.rs                 | 21 +++++++++++++++------
 src/widget/input/checkbox.rs       | 12 ++++++++++++
 src/widget/input/color_selector.rs |  6 ++++++
 src/widget/input/dropdown.rs       | 29 +++++++++++++++++++++++++++++
 src/widget/input/font_selector.rs  |  6 ++++++
 src/widget/input/slider.rs         |  6 ++++++
 src/widget/input/spinbox.rs        |  6 ++++++
 src/widget/input/text_box.rs       |  6 ++++++
 9 files changed, 110 insertions(+), 13 deletions(-)

diff --git a/src/context.rs b/src/context.rs
index 3236f80..e8d5d3b 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -410,8 +410,8 @@ impl UiContext {
         crate::widget::context_menu::is_visible()
     }
 
-    pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
-        crate::widget::context_menu::show(x, y, options, target);
+    pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, header_count: usize, target: *mut (dyn Element + 'static)) {
+        crate::widget::context_menu::show(x, y, options, header_count, target);
     }
 
     pub fn handle_right_click(&mut self, target: *mut (dyn Element + 'static), px: f32, py: f32) {
@@ -426,15 +426,32 @@ impl UiContext {
             format!("[{}]", name)
         };
 
-        let options = if name == "TextBox" {
-            vec![header, "Cut".to_string(), "Copy".to_string(), "Paste".to_string(), "Select All".to_string()]
+        let mut config_info = None;
+        if let Some(b) = unsafe { (*target).base() } {
+            if let (Some(ref file), Some(ref key)) = (&b.config_file, &b.config_key) {
+                config_info = Some((file.clone(), key.clone()));
+            }
+        }
+
+        let mut options = Vec::new();
+        options.push(header);
+        let mut header_count = 1;
+
+        if let Some((file, key)) = config_info {
+            options.push(format!("File: {}", file));
+            options.push(format!("Key: {}", key));
+            header_count = 3;
+        }
+
+        if name == "TextBox" {
+            options.extend(vec!["Cut".to_string(), "Copy".to_string(), "Paste".to_string(), "Select All".to_string()]);
         } else {
-            vec![header, "Copy".to_string(), "Paste".to_string()]
-        };
+            options.extend(vec!["Copy".to_string(), "Paste".to_string()]);
+        }
 
         let scroll_y = crate::widget::hover_animation::get_scroll_offset();
         let adjusted_py = py - scroll_y;
-        crate::widget::context_menu::show(px, adjusted_py, options, target);
+        crate::widget::context_menu::show(px, adjusted_py, options, header_count, target);
     }
 
     pub fn hide_context_menu(&mut self) {
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 7b1cd27..45ec05e 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -457,6 +457,7 @@ pub mod context_menu {
         pub options: Vec<String>,
         pub hovered_item: Option<usize>,
         pub target: Option<*mut (dyn Element + 'static)>,
+        pub header_count: usize,
     }
 
     impl ContextMenuState {
@@ -470,10 +471,11 @@ pub mod context_menu {
                 options: Vec::new(),
                 hovered_item: None,
                 target: None,
+                header_count: 0,
             }
         }
 
-        pub fn show(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
+        pub fn show(&mut self, x: f32, y: f32, options: Vec<String>, header_count: usize, target: *mut (dyn Element + 'static)) {
             if target.is_null() {
                 return;
             }
@@ -486,6 +488,7 @@ pub mod context_menu {
             self.visible = true;
             self.hovered_item = None;
             self.target = Some(target);
+            self.header_count = header_count;
         }
 
         pub fn hide(&mut self) {
@@ -504,7 +507,7 @@ pub mod context_menu {
             self.hovered_item = None;
             if px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h {
                 let idx = ((py - self.y) / 24.0) as usize;
-                if idx < self.options.len() && idx > 0 {
+                if idx < self.options.len() && idx >= self.header_count {
                     self.hovered_item = Some(idx);
                 }
             }
@@ -524,7 +527,7 @@ pub mod context_menu {
             if px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h {
                 let idx = ((py - self.y) / 24.0) as usize;
                 if idx < self.options.len() {
-                    if idx > 0 {
+                    if idx >= self.header_count {
                         let opt = self.options[idx].clone();
                         if let Some(target_ptr) = self.target {
                             if !target_ptr.is_null() {
@@ -581,7 +584,7 @@ pub mod context_menu {
 
             for (idx, opt) in self.options.iter().enumerate() {
                 let iy = self.y + idx as f32 * 24.0 + (24.0 - 12.0) / 2.0;
-                let text_color = if idx == 0 {
+                let text_color = if idx < self.header_count {
                     [0x70, 0x70, 0x78]
                 } else if self.hovered_item == Some(idx) {
                     [0xff, 0xff, 0xff]
@@ -609,8 +612,8 @@ pub mod context_menu {
         CONTEXT_MENU.with(|m| m.borrow().visible)
     }
 
-    pub fn show(x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
-        CONTEXT_MENU.with(|m| m.borrow_mut().show(x, y, options, target));
+    pub fn show(x: f32, y: f32, options: Vec<String>, header_count: usize, target: *mut (dyn Element + 'static)) {
+        CONTEXT_MENU.with(|m| m.borrow_mut().show(x, y, options, header_count, target));
     }
 
     pub fn hide() {
@@ -672,6 +675,8 @@ pub struct Widget {
     pub focused: bool,
     pub id: std::cell::Cell<Option<crate::widget::WidgetId>>,
     pub dirty: bool,
+    pub config_file: Option<String>,
+    pub config_key: Option<String>,
 }
 
 impl Widget {
@@ -688,6 +693,8 @@ impl Widget {
             focused: false,
             id: std::cell::Cell::new(None),
             dirty: true,
+            config_file: None,
+            config_key: None,
         }
     }
 
@@ -704,6 +711,8 @@ impl Widget {
             focused: false,
             id: std::cell::Cell::new(None),
             dirty: true,
+            config_file: None,
+            config_key: None,
         }
     }
 
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index ffd9464..b79c926 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -23,6 +23,12 @@ impl Checkbox {
         self
     }
 
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
+        self
+    }
+
     pub fn set_checked(&mut self, checked: bool) {
         self.checked = checked;
     }
@@ -196,6 +202,12 @@ impl Toggle {
         self
     }
 
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
+        self
+    }
+
     pub fn set_label(&mut self, label: &str) {
         self.base.label = Some(label.to_string());
     }
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index baacbaf..d009036 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -93,6 +93,12 @@ impl ColorSelector {
         self
     }
 
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
+        self
+    }
+
     pub fn with_font_family(mut self, font_family: &str) -> Self {
         self.font_family = font_family.to_string();
         self
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 40cff69..ffdbcbd 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -39,6 +39,12 @@ impl Dropdown {
         self
     }
 
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
+        self
+    }
+
     pub fn set_label(&mut self, label: &str) {
         self.base.label = Some(label.to_string());
     }
@@ -437,5 +443,28 @@ mod tests {
         assert_eq!(dd.selected, 1);
         assert!(dd.take_change());
     }
+
+    #[test]
+    fn test_dropdown_context_menu_with_config() {
+        let mut dummy = crate::context::UiContext::new();
+        let options = vec!["Option A".to_string(), "Option B".to_string()];
+        let mut dd = Dropdown::new(options, 0).with_config("path/to/config.json", "some_key");
+        dd.set_rect(10.0, 10.0, 100.0, 24.0);
+
+        assert!(!crate::widget::context_menu::is_visible());
+
+        // Right click dropdown
+        let handled = dd.mouse_input(MouseButton::Right, ElementState::Pressed, 50.0, 20.0, &mut dummy);
+        assert!(handled);
+
+        assert!(crate::widget::context_menu::is_visible());
+        let menu_options = crate::widget::context_menu::options();
+        assert!(menu_options.len() >= 3);
+        assert_eq!(menu_options[1], "File: path/to/config.json");
+        assert_eq!(menu_options[2], "Key: some_key");
+
+        crate::widget::context_menu::hide();
+        assert!(!crate::widget::context_menu::is_visible());
+    }
 }
 
diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index dd50b2a..b436d8a 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -27,6 +27,12 @@ impl FontSelector {
         self
     }
 
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
+        self
+    }
+
     pub fn take_change(&mut self) -> bool {
         let changed = self.just_changed;
         self.just_changed = false;
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index e8d23f6..40a71fe 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -46,6 +46,12 @@ impl Slider {
         self
     }
 
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
+        self
+    }
+
     pub fn set_label(&mut self, label: &str) {
         self.base.label = Some(label.to_string());
     }
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index 102e7f5..3d79fc5 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -46,6 +46,12 @@ impl Spinbox {
         self
     }
 
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
+        self
+    }
+
     pub fn set_label(&mut self, label: &str) {
         self.base.label = Some(label.to_string());
     }
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index f01b962..98c6799 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -191,6 +191,12 @@ impl TextBox {
         self
     }
 
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
+        self
+    }
+
     pub fn set_label(&mut self, label: &str) {
         self.base.label = Some(label.to_string());
     }