From d8e5b2e9beab39a9685304e83eac597818762bd1 Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Sat, 25 Apr 2026 03:28:28 +0200 Subject: [PATCH] Added MenuBar --- .clangd | 1 + CMakeLists.txt | 1 + extra/DefaultTheme.oss | 15 +++ other/TODO.txt | 8 +- src/ogfx/gui/ContextMenu.cpp | 16 ++- src/ogfx/gui/MenuBar.cpp | 182 ++++++++++++++++++++++++++++ src/ogfx/gui/MenuBar.hpp | 79 ++++++++++++ src/ogfx/gui/Window.cpp | 11 ++ src/ogfx/gui/Window.hpp | 7 ++ src/ogfx/gui/widgets/RootWidget.cpp | 7 +- src/test/GuiTest.cpp | 116 +++++++++++++++++- 11 files changed, 437 insertions(+), 6 deletions(-) create mode 100644 src/ogfx/gui/MenuBar.cpp create mode 100644 src/ogfx/gui/MenuBar.hpp diff --git a/.clangd b/.clangd index e6defe1..c60968a 100644 --- a/.clangd +++ b/.clangd @@ -1,4 +1,5 @@ CompileFlags: CompilationDatabase: bin + Add: ["--target=x86_64-w64-mingw32"] Diagnostics: MissingIncludes: None \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 16cdb5e..e9f0852 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ list(APPEND OGFX_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/WindowOutputHandler.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/WidgetManager.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/ContextMenu.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/MenuBar.cpp # gui/widgets ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Widget.cpp diff --git a/extra/DefaultTheme.oss b/extra/DefaultTheme.oss index ca6b1e2..02cc12b 100644 --- a/extra/DefaultTheme.oss +++ b/extra/DefaultTheme.oss @@ -293,3 +293,18 @@ const $accentColorDark = #6B0A1DFF selectionGradient = gradientV(#C21135FF - #820B23FF) } % ========================== + + + +% ====== MenuBar ====== +(menubar) { + height = 24.0 + itemPadding = rect(0, 0, 15, 0) + fontSize = 16 + backgroundColor = #CCAAAAFF + textColor = $textColor + selectionColor = $accentColor + selectionTextColor = #FFFFFFFF + borderColor = #400000FF +} +% ===================== diff --git a/other/TODO.txt b/other/TODO.txt index 1372393..b0e15aa 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -34,6 +34,7 @@ Cache getStringDimensions() call in TabPanel::draw_tabs FIX: Optimize ListView FIX: Padding for ListView items Add icons to ListView +FIX: Limit FPS even when continuous events happen @@ -57,15 +58,16 @@ Implement following widgets: ***Vertical Slider ***ListBox ***Context Menu - ComboBox - Menu Bar + ***MenuBar Toolbar StatusBar + ComboBox + TreeView Layouts Vertical Horizontal Grid Fill Text Input - TreeView + Text Area Hex Editor diff --git a/src/ogfx/gui/ContextMenu.cpp b/src/ogfx/gui/ContextMenu.cpp index b29b46a..071fd0a 100644 --- a/src/ogfx/gui/ContextMenu.cpp +++ b/src/ogfx/gui/ContextMenu.cpp @@ -144,7 +144,7 @@ namespace ogfx } } - hide(); + // hide(); event.handle(); } @@ -244,6 +244,20 @@ namespace ogfx void ContextMenu::onMousePressed(const Event& event) { + if (!m_visible) return; + Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; + + for (i32 i = (i32)m_panels.size() - 1; i >= 0; --i) + { + i32 idx = entry_index_at(m_panels[i], mousePos); + if (idx >= 0) + { + event.handle(); + return; + } + } + + hide(); event.handle(); } diff --git a/src/ogfx/gui/MenuBar.cpp b/src/ogfx/gui/MenuBar.cpp new file mode 100644 index 0000000..b47f503 --- /dev/null +++ b/src/ogfx/gui/MenuBar.cpp @@ -0,0 +1,182 @@ +#include "MenuBar.hpp" +#include "../render/BasicRenderer.hpp" +#include "Window.hpp" + +namespace ogfx +{ + namespace gui + { + MenuBar& MenuBar::create(void) + { + setSize(m_window.getWindowWidth(), m_height); + setPosition(0, 0); + return *this; + } + + MenuBar& MenuBar::addMenu(const String& label, const ContextMenu::Instance& instance) + { + m_slots.push_back({ label, instance, {} }); + recompute_layout(); + return *this; + } + + MenuBar& MenuBar::addMenu(const Menu& menu) + { + return addMenu(menu.label, menu.instance); + } + + void MenuBar::clearMenus(void) + { + close_menu(); + m_slots.clear(); + } + + void MenuBar::onWindowResized(const Event& event) + { + setSize(m_window.getWindowWidth(), m_height); + setPosition(0, 0); + recompute_layout(); + } + + void MenuBar::applyTheme(const ostd::Stylesheet& theme) + { + setHeight(theme.get("menubar.height", getHeight(), {}, {})); + setItemPadding(theme.get("menubar.itemPadding", getItemPadding(), {}, {})); + setFontSize(theme.get("menubar.fontSize", getFontSize(), {}, {})); + setBackgroundColor(theme.get("menubar.backgroundColor", getBackgroundColor(), {}, {})); + setTextColor(theme.get("menubar.textColor", getTextColor(), {}, {})); + setSelectionColor(theme.get("menubar.selectionColor", getSelectionColor(), {}, {})); + setSelectionTextColor(theme.get("menubar.selectionTextColor", getSelectionTextColor(), {}, {})); + setBorderColor(theme.get("menubar.borderColor", getBorderColor(), {}, {})); + setSize(m_window.getWindowWidth(), m_height); + recompute_layout(); + } + + void MenuBar::recompute_layout(void) + { + auto& gfx = m_window.getGFX(); + f32 x = m_itemPadding.x; + for (auto& slot : m_slots) + { + Vec2 textSize = gfx.getStringDimensions(slot.label, m_fontSize); + f32 slotW = textSize.x + m_itemPadding.x + m_itemPadding.w; + slot.rect = { x, 0, slotW, m_height }; + x += slotW; + } + } + + i32 MenuBar::slot_index_at(const Vec2& mousePos) const + { + for (i32 i = 0; i < (i32)m_slots.size(); ++i) + { + if (m_slots[i].rect.contains(mousePos, true)) + return i; + } + return -1; + } + + void MenuBar::open_menu(i32 index) + { + if (index < 0 || index >= (i32)m_slots.size()) return; + + const Slot& slot = m_slots[index]; + // Anchor menu just below the label + Vec2 anchor { slot.rect.x, slot.rect.y + slot.rect.h }; + + m_window.setContextMenu(slot.instance); + m_window.showContextMenu(anchor); + + m_openIndex = index; + m_active = true; + } + + void MenuBar::close_menu(void) + { + if (m_window.isContextMenuVisible()) + m_window.hideContextMenu(); + m_openIndex = -1; + m_active = false; + } + + void MenuBar::draw(BasicRenderer2D& gfx) + { + if (!m_visible) return; + + gfx.fillRect(*this, m_backgroundColor); + + for (i32 i = 0; i < (i32)m_slots.size(); ++i) + { + const Slot& slot = m_slots[i]; + bool highlighted = (i == m_hoveredIndex) || (i == m_openIndex); + + if (highlighted) + { + gfx.fillRect(slot.rect, m_selectionColor); + gfx.drawCenteredString(slot.label, slot.rect, m_selectionTextColor, m_fontSize); + } + else + { + gfx.drawCenteredString(slot.label, slot.rect, m_textColor, m_fontSize); + } + } + + // Bottom border line + gfx.drawLine({ { getx(), gety() + m_height }, { getx() + getw(), gety() + m_height } }, m_borderColor); + } + + void MenuBar::update(void) + { + if (!m_visible) return; + + // If we're in active mode but the context menu got dismissed + // (item clicked, click-outside, escape, etc.), drop out of active mode. + if (m_active && !m_window.isContextMenuVisible()) + { + m_openIndex = -1; + m_active = false; + } + } + + void MenuBar::onMousePressed(const Event& event) + { + if (!m_visible) return; + + Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; + i32 idx = slot_index_at(mousePos); + + if (idx < 0) + { + // Press outside the bar — let the context menu's own click-outside + // handling close it; we'll catch up via update(). + return; + } + + if (m_openIndex == idx) + { + // Clicking the open menu's own label closes it (toggle). + close_menu(); + } + else + { + open_menu(idx); + } + event.handle(); + } + + void MenuBar::onMouseMoved(const Event& event) + { + if (!m_visible) return; + + Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; + m_hoveredIndex = slot_index_at(mousePos); + + // Hover-switching: in active mode, moving onto a different label + // instantly switches the open menu. + if (m_active && m_hoveredIndex >= 0 && m_hoveredIndex != m_openIndex) + { + open_menu(m_hoveredIndex); + event.handle(); + } + } + } +} diff --git a/src/ogfx/gui/MenuBar.hpp b/src/ogfx/gui/MenuBar.hpp new file mode 100644 index 0000000..af093c3 --- /dev/null +++ b/src/ogfx/gui/MenuBar.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +namespace ogfx +{ + namespace gui + { + class Window; + class MenuBar : public Rectangle + { + public: struct Menu + { + String label; + ContextMenu::Instance instance; + }; + + public: + inline MenuBar(Window& window) : m_window(window) { create(); } + MenuBar& create(void); + MenuBar& addMenu(const String& label, const ContextMenu::Instance& instance); + MenuBar& addMenu(const Menu& menu); + void clearMenus(void); + + void onWindowResized(const Event& event); + void applyTheme(const ostd::Stylesheet& theme); + void draw(BasicRenderer2D& gfx); + void update(void); + void onMousePressed(const Event& event); + void onMouseMoved(const Event& event); + + inline bool isVisible(void) const { return m_visible; } + inline void show(bool v = true) { m_visible = v; } + inline void hide(void) { show(false); m_active = false; m_openIndex = -1; } + inline bool isActive(void) const { return m_active; } + + OSTD_PARAM_GETSET(f32, Height, m_height); + OSTD_PARAM_GETSET(Rectangle, ItemPadding, m_itemPadding); + OSTD_PARAM_GETSET(i32, FontSize, m_fontSize); + OSTD_PARAM_GETSET(Color, BackgroundColor, m_backgroundColor); + OSTD_PARAM_GETSET(Color, TextColor, m_textColor); + OSTD_PARAM_GETSET(Color, SelectionColor, m_selectionColor); + OSTD_PARAM_GETSET(Color, SelectionTextColor, m_selectionTextColor); + OSTD_PARAM_GETSET(Color, BorderColor, m_borderColor); + + private: + struct Slot + { + String label; + ContextMenu::Instance instance; + Rectangle rect; // screen-space, recomputed on layout + }; + + void recompute_layout(void); + i32 slot_index_at(const Vec2& mousePos) const; + void open_menu(i32 index); + void close_menu(void); + + private: + Window& m_window; + stdvec m_slots; + + i32 m_openIndex { -1 }; + i32 m_hoveredIndex { -1 }; + bool m_active { false }; + bool m_visible { true }; + + f32 m_height { 26 }; + Rectangle m_itemPadding { 12, 0, 12, 0 }; // x = left, y = top, w = right, h = bottom + i32 m_fontSize { 16 }; + Color m_backgroundColor { "#6B0A1DFF" }; + Color m_textColor { "#F16A85FF" }; + Color m_selectionColor { "#DC143CFF" }; + Color m_selectionTextColor { "#F16A85FF" }; + Color m_borderColor { "#400000FF" }; + }; + } +} diff --git a/src/ogfx/gui/Window.cpp b/src/ogfx/gui/Window.cpp index 88929af..5599689 100644 --- a/src/ogfx/gui/Window.cpp +++ b/src/ogfx/gui/Window.cpp @@ -830,6 +830,7 @@ namespace ogfx m_rootWidget.__applyTheme(theme, true); m_rootWidget.reloadTheme(true); m_cmenu.applyTheme(theme); + m_menubar.applyTheme(theme); } void Window::__on_window_init(i32 width, i32 height, const String& title) @@ -871,6 +872,8 @@ namespace ogfx m_rootWidget.__update(); m_rootWidget.__draw(m_gfx); onRedraw(m_gfx); + if (m_menubar.isVisible()) + m_menubar.draw(m_gfx); if (m_cmenu.isVisible()) { stopTooltipTimer(); @@ -934,12 +937,16 @@ namespace ogfx m_cmenu.hide(); evt.windowResized = &(ogfx::WindowResizedData&)signal.userData; evt.__original_signal_id = ostd::BuiltinSignals::WindowResized; + if (m_menubar.isVisible()) + m_menubar.onWindowResized(evt); m_rootWidget.__onWindowResized(evt); } else if (signal.ID == ostd::BuiltinSignals::MouseMoved) { evt.mouse = &(ogfx::MouseEventData&)signal.userData; evt.__original_signal_id = ostd::BuiltinSignals::MouseMoved; + if (m_menubar.isVisible()) + m_menubar.onMouseMoved(evt); if (m_cmenu.isVisible()) m_cmenu.onMouseMoved(evt); m_rootWidget.__onMouseMoved(evt); @@ -958,6 +965,8 @@ namespace ogfx evt.__original_signal_id = ostd::BuiltinSignals::MousePressed; if (m_cmenu.isVisible()) m_cmenu.onMousePressed(evt); + if (m_menubar.isVisible()) + m_menubar.onMousePressed(evt); m_rootWidget.__onMousePressed(evt); } else if (signal.ID == ostd::BuiltinSignals::MouseReleased) @@ -994,6 +1003,8 @@ namespace ogfx void Window::__on_update(f64 delta) { m_cmenu.update(); + if (m_menubar.isVisible()) + m_menubar.update(); onUpdate(delta); } diff --git a/src/ogfx/gui/Window.hpp b/src/ogfx/gui/Window.hpp index d223d57..5a7ecd9 100644 --- a/src/ogfx/gui/Window.hpp +++ b/src/ogfx/gui/Window.hpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace ogfx { @@ -272,6 +273,9 @@ namespace ogfx void setTheme(const ostd::Stylesheet& theme) override; inline void showContextMenu(const Vec2& pos) { m_cmenu.show(pos); } inline void setContextMenu(const ContextMenu::Instance& instance) { m_cmenu.setInstance(instance); } + inline bool isContextMenuVisible(void) const { return m_cmenu.isVisible(); } + inline void hideContextMenu(void) { m_cmenu.hide(); } + inline MenuBar& getMenuBar(void) { return m_menubar; } inline virtual void onInitialize(void) { } inline virtual void onDestroy(void) { } @@ -297,6 +301,9 @@ namespace ogfx ostd::StepTimer m_fixedUpdateTimer; ostd::StepTimer::TimePoint m_lastFrameTime; ContextMenu m_cmenu { *this }; + MenuBar m_menubar { *this }; + + friend class widgets::RootWidget; }; } } diff --git a/src/ogfx/gui/widgets/RootWidget.cpp b/src/ogfx/gui/widgets/RootWidget.cpp index 660ba8b..254ae48 100644 --- a/src/ogfx/gui/widgets/RootWidget.cpp +++ b/src/ogfx/gui/widgets/RootWidget.cpp @@ -38,7 +38,12 @@ namespace ogfx void RootWidget::onWindowResized(const Event& event) { - setSize(cast(event.windowResized->new_width), cast(event.windowResized->new_height)); + Window& win = cast(getWindow()); //TODO: Potentially unsage? + f32 offset_y = 0; + if (win.m_menubar.isVisible()) + offset_y += win.m_menubar.geth(); + sety(offset_y); + setSize(cast(event.windowResized->new_width), cast(event.windowResized->new_height) - offset_y); } void RootWidget::applyTheme(const ostd::Stylesheet& theme) diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index 05e401f..0460c83 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -189,6 +189,31 @@ class Window : public ogfx::gui::Window }; setContextMenu(m_menu); + + auto onActivate = [this](const ogfx::gui::ContextMenu::Entry& e) { + out().fg("cyan").p("[MenuBar] Activated: ").fg("yellow").p(e.text) + .fg("cyan").p(" (id=").fg("green").p(e.id).fg("cyan").p(")").reset().nl(); + + switch (e.id) + { + case TestMenuId::File_Exit: + close(); + break; + // ... add real handlers as you build them out + default: + break; + } + }; + + fileMenu.onActivate = onActivate; + editMenu.onActivate = onActivate; + viewMenu.onActivate = onActivate; + helpMenu.onActivate = onActivate; + + getMenuBar().addMenu("File", fileMenu) + .addMenu("Edit", editMenu) + .addMenu("View", viewMenu) + .addMenu("Help", helpMenu); } inline void onSignal(ostd::Signal& signal) override @@ -239,7 +264,7 @@ class Window : public ogfx::gui::Window ListView m_list { *this }; Label m_drawCallsLbl { *this }; - enum MenuId { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted }; + enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted }; ogfx::gui::ContextMenu::Instance m_menu { { { "File", -1, { @@ -258,6 +283,95 @@ class Window : public ogfx::gui::Window nullptr }; + + + enum TestMenuId : i32 + { + // File + File_New = 1, + File_Open, + File_OpenRecent_Project1, + File_OpenRecent_Project2, + File_OpenRecent_Project3, + File_OpenRecent_ClearList, + File_Save, + File_SaveAs, + File_Export_PNG, + File_Export_JPG, + File_Export_SVG, + File_Export_PDF, + File_Exit, + + // Edit + Edit_Undo, + Edit_Redo, + Edit_Cut, + Edit_Copy, + Edit_Paste, + Edit_SelectAll, + + // View + View_ZoomIn, + View_ZoomOut, + View_ZoomReset, + View_Theme_Dark, + View_Theme_Light, + View_Theme_Crimson, + + // Help + Help_Documentation, + Help_About, + }; + + ogfx::gui::ContextMenu::Instance fileMenu { { + { "New", TestMenuId::File_New }, + { "Open...", TestMenuId::File_Open }, + { "Open Recent", { + { "Project1.proj", TestMenuId::File_OpenRecent_Project1 }, + { "Project2.proj", TestMenuId::File_OpenRecent_Project2 }, + { "Project3.proj", TestMenuId::File_OpenRecent_Project3 }, + { "Clear Recent List", TestMenuId::File_OpenRecent_ClearList }, + }}, + { "Save", TestMenuId::File_Save }, + { "Save As...",TestMenuId::File_SaveAs }, + { "Export As", { + { "PNG Image", TestMenuId::File_Export_PNG }, + { "JPG Image", TestMenuId::File_Export_JPG }, + { "SVG Vector",TestMenuId::File_Export_SVG }, + { "PDF Document", TestMenuId::File_Export_PDF }, + }}, + { "Exit", TestMenuId::File_Exit } }, + nullptr + }; + + ogfx::gui::ContextMenu::Instance editMenu { { + { "Undo", TestMenuId::Edit_Undo }, + { "Redo", TestMenuId::Edit_Redo }, + { "Cut", TestMenuId::Edit_Cut }, + { "Copy", TestMenuId::Edit_Copy }, + { "Paste", TestMenuId::Edit_Paste }, + { "Select All", TestMenuId::Edit_SelectAll } }, + nullptr + }; + + ogfx::gui::ContextMenu::Instance viewMenu { { + { "Zoom In", TestMenuId::View_ZoomIn }, + { "Zoom Out", TestMenuId::View_ZoomOut }, + { "Reset Zoom", TestMenuId::View_ZoomReset }, + { "Theme", { + { "Dark", TestMenuId::View_Theme_Dark }, + { "Light", TestMenuId::View_Theme_Light }, + { "Crimson", TestMenuId::View_Theme_Crimson }, + }} }, + nullptr + }; + + ogfx::gui::ContextMenu::Instance helpMenu { { + { "Documentation", TestMenuId::Help_Documentation }, + { "About", TestMenuId::Help_About } }, + nullptr + }; + ostd::StepTimer m_timer; ostd::AsyncJob m_progressJob;