git.lucas.co / cce-text-editor
text editor
git clone https://git.lucas.co/cce-text-editor.git

commit07139f1da9c9982e86800d5708f2c8f6c81e5e65
parentf2b6d9af02
authorLucas Galante <[email protected]>
date2026-07-04 15:03
feat: move menubar buttons into a File dropdown menu

 src/main.rs | 160 +++++++++++++++++++++++++++++++++---------------------------
 1 file changed, 88 insertions(+), 72 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 82c1036..4c7f456 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,7 +3,7 @@ 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,
-    TextBox, Button, TextLabel, Key
+    TextBox, TextLabel, Key, Dropdown
 };
 
 #[derive(Debug, Clone)]
@@ -16,12 +16,8 @@ enum AppMessage {
 }
 
 struct TextEditorApp {
-    // Buttons
-    btn_new: Button,
-    btn_open: Button,
-    btn_save: Button,
-    btn_save_as: Button,
-    btn_exit: Button,
+    // File menu dropdown
+    menu_dropdown: Dropdown,
     
     // Editor TextBox
     editor: TextBox,
@@ -115,12 +111,8 @@ impl TextEditorApp {
         let scale = cce_ui::scale::scale_factor();
         let mut labels = Vec::new();
 
-        // 1. Button labels
-        labels.extend(self.btn_new.text_labels());
-        labels.extend(self.btn_open.text_labels());
-        labels.extend(self.btn_save.text_labels());
-        labels.extend(self.btn_save_as.text_labels());
-        labels.extend(self.btn_exit.text_labels());
+        // 1. Menu dropdown labels
+        labels.extend(self.menu_dropdown.text_labels());
 
         // 2. File path info in the toolbar
         let is_dirty = if self.editor.editing {
@@ -236,11 +228,16 @@ impl Application for TextEditorApp {
     }
 
     fn new(_qh: &QueueHandle<EngineState<Self>>, _sender: calloop::channel::Sender<Self::Message>) -> Self {
-        let btn_new = Button::new(10.0, 8.0, 70.0, 26.0).with_label("New");
-        let btn_open = Button::new(90.0, 8.0, 70.0, 26.0).with_label("Open");
-        let btn_save = Button::new(170.0, 8.0, 70.0, 26.0).with_label("Save");
-        let btn_save_as = Button::new(250.0, 8.0, 75.0, 26.0).with_label("Save As");
-        let btn_exit = Button::new(335.0, 8.0, 70.0, 26.0).with_label("Exit");
+        let dropdown_options = vec![
+            "New".to_string(),
+            "Open...".to_string(),
+            "Save".to_string(),
+            "Save As...".to_string(),
+            "-".to_string(),
+            "Exit".to_string(),
+        ];
+        let mut menu_dropdown = Dropdown::new(dropdown_options, 0).with_custom_display_text("File");
+        menu_dropdown.set_rect(10.0, 8.0, 70.0, 26.0);
 
         // Monospace textbox setup
         let mut editor = TextBox::new(String::new())
@@ -265,11 +262,7 @@ impl Application for TextEditorApp {
         }
 
         Self {
-            btn_new,
-            btn_open,
-            btn_save,
-            btn_save_as,
-            btn_exit,
+            menu_dropdown,
             editor,
             current_file_path,
             width: 800,
@@ -385,12 +378,8 @@ impl Application for TextEditorApp {
             self.height = size.height as u32;
             self.scale_factor = scale;
 
-            // Set button dimensions and coordinates
-            self.btn_new.set_rect(10.0, 8.0, 70.0, 26.0);
-            self.btn_open.set_rect(90.0, 8.0, 70.0, 26.0);
-            self.btn_save.set_rect(170.0, 8.0, 70.0, 26.0);
-            self.btn_save_as.set_rect(250.0, 8.0, 75.0, 26.0);
-            self.btn_exit.set_rect(335.0, 8.0, 70.0, 26.0);
+            // Set menu dropdown dimensions and coordinates
+            self.menu_dropdown.set_rect(10.0, 8.0, 70.0, 26.0);
 
             // TextBox occupies the remaining space between the top bar and status bar
             let editor_w = (self.width as f32 - 20.0).max(100.0);
@@ -413,26 +402,30 @@ impl Application for TextEditorApp {
         quads.push((0.0, status_y, self.width as f32, 30.0, [0.08, 0.08, 0.10, 1.0]));
         quads.push((0.0, status_y, self.width as f32, 1.0, [0.18, 0.18, 0.22, 1.0]));
 
-        // 4. Buttons graphics
-        quads.extend(self.btn_new.all_quads(&self.ui_context));
-        quads.extend(self.btn_open.all_quads(&self.ui_context));
-        quads.extend(self.btn_save.all_quads(&self.ui_context));
-        quads.extend(self.btn_save_as.all_quads(&self.ui_context));
-        quads.extend(self.btn_exit.all_quads(&self.ui_context));
+        // 4. Menu dropdown graphics
+        quads.extend(self.menu_dropdown.all_quads(&self.ui_context));
 
         // 5. TextBox Editor graphics
         quads.extend(self.editor.all_quads(&self.ui_context));
+
+        // 6. Popovers registration (since this app bypasses the layout engine)
+        self.ui_context.clear_popovers();
+        cce_ui::widget::popovers::clear();
+        if self.menu_dropdown.popover_rect().is_some() {
+            self.ui_context.register_popover(&self.menu_dropdown);
+            cce_ui::widget::popovers::register(&self.menu_dropdown);
+        }
     }
 
     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.btn_new.all_rounded_quads(&self.ui_context));
-        quads.extend(self.btn_open.all_rounded_quads(&self.ui_context));
-        quads.extend(self.btn_save.all_rounded_quads(&self.ui_context));
-        quads.extend(self.btn_save_as.all_rounded_quads(&self.ui_context));
-        quads.extend(self.btn_exit.all_rounded_quads(&self.ui_context));
+        quads.extend(self.menu_dropdown.all_rounded_quads(&self.ui_context));
         quads.extend(self.editor.all_rounded_quads(&self.ui_context));
     }
 
+    fn render_popovers(&self, pc: &mut dyn cce_ui::layout::RenderTarget) {
+        cce_ui::layout::render_popovers(pc, &self.ui_context);
+    }
+
     fn text_items(&self) -> &[TextItem] {
         &self.text_items
     }
@@ -442,11 +435,7 @@ impl Application for TextEditorApp {
         let px = pos.x as f32;
         let py = pos.y as f32;
 
-        if self.btn_new.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
-        if self.btn_open.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
-        if self.btn_save.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
-        if self.btn_save_as.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
-        if self.btn_exit.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.menu_dropdown.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
         
         if self.editor.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
 
@@ -462,34 +451,31 @@ impl Application for TextEditorApp {
         let px = pos.x as f32;
         let py = pos.y as f32;
 
-        if self.btn_new.mouse_input(button, state, px, py, &mut self.ui_context) {
-            changed = true;
-            if state == ElementState::Released && self.btn_new.take_click() {
-                msg_out = Some(AppMessage::NewDocument);
-            }
-        }
-        if self.btn_open.mouse_input(button, state, px, py, &mut self.ui_context) {
-            changed = true;
-            if state == ElementState::Released && self.btn_open.take_click() {
-                msg_out = Some(AppMessage::OpenDocument);
-            }
-        }
-        if self.btn_save.mouse_input(button, state, px, py, &mut self.ui_context) {
+        if self.menu_dropdown.mouse_input(button, state, px, py, &mut self.ui_context) {
             changed = true;
-            if state == ElementState::Released && self.btn_save.take_click() {
-                msg_out = Some(AppMessage::SaveDocument);
-            }
-        }
-        if self.btn_save_as.mouse_input(button, state, px, py, &mut self.ui_context) {
-            changed = true;
-            if state == ElementState::Released && self.btn_save_as.take_click() {
-                msg_out = Some(AppMessage::SaveDocumentAs);
-            }
-        }
-        if self.btn_exit.mouse_input(button, state, px, py, &mut self.ui_context) {
-            changed = true;
-            if state == ElementState::Released && self.btn_exit.take_click() {
-                msg_out = Some(AppMessage::Exit);
+            if self.menu_dropdown.take_change() {
+                let selected_idx = self.menu_dropdown.selected;
+                if selected_idx < self.menu_dropdown.options.len() {
+                    let option_text = &self.menu_dropdown.options[selected_idx];
+                    match option_text.as_str() {
+                        "New" => {
+                            msg_out = Some(AppMessage::NewDocument);
+                        }
+                        "Open..." => {
+                            msg_out = Some(AppMessage::OpenDocument);
+                        }
+                        "Save" => {
+                            msg_out = Some(AppMessage::SaveDocument);
+                        }
+                        "Save As..." => {
+                            msg_out = Some(AppMessage::SaveDocumentAs);
+                        }
+                        "Exit" => {
+                            msg_out = Some(AppMessage::Exit);
+                        }
+                        _ => {}
+                    }
+                }
             }
         }
 
@@ -583,6 +569,36 @@ impl Application for TextEditorApp {
             }
         }
 
+        if !handled {
+            if self.menu_dropdown.keyboard_input(event, &mut self.ui_context) {
+                handled = true;
+                if self.menu_dropdown.take_change() {
+                    let selected_idx = self.menu_dropdown.selected;
+                    if selected_idx < self.menu_dropdown.options.len() {
+                        let option_text = &self.menu_dropdown.options[selected_idx];
+                        match option_text.as_str() {
+                            "New" => {
+                                msg_out = Some(AppMessage::NewDocument);
+                            }
+                            "Open..." => {
+                                msg_out = Some(AppMessage::OpenDocument);
+                            }
+                            "Save" => {
+                                msg_out = Some(AppMessage::SaveDocument);
+                            }
+                            "Save As..." => {
+                                msg_out = Some(AppMessage::SaveDocumentAs);
+                            }
+                            "Exit" => {
+                                msg_out = Some(AppMessage::Exit);
+                            }
+                            _ => {}
+                        }
+                    }
+                }
+            }
+        }
+
         if !handled {
             if self.editor.keyboard_input(event, &mut self.ui_context) {
                 handled = true;