Added MenuBar

This commit is contained in:
OmniaX-Dev 2026-04-25 03:28:28 +02:00
parent 22ed3c0744
commit d8e5b2e9be
11 changed files with 437 additions and 6 deletions

View file

@ -1,4 +1,5 @@
CompileFlags: CompileFlags:
CompilationDatabase: bin CompilationDatabase: bin
Add: ["--target=x86_64-w64-mingw32"]
Diagnostics: Diagnostics:
MissingIncludes: None MissingIncludes: None

View file

@ -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/WindowOutputHandler.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/WidgetManager.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/ContextMenu.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/MenuBar.cpp
# gui/widgets # gui/widgets
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Widget.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Widget.cpp

View file

@ -293,3 +293,18 @@ const $accentColorDark = #6B0A1DFF
selectionGradient = gradientV(#C21135FF - #820B23FF) 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
}
% =====================

View file

@ -34,6 +34,7 @@ Cache getStringDimensions() call in TabPanel::draw_tabs
FIX: Optimize ListView FIX: Optimize ListView
FIX: Padding for ListView items FIX: Padding for ListView items
Add icons to ListView Add icons to ListView
FIX: Limit FPS even when continuous events happen
@ -57,15 +58,16 @@ Implement following widgets:
***Vertical Slider ***Vertical Slider
***ListBox ***ListBox
***Context Menu ***Context Menu
ComboBox ***MenuBar
Menu Bar
Toolbar Toolbar
StatusBar StatusBar
ComboBox
TreeView
Layouts Layouts
Vertical Vertical
Horizontal Horizontal
Grid Grid
Fill Fill
Text Input Text Input
TreeView Text Area
Hex Editor Hex Editor

View file

@ -144,7 +144,7 @@ namespace ogfx
} }
} }
hide(); // hide();
event.handle(); event.handle();
} }
@ -244,6 +244,20 @@ namespace ogfx
void ContextMenu::onMousePressed(const Event& event) 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(); event.handle();
} }

182
src/ogfx/gui/MenuBar.cpp Normal file
View file

@ -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<f32>("menubar.height", getHeight(), {}, {}));
setItemPadding(theme.get<Rectangle>("menubar.itemPadding", getItemPadding(), {}, {}));
setFontSize(theme.get<i32>("menubar.fontSize", getFontSize(), {}, {}));
setBackgroundColor(theme.get<Color>("menubar.backgroundColor", getBackgroundColor(), {}, {}));
setTextColor(theme.get<Color>("menubar.textColor", getTextColor(), {}, {}));
setSelectionColor(theme.get<Color>("menubar.selectionColor", getSelectionColor(), {}, {}));
setSelectionTextColor(theme.get<Color>("menubar.selectionTextColor", getSelectionTextColor(), {}, {}));
setBorderColor(theme.get<Color>("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();
}
}
}
}

79
src/ogfx/gui/MenuBar.hpp Normal file
View file

@ -0,0 +1,79 @@
#pragma once
#include <ogfx/gui/widgets/Widget.hpp>
#include <ogfx/gui/ContextMenu.hpp>
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<Slot> 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" };
};
}
}

View file

