git.lucas.co / cce-graph
node-based graph editor
git clone https://git.lucas.co/cce-graph.git

commit9e9218da425430a7c26a5601b2f862fcfd33b0e0
parentb3f8d971b0
authorLucas Galante <[email protected]>
date2026-07-03 15:54
feat: replace native MenuBar items with separate Dropdowns

 src/main.rs | 251 ++++++++++++++++++++++++++++++++++++++++--------------------
 1 file changed, 167 insertions(+), 84 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 62820bc..00caad9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
 use wayland_client::QueueHandle;
 use glyphon::{FontSystem, Buffer, Metrics, Attrs};
 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
-use cce_ui::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element, Graph, GraphNode, MenuBar, MenuController, GraphController, WidgetId};
+use cce_ui::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element, Graph, GraphNode, MenuBar, GraphController, WidgetId, Dropdown};
 use image::GenericImageView;
 
 #[derive(Debug, Clone)]
@@ -48,6 +48,9 @@ struct LoadedImage {
 
 struct GraphApp {
     menu_bar: MenuBar,
+    dropdown_file: Dropdown,
+    dropdown_edit: Dropdown,
+    dropdown_view: Dropdown,
     graph: Graph,
     graph_id: WidgetId,
     text_items: Vec<TextItem>,
@@ -64,6 +67,16 @@ struct GraphApp {
     loaded_images: Vec<LoadedImage>,
 }
 
+fn get_view_options(show_grid: bool, uniform_bg: bool, opacity: f32) -> Vec<String> {
+    vec![
+        format!("{} Show Grid", if show_grid { "✓" } else { "  " }),
+        format!("{} Uniform Background", if uniform_bg { "✓" } else { "  " }),
+        format!("{} Opacity: 95%", if (opacity - 0.95).abs() < 0.05 { "✓" } else { "  " }),
+        format!("{} Opacity: 75%", if (opacity - 0.75).abs() < 0.05 { "✓" } else { "  " }),
+        format!("{} Opacity: 50%", if (opacity - 0.50).abs() < 0.05 { "✓" } else { "  " }),
+    ]
+}
+
 fn load_image_pixels(path: &std::path::Path) -> Option<(Vec<[u8; 4]>, u32, u32)> {
     let img = image::open(path).ok()?;
     let max_dim = 96;
@@ -82,6 +95,10 @@ fn load_image_pixels(path: &std::path::Path) -> Option<(Vec<[u8; 4]>, u32, u32)>
 }
 
 impl GraphApp {
+    fn update_view_options(&mut self) {
+        self.dropdown_view.options = get_view_options(self.show_grid, self.uniform_background, self.opacity);
+    }
+
     fn rebuild_text_items(&mut self) {
         self.text_items.clear();
         
@@ -91,6 +108,11 @@ impl GraphApp {
         // 2. Collect labels from MenuBar widget
         labels.extend(self.menu_bar.text_labels_with_bounds(&self.ui_context));
         
+        // 3. Collect labels from Dropdown widgets
+        labels.extend(self.dropdown_file.text_labels_with_bounds(&self.ui_context));
+        labels.extend(self.dropdown_edit.text_labels_with_bounds(&self.ui_context));
+        labels.extend(self.dropdown_view.text_labels_with_bounds(&self.ui_context));
+        
         let scale = cce_ui::scale::scale_factor();
         for (label, bounds) in labels {
             let physical_size = label.font_size * scale;
@@ -200,12 +222,8 @@ impl GraphApp {
         self.graph.set_uniform_background(self.uniform_background);
         self.graph.set_network_opacity(self.opacity);
 
-        // Update menu bar checkboxes
-        self.menu_bar.set_item_checked(2, 0, self.show_grid);
-        self.menu_bar.set_item_checked(2, 1, self.uniform_background);
-        self.menu_bar.set_item_checked(2, 2, (self.opacity - 0.95).abs() < 0.05);
-        self.menu_bar.set_item_checked(2, 3, (self.opacity - 0.75).abs() < 0.05);
-        self.menu_bar.set_item_checked(2, 4, (self.opacity - 0.50).abs() < 0.05);
+        // Update view dropdown options
+        self.update_view_options();
 
         self.loaded_project_path = Some(project_dir);
         self.needs_rebuild = true;
@@ -306,29 +324,44 @@ impl Application for GraphApp {
         graph.set_network_opacity(opacity);
 
         // Build Menu Bar with options to toggle new features
-        let mut menu_bar = MenuBar::new(0.0, 0.0, 1024.0, 42.0)
-            .with_color([0.08, 0.08, 0.12, 1.0])
-            .with_title("cce-graph")
-            .with_item("File", &["New", "Open", "Save", "Save As", "Exit"])
-            .with_item("Edit", &["Add Node", "Add Image"])
-            .with_item("View", &[
-                "Show Grid",
-                "Uniform Background",
-                "Opacity: 95%",
-                "Opacity: 75%",
-                "Opacity: 50%"
-            ]);
-            
-        menu_bar.set_item_checked(2, 0, show_grid);  // Show Grid checked
-        menu_bar.set_item_checked(2, 1, uniform_background); // Uniform Background unchecked
-        menu_bar.set_item_checked(2, 2, (opacity - 0.95).abs() < 0.05);  // Opacity 95% checked
-        menu_bar.set_item_checked(2, 3, (opacity - 0.75).abs() < 0.05);  // Opacity 75% checked
-        menu_bar.set_item_checked(2, 4, (opacity - 0.50).abs() < 0.05);  // Opacity 50% checked
+        // Build Menu Bar background
+        let menu_bar = MenuBar::new(0.0, 0.0, 1024.0, 42.0)
+            .with_color([0.08, 0.08, 0.12, 1.0]);
+
+        let dropdown_file = Dropdown::new(
+            vec![
+                "New".to_string(),
+                "Open".to_string(),
+                "Save".to_string(),
+                "Save As".to_string(),
+                "Exit".to_string(),
+            ],
+            999,
+        )
+        .with_custom_display_text("File");
+
+        let dropdown_edit = Dropdown::new(
+            vec![
+                "Add Node".to_string(),
+                "Add Image".to_string(),
+            ],
+            999,
+        )
+        .with_custom_display_text("Edit");
+
+        let dropdown_view = Dropdown::new(
+            get_view_options(show_grid, uniform_background, opacity),
+            999,
+        )
+        .with_custom_display_text("View");
 
         let graph_id = WidgetId(cce_ui::widget::NEXT_WIDGET_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst));
 
         let mut app = Self {
             menu_bar,
+            dropdown_file,
+            dropdown_edit,
+            dropdown_view,
             graph,
             graph_id,
             text_items: Vec::new(),
@@ -346,6 +379,9 @@ impl Application for GraphApp {
         };
         
         app.menu_bar.set_rect(0.0, 0.0, 1024.0, 42.0);
+        app.dropdown_file.set_rect(10.0, 8.0, 70.0, 26.0);
+        app.dropdown_edit.set_rect(90.0, 8.0, 70.0, 26.0);
+        app.dropdown_view.set_rect(170.0, 8.0, 70.0, 26.0);
         app.graph.set_rect(0.0, 42.0, 1024.0, 768.0 - 42.0);
         app.rebuild_text_items();
         app
@@ -415,7 +451,7 @@ impl Application for GraphApp {
             AppMessage::ToggleGrid => {
                 self.show_grid = !self.show_grid;
                 self.graph.set_show_network_grid(self.show_grid);
-                self.menu_bar.set_item_checked(2, 0, self.show_grid);
+                self.update_view_options();
                 write_config_value("graph_show_grid", &self.show_grid.to_string());
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
@@ -423,7 +459,7 @@ impl Application for GraphApp {
             AppMessage::ToggleUniformBackground => {
                 self.uniform_background = !self.uniform_background;
                 self.graph.set_uniform_background(self.uniform_background);
-                self.menu_bar.set_item_checked(2, 1, self.uniform_background);
+                self.update_view_options();
                 write_config_value("style.surface.graph.uniform_background", &self.uniform_background.to_string());
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
@@ -431,9 +467,7 @@ impl Application for GraphApp {
             AppMessage::SetOpacity95 => {
                 self.opacity = 0.95;
                 self.graph.set_network_opacity(0.95);
-                self.menu_bar.set_item_checked(2, 2, true);
-                self.menu_bar.set_item_checked(2, 3, false);
-                self.menu_bar.set_item_checked(2, 4, false);
+                self.update_view_options();
                 write_config_value("graph_network_opacity", "0.95");
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
@@ -441,9 +475,7 @@ impl Application for GraphApp {
             AppMessage::SetOpacity75 => {
                 self.opacity = 0.75;
                 self.graph.set_network_opacity(0.75);
-                self.menu_bar.set_item_checked(2, 2, false);
-                self.menu_bar.set_item_checked(2, 3, true);
-                self.menu_bar.set_item_checked(2, 4, false);
+                self.update_view_options();
                 write_config_value("graph_network_opacity", "0.75");
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
@@ -451,9 +483,7 @@ impl Application for GraphApp {
             AppMessage::SetOpacity50 => {
                 self.opacity = 0.50;
                 self.graph.set_network_opacity(0.50);
-                self.menu_bar.set_item_checked(2, 2, false);
-                self.menu_bar.set_item_checked(2, 3, false);
-                self.menu_bar.set_item_checked(2, 4, true);
+                self.update_view_options();
                 write_config_value("graph_network_opacity", "0.50");
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
@@ -497,13 +527,28 @@ impl Application for GraphApp {
 
         // Register widgets with correct, stable self addresses
         let menu_ptr = &mut self.menu_bar as *mut MenuBar as *mut (dyn Element + 'static);
+        let file_ptr = &mut self.dropdown_file as *mut Dropdown as *mut (dyn Element + 'static);
+        let edit_ptr = &mut self.dropdown_edit as *mut Dropdown as *mut (dyn Element + 'static);
+        let view_ptr = &mut self.dropdown_view as *mut Dropdown as *mut (dyn Element + 'static);
         let graph_ptr = &mut self.graph as *mut Graph as *mut (dyn Element + 'static);
+
         self.ui_context.register_widget(self.menu_bar.base.id(), menu_ptr);
+        self.ui_context.register_widget(self.dropdown_file.base().unwrap().id(), file_ptr);
+        self.ui_context.register_widget(self.dropdown_edit.base().unwrap().id(), edit_ptr);
+        self.ui_context.register_widget(self.dropdown_view.base().unwrap().id(), view_ptr);
         self.ui_context.register_widget(self.graph_id, graph_ptr);
 
-        if self.menu_bar.popover_rect().is_some() {
-            self.ui_context.register_popover(&self.menu_bar);
-            cce_ui::widget::popovers::register(&self.menu_bar);
+        if self.dropdown_file.popover_rect().is_some() {
+            self.ui_context.register_popover(&self.dropdown_file);
+            cce_ui::widget::popovers::register(&self.dropdown_file);
+        }
+        if self.dropdown_edit.popover_rect().is_some() {
+            self.ui_context.register_popover(&self.dropdown_edit);
+            cce_ui::widget::popovers::register(&self.dropdown_edit);
+        }
+        if self.dropdown_view.popover_rect().is_some() {
+            self.ui_context.register_popover(&self.dropdown_view);
+            cce_ui::widget::popovers::register(&self.dropdown_view);
         }
 
         let size_changed = self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale;
@@ -515,6 +560,11 @@ impl Application for GraphApp {
             // Layout MenuBar at the top
             self.menu_bar.set_rect(0.0, 0.0, size.width, 42.0);
             
+            // Layout Dropdowns on MenuBar
+            self.dropdown_file.set_rect(10.0, 8.0, 70.0, 26.0);
+            self.dropdown_edit.set_rect(90.0, 8.0, 70.0, 26.0);
+            self.dropdown_view.set_rect(170.0, 8.0, 70.0, 26.0);
+            
             // Layout Graph below MenuBar
             self.graph.set_rect(0.0, 42.0, size.width, size.height - 42.0);
             
@@ -598,10 +648,16 @@ impl Application for GraphApp {
         let (mb_x, mb_y, mb_w, mb_h) = self.menu_bar.rect();
         quads.push((mb_x, mb_y, mb_w, mb_h, mb_color));
 
+        // Add Dropdown flat trigger backgrounds (when rounded corners are not active)
+        quads.extend(self.dropdown_file.extra_quads());
+        quads.extend(self.dropdown_edit.extra_quads());
+        quads.extend(self.dropdown_view.extra_quads());
+    }
 
-
-        // Add MenuBar extra quads (dropdown boxes)
-        quads.extend(self.menu_bar.extra_quads());
+    fn view_rounded_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))>, _size: LogicalSize, _scale: f64) {
+        quads.extend(self.dropdown_file.all_rounded_quads(&self.ui_context));
+        quads.extend(self.dropdown_edit.all_rounded_quads(&self.ui_context));
+        quads.extend(self.dropdown_view.all_rounded_quads(&self.ui_context));
     }
 
     fn text_items(&self) -> &[TextItem] {
@@ -615,11 +671,16 @@ impl Application for GraphApp {
     fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
         let mut changed = false;
 
-        // If MenuBar has an open menu, or cursor is over MenuBar, feed it first
-        if self.menu_bar.is_menu_open() || self.menu_bar.hit_test(pos.x, pos.y, &self.ui_context) {
-            if self.menu_bar.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) {
-                changed = true;
-            }
+        let over_menu = self.dropdown_file.open || self.dropdown_edit.open || self.dropdown_view.open
+            || self.dropdown_file.hit_test(pos.x, pos.y, &self.ui_context)
+            || self.dropdown_edit.hit_test(pos.x, pos.y, &self.ui_context)
+            || self.dropdown_view.hit_test(pos.x, pos.y, &self.ui_context)
+            || self.menu_bar.hit_test(pos.x, pos.y, &self.ui_context);
+
+        if over_menu {
+            if self.dropdown_file.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
+            if self.dropdown_edit.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
+            if self.dropdown_view.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
         } else {
             // Otherwise feed it to Graph
             if self.graph.is_dragging() {
@@ -631,10 +692,10 @@ impl Application for GraphApp {
                     changed = true;
                 }
             }
-            // Clear menu bar hover if cursor moved away
-            if self.menu_bar.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) {
-                changed = true;
-            }
+            // Clear hover states if cursor moved away
+            if self.dropdown_file.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
+            if self.dropdown_edit.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
+            if self.dropdown_view.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
         }
 
         if changed {
@@ -647,44 +708,66 @@ impl Application for GraphApp {
         let mut changed = false;
         let mut msg_out = None;
 
-        if self.menu_bar.is_menu_open() || self.menu_bar.hit_test(pos.x, pos.y, &self.ui_context) {
-            if self.menu_bar.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
+        let over_menu = self.dropdown_file.open || self.dropdown_edit.open || self.dropdown_view.open
+            || self.dropdown_file.hit_test(pos.x, pos.y, &self.ui_context)
+            || self.dropdown_edit.hit_test(pos.x, pos.y, &self.ui_context)
+            || self.dropdown_view.hit_test(pos.x, pos.y, &self.ui_context)
+            || self.menu_bar.hit_test(pos.x, pos.y, &self.ui_context);
+
+        if over_menu {
+            if self.dropdown_file.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
                 changed = true;
-                
-                // Check if a dropdown menu item was clicked
-                if let Some((menu_idx, item_idx)) = self.menu_bar.menu_click() {
-                    if menu_idx == 0 { // File
-                        match item_idx {
-                            0 => msg_out = Some(AppMessage::New),
-                            1 => msg_out = Some(AppMessage::Open),
-                            2 => msg_out = Some(AppMessage::Save),
-                            3 => msg_out = Some(AppMessage::SaveAs),
-                            4 => msg_out = Some(AppMessage::Exit),
-                            _ => {}
-                        }
-                    } else if menu_idx == 1 { // Edit
-                        match item_idx {
-                            0 => msg_out = Some(AppMessage::AddNode),
-                            1 => msg_out = Some(AppMessage::AddImage),
-                            _ => {}
-                        }
-                    } else if menu_idx == 2 { // View
-                        match item_idx {
-                            0 => msg_out = Some(AppMessage::ToggleGrid),
-                            1 => msg_out = Some(AppMessage::ToggleUniformBackground),
-                            2 => msg_out = Some(AppMessage::SetOpacity95),
-                            3 => msg_out = Some(AppMessage::SetOpacity75),
-                            4 => msg_out = Some(AppMessage::SetOpacity50),
-                            _ => {}
-                        }
+                if self.dropdown_file.take_change() {
+                    let idx = self.dropdown_file.selected;
+                    match idx {
+                        0 => msg_out = Some(AppMessage::New),
+                        1 => msg_out = Some(AppMessage::Open),
+                        2 => msg_out = Some(AppMessage::Save),
+                        3 => msg_out = Some(AppMessage::SaveAs),
+                        4 => msg_out = Some(AppMessage::Exit),
+                        _ => {}
                     }
+                    self.dropdown_file.selected = 999;
                 }
             }
-            
-            // If the user clicked outside the open menu, close it
-            if state == ElementState::Pressed && !self.menu_bar.hit_test(pos.x, pos.y, &self.ui_context) {
-                self.menu_bar.unfocus();
+            if self.dropdown_edit.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
+                changed = true;
+                if self.dropdown_edit.take_change() {
+                    let idx = self.dropdown_edit.selected;
+                    match idx {
+                        0 => msg_out = Some(AppMessage::AddNode),
+                        1 => msg_out = Some(AppMessage::AddImage),
+                        _ => {}
+                    }
+                    self.dropdown_edit.selected = 999;
+                }
+            }
+            if self.dropdown_view.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
                 changed = true;
+                if self.dropdown_view.take_change() {
+                    let idx = self.dropdown_view.selected;
+                    match idx {
+                        0 => msg_out = Some(AppMessage::ToggleGrid),
+                        1 => msg_out = Some(AppMessage::ToggleUniformBackground),
+                        2 => msg_out = Some(AppMessage::SetOpacity95),
+                        3 => msg_out = Some(AppMessage::SetOpacity75),
+                        4 => msg_out = Some(AppMessage::SetOpacity50),
+                        _ => {}
+                    }
+                    self.dropdown_view.selected = 999;
+                }
+            }
+
+            if state == ElementState::Pressed {
+                if !self.dropdown_file.hit_test(pos.x, pos.y, &self.ui_context) {
+                    self.dropdown_file.open = false;
+                }
+                if !self.dropdown_edit.hit_test(pos.x, pos.y, &self.ui_context) {
+                    self.dropdown_edit.open = false;
+                }
+                if !self.dropdown_view.hit_test(pos.x, pos.y, &self.ui_context) {
+                    self.dropdown_view.open = false;
+                }
             }
         } else {
             // Otherwise route to Graph