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

commite6af8157dae6ec7a647186789895e9958bf16e00
parentf850b8a9cb
authorLucas Galante <[email protected]>
date2026-07-03 01:57
Added support for separators in Dropdown widget

 src/widget/input/dropdown.rs | 98 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 84 insertions(+), 14 deletions(-)

diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index c5db180..70c22ba 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -104,6 +104,12 @@ impl Dropdown {
         
         for (idx, opt) in self.options.iter().enumerate() {
             let iy = crate::layout::align_text_y(dy + idx as f32 * 24.0, 24.0, 12.0, 0.0);
+            
+            if opt == "-" {
+                pc.rect(theme.surface_border, self.base.x + 8.0, dy + idx as f32 * 24.0 + 11.5, pw - 16.0, 1.0);
+                continue;
+            }
+
             let text_color = if self.hovered_item == Some(idx) {
                 [0xff, 0xff, 0xff]
             } else if self.selected == idx {
@@ -248,7 +254,9 @@ impl Element for Dropdown {
             if px >= x && px <= x + pw && py >= dy && py <= dy + dh {
                 let idx = ((py - dy) / 24.0) as usize;
                 if idx < self.options.len() {
-                    self.hovered_item = Some(idx);
+                    if self.options[idx] != "-" {
+                        self.hovered_item = Some(idx);
+                    }
                 }
             }
         }
@@ -276,6 +284,9 @@ impl Element for Dropdown {
         if inside_popover {
             let idx = ((py - dy) / 24.0) as usize;
             if idx < self.options.len() {
+                if self.options[idx] == "-" {
+                    return true;
+                }
                 if self.selected != idx || self.custom_display_text.is_some() {
                     self.selected = idx;
                     self.just_changed = true;
@@ -316,7 +327,16 @@ impl Element for Dropdown {
         if !self.open {
             if let Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) = event.logical_key {
                 self.open = true;
-                self.hovered_item = Some(self.selected);
+                let mut start_idx = self.selected;
+                if start_idx < self.options.len() && self.options[start_idx] == "-" {
+                    for i in 0..self.options.len() {
+                        if self.options[i] != "-" {
+                            start_idx = i;
+                            break;
+                        }
+                    }
+                }
+                self.hovered_item = Some(start_idx);
                 return true;
             }
             return false;
@@ -325,30 +345,38 @@ impl Element for Dropdown {
         match event.logical_key {
             Key::Named(NamedKey::ArrowDown) => {
                 let current = self.hovered_item.unwrap_or(self.selected);
-                if current + 1 < self.options.len() {
-                    self.hovered_item = Some(current + 1);
-                } else {
-                    self.hovered_item = Some(0);
+                let mut next = (current + 1) % self.options.len();
+                for _ in 0..self.options.len() {
+                    if self.options[next] != "-" {
+                        self.hovered_item = Some(next);
+                        break;
+                    }
+                    next = (next + 1) % self.options.len();
                 }
                 true
             }
             Key::Named(NamedKey::ArrowUp) => {
                 let current = self.hovered_item.unwrap_or(self.selected);
-                if current > 0 {
-                    self.hovered_item = Some(current - 1);
-                } else {
-                    self.hovered_item = Some(self.options.len() - 1);
+                let mut prev = if current == 0 { self.options.len() - 1 } else { current - 1 };
+                for _ in 0..self.options.len() {
+                    if self.options[prev] != "-" {
+                        self.hovered_item = Some(prev);
+                        break;
+                    }
+                    prev = if prev == 0 { self.options.len() - 1 } else { prev - 1 };
                 }
                 true
             }
             Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
                 if let Some(idx) = self.hovered_item {
-                    if self.selected != idx || self.custom_display_text.is_some() {
-                        self.selected = idx;
-                        self.just_changed = true;
+                    if idx < self.options.len() && self.options[idx] != "-" {
+                        if self.selected != idx || self.custom_display_text.is_some() {
+                            self.selected = idx;
+                            self.just_changed = true;
+                        }
+                        self.open = false;
                     }
                 }
-                self.open = false;
                 true
             }
             Key::Named(NamedKey::Escape) => {
@@ -503,5 +531,47 @@ mod tests {
         crate::widget::context_menu::hide();
         assert!(!crate::widget::context_menu::is_visible());
     }
+
+    #[test]
+    fn test_dropdown_separators() {
+        let mut dummy = crate::context::UiContext::new();
+        let options = vec![
+            "Option A".to_string(),
+            "-".to_string(),
+            "Option B".to_string(),
+        ];
+        let mut dd = Dropdown::new(options, 0);
+        dd.set_rect(10.0, 10.0, 100.0, 24.0);
+
+        // Open dropdown
+        dd.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0, &mut dummy);
+        assert!(dd.open);
+
+        // Hover over separator at index 1 at y = 34 + 24 + 12 = 70.0
+        dd.on_cursor_moved(50.0, 70.0, &mut dummy);
+        assert_eq!(dd.hovered_item, None); // Separator should not be hovered
+
+        // Click separator at index 1
+        let clicked = dd.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 70.0, &mut dummy);
+        assert!(clicked);
+        assert!(dd.open); // Dropdown should remain open
+        assert_eq!(dd.selected, 0); // Selection should not change
+
+        // Hover over Option B at index 2 at y = 34 + 48 + 12 = 94.0
+        dd.on_cursor_moved(50.0, 94.0, &mut dummy);
+        assert_eq!(dd.hovered_item, Some(2));
+
+        // Keyboard arrow up from index 2 should skip separator (index 1) and go to index 0
+        let key_up = crate::widget::KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Named(NamedKey::ArrowUp),
+            text: None,
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        dd.keyboard_input(&key_up, &mut dummy);
+        assert_eq!(dd.hovered_item, Some(0));
+    }
 }