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:
CompilationDatabase: bin
Add: ["--target=x86_64-w64-mingw32"]
Diagnostics:
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/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

View file

@ -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
}
% =====================

View file

@ -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

View file

@ -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();
}

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.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);
}

View file

@ -30,6 +30,7 @@
#include <ogfx/render/BasicRenderer.hpp>
#include <ogfx/gui/WindowOutputHandler.hpp>
#include <ogfx/gui/ContextMenu.hpp>
#include <ogfx/gui/MenuBar.hpp>
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;
};
}
}

View file

@ -38,7 +38,12 @@ namespace ogfx
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)

View file

@ -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<bool> m_progressJob;