@ -830,6 +830,7 @@ namespace ogfx
m_rootWidget.__applyTheme(theme, true); m_rootWidget.__applyTheme(theme, true);
m_rootWidget.reloadTheme(true); m_rootWidget.reloadTheme(true);
m_cmenu.applyTheme(theme); m_cmenu.applyTheme(theme);
m_menubar.applyTheme(theme);
} }
void Window::__on_window_init(i32 width, i32 height, const String& title) void Window::__on_window_init(i32 width, i32 height, const String& title)
@ -871,6 +872,8 @@ namespace ogfx
m_rootWidget.__update(); m_rootWidget.__update();
m_rootWidget.__draw(m_gfx); m_rootWidget.__draw(m_gfx);
onRedraw(m_gfx); onRedraw(m_gfx);
if (m_menubar.isVisible())
m_menubar.draw(m_gfx);
if (m_cmenu.isVisible()) if (m_cmenu.isVisible())
{ {
stopTooltipTimer(); stopTooltipTimer();
@ -934,12 +937,16 @@ namespace ogfx
m_cmenu.hide(); m_cmenu.hide();
evt.windowResized = &(ogfx::WindowResizedData&)signal.userData; evt.windowResized = &(ogfx::WindowResizedData&)signal.userData;
evt.__original_signal_id = ostd::BuiltinSignals::WindowResized; evt.__original_signal_id = ostd::BuiltinSignals::WindowResized;
if (m_menubar.isVisible())
m_menubar.onWindowResized(evt);
m_rootWidget.__onWindowResized(evt); m_rootWidget.__onWindowResized(evt);
} }
else if (signal.ID == ostd::BuiltinSignals::MouseMoved) else if (signal.ID == ostd::BuiltinSignals::MouseMoved)
{ {
evt.mouse = &(ogfx::MouseEventData&)signal.userData; evt.mouse = &(ogfx::MouseEventData&)signal.userData;
evt.__original_signal_id = ostd::BuiltinSignals::MouseMoved; evt.__original_signal_id = ostd::BuiltinSignals::MouseMoved;
if (m_menubar.isVisible())
m_menubar.onMouseMoved(evt);
if (m_cmenu.isVisible()) if (m_cmenu.isVisible())
m_cmenu.onMouseMoved(evt); m_cmenu.onMouseMoved(evt);
m_rootWidget.__onMouseMoved(evt); m_rootWidget.__onMouseMoved(evt);
@ -958,6 +965,8 @@ namespace ogfx
evt.__original_signal_id = ostd::BuiltinSignals::MousePressed; evt.__original_signal_id = ostd::BuiltinSignals::MousePressed;
if (m_cmenu.isVisible()) if (m_cmenu.isVisible())
m_cmenu.onMousePressed(evt); m_cmenu.onMousePressed(evt);
if (m_menubar.isVisible())
m_menubar.onMousePressed(evt);
m_rootWidget.__onMousePressed(evt); m_rootWidget.__onMousePressed(evt);
} }
else if (signal.ID == ostd::BuiltinSignals::MouseReleased) else if (signal.ID == ostd::BuiltinSignals::MouseReleased)
@ -994,6 +1003,8 @@ namespace ogfx
void Window::__on_update(f64 delta) void Window::__on_update(f64 delta)
{ {
m_cmenu.update(); m_cmenu.update();
if (m_menubar.isVisible())
m_menubar.update();
onUpdate(delta); onUpdate(delta);
} }

View file

@ -30,6 +30,7 @@
#include <ogfx/render/BasicRenderer.hpp> #include <ogfx/render/BasicRenderer.hpp>
#include <ogfx/gui/WindowOutputHandler.hpp> #include <ogfx/gui/WindowOutputHandler.hpp>
#include <ogfx/gui/ContextMenu.hpp> #include <ogfx/gui/ContextMenu.hpp>
#include <ogfx/gui/MenuBar.hpp>
namespace ogfx namespace ogfx
{ {
@ -272,6 +273,9 @@ namespace ogfx
void setTheme(const ostd::Stylesheet& theme) override; void setTheme(const ostd::Stylesheet& theme) override;
inline void showContextMenu(const Vec2& pos) { m_cmenu.show(pos); } inline void showContextMenu(const Vec2& pos) { m_cmenu.show(pos); }
inline void setContextMenu(const ContextMenu::Instance& instance) { m_cmenu.setInstance(instance); } 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 onInitialize(void) { }
inline virtual void onDestroy(void) { } inline virtual void onDestroy(void) { }
@ -297,6 +301,9 @@ namespace ogfx
ostd::StepTimer m_fixedUpdateTimer; ostd::StepTimer m_fixedUpdateTimer;
ostd::StepTimer::TimePoint m_lastFrameTime; ostd::StepTimer::TimePoint m_lastFrameTime;
ContextMenu m_cmenu { *this }; ContextMenu m_cmenu { *this };
MenuBar m_menubar { *this };
friend class widgets::RootWidget;
}; };
} }
} }

View file

@ -38,7 +38,12 @@ namespace ogfx
void RootWidget::onWindowResized(const Event& event) void RootWidget::onWindowResized(const Event& event)
{ {
setSize(cast<f32>(event.windowResized->new_width), cast<f32>(event.windowResized->new_height)); Window& win = cast<Window&>(getWindow()); //TODO: Potentially unsage?
f32 offset_y = 0;
if (win.m_menubar.isVisible())
offset_y += win.m_menubar.geth();
sety(offset_y);
setSize(cast<f32>(event.windowResized->new_width), cast<f32>(event.windowResized->new_height) - offset_y);
} }
void RootWidget::applyTheme(const ostd::Stylesheet& theme) void RootWidget::applyTheme(const ostd::Stylesheet& theme)

View file

@ -189,6 +189,31 @@ class Window : public ogfx::gui::Window
}; };
setContextMenu(m_menu); 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 inline void onSignal(ostd::Signal& signal) override
@ -239,7 +264,7 @@ class Window : public ogfx::gui::Window
ListView m_list { *this }; ListView m_list { *this };
Label m_drawCallsLbl { *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 { { ogfx::gui::ContextMenu::Instance m_menu { {
{ "File", -1, { { "File", -1, {
@ -258,6 +283,95 @@ class Window : public ogfx::gui::Window
nullptr 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::StepTimer m_timer;
ostd::AsyncJob<bool> m_progressJob; ostd::AsyncJob<bool> m_progressJob;