Finished Context Menu

This commit is contained in:
OmniaX-Dev 2026-04-24 13:12:52 +02:00
parent eb27fa42d1
commit b38874c78a
7 changed files with 359 additions and 71 deletions

View file

@ -289,5 +289,7 @@ const $accentColorDark = #6B0A1DFF
textColor = $textColor textColor = $textColor
submenuIndicatorColor = $accentColorDark submenuIndicatorColor = $accentColorDark
borderColor = #400000FF borderColor = #400000FF
useSelectionGradient = true
selectionGradient = gradientV(#C21135FF - #820B23FF)
} }
% ========================== % ==========================

View file

@ -56,13 +56,15 @@ Implement following widgets:
***Horizontal Slider ***Horizontal Slider
***Vertical Slider ***Vertical Slider
***ListBox ***ListBox
***Context Menu
ComboBox ComboBox
Context Menu
Menu Bar Menu Bar
Toolbar
Layouts Layouts
Vertical Vertical
Horizontal Horizontal
Grid Grid
Fill
Text Input Text Input
TreeView TreeView
Hex Editor Hex Editor

View file

@ -1 +1 @@
2073 2074

View file

@ -39,9 +39,12 @@ namespace ogfx
} }
} }
ContextMenu& ContextMenu::create(void) ContextMenu& ContextMenu::create(void)
{ {
setSize(130, 300);
return *this; return *this;
} }
@ -57,64 +60,185 @@ namespace ogfx
setTextColor(theme.get<Color>("context.textColor", getTextColor(), {}, {})); setTextColor(theme.get<Color>("context.textColor", getTextColor(), {}, {}));
setSubmenuIndicatorColor(theme.get<Color>("context.submenuIndicatorColor", getSubmenuIndicatorColor(), {}, {})); setSubmenuIndicatorColor(theme.get<Color>("context.submenuIndicatorColor", getSubmenuIndicatorColor(), {}, {}));
setBorderColor(theme.get<Color>("context.borderColor", getBorderColor(), {}, {})); setBorderColor(theme.get<Color>("context.borderColor", getBorderColor(), {}, {}));
enableSelectionGradient(theme.get<bool>("context.useSelectionGradient", isSelectionGradientEnabled(), {}, {}));
setSelectionGradient(theme.get<ColorGradient>("context.selectionGradient", getSelectionGradient(), {}, {}));
} }
void ContextMenu::draw(BasicRenderer2D& gfx) void ContextMenu::draw(BasicRenderer2D& gfx)
{ {
gfx.fillRect(*this, getBackgroundColor()); if (!m_visible) return;
const f32 triPad = 14; for (const auto& panel : m_panels)
f32 y = 0; draw_panel(gfx, panel);
i32 i = 0;
for (auto& entry : m_data.entries)
{
Vec2 entryPos = getPosition() + Vec2 { m_padding.x, y + m_padding.y };
Rectangle rect = { entryPos - Vec2 { m_padding.x, 0 }, getw(), m_entryHeight };
if (rect.contains(m_mousePos, true))
{
gfx.outlinedRect(rect, getSelectionColor(), getSeparatorLineColor(), 1, false, false, i != m_data.entries.size() , false);
gfx.drawVCenteredString(entry.text, { entryPos, getw(), m_entryHeight }, getSelectionTextColor(), getFontSize());
} }
else
void ContextMenu::update(void)
{ {
gfx.drawRect(rect, getSeparatorLineColor(), 1, false, false, i != m_data.entries.size() , false); if (!m_visible) return;
gfx.drawVCenteredString(entry.text, { entryPos, getw(), m_entryHeight }, getTextColor(), getFontSize());
} if (m_pendingOpenPanelDepth >= 0 && m_hoverOpenTimerActive && m_hoverOpenTimer.read() >= SubmenuOpenDelayMs)
if (entry.submenus.size() > 0)
{ {
Triangle tri { Panel& parent = m_panels[m_pendingOpenPanelDepth];
{ rect.x + rect.w - m_entryHeight + triPad, rect.y + (triPad * 0.5f) }, i32 entryIdx = m_pendingOpenEntryIndex;
{ rect.x + rect.w - (triPad * 0.5f), rect.y + (rect.h * 0.5f) }, Entry& e = (*parent.entries)[entryIdx];
{ rect.x + rect.w - m_entryHeight + triPad, rect.y + rect.h - (triPad * 0.5f) }
}; pop_to_depth(m_pendingOpenPanelDepth);
gfx.fillTriangle(tri, getSubmenuIndicatorColor());
Rectangle r = entry_rect(parent, entryIdx);
Vec2 anchor { parent.position.x + parent.size.x, r.y };
push_panel(e.submenus, anchor, false);
m_panels[m_pendingOpenPanelDepth].openedSubmenuIndex = entryIdx;
m_hoverOpenTimer.endCount();
m_hoverOpenTimerActive = false;
m_pendingOpenPanelDepth = -1;
m_pendingOpenEntryIndex = -1;
} }
y += m_entryHeight;
i++; if (m_hoverCloseTimerActive && m_hoverCloseTimer.read() >= SubmenuCloseDelayMs)
{
i32 keepDepth = -1;
for (i32 i = 0; i < (i32)m_panels.size(); ++i)
{
Rectangle r { m_panels[i].position, m_panels[i].size };
if (r.contains(m_mousePos, true)) keepDepth = i;
}
if (keepDepth >= 0)
{
pop_to_depth(keepDepth);
m_panels[keepDepth].openedSubmenuIndex = -1;
}
m_hoverCloseTimer.endCount();
m_hoverCloseTimerActive = false;
} }
gfx.drawRect(*this, getBorderColor(), 2);
} }
void ContextMenu::onMouseReleased(const Event& event) void ContextMenu::onMouseReleased(const Event& event)
{ {
if (!m_visible) return;
Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; Vec2 mousePos { event.mouse->position_x, event.mouse->position_y };
if (contains(mousePos, true))
{
for (i32 i = (i32)m_panels.size() - 1; i >= 0; --i)
{
i32 idx = entry_index_at(m_panels[i], mousePos);
if (idx >= 0)
{
Entry& e = (*m_panels[i].entries)[idx];
if (e.submenus.empty())
{
if (m_data.onActivate && e.id >= 0)
m_data.onActivate(e);
hide();
event.handle();
hide();
} }
else else
{ {
hide(); pop_to_depth(i);
Rectangle r = entry_rect(m_panels[i], idx);
Vec2 anchor { m_panels[i].position.x + m_panels[i].size.x, r.y };
push_panel(e.submenus, anchor, false);
m_panels[i].openedSubmenuIndex = idx;
} }
event.handle(); event.handle();
return;
}
}
hide();
event.handle();
} }
void ContextMenu::onMouseMoved(const Event& event) void ContextMenu::onMouseMoved(const Event& event)
{ {
if (!m_visible) return;
Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; Vec2 mousePos { event.mouse->position_x, event.mouse->position_y };
if (contains(mousePos, true))
{
m_mousePos = mousePos; m_mousePos = mousePos;
i32 hitPanel = -1;
i32 hitEntry = -1;
for (i32 i = (i32)m_panels.size() - 1; i >= 0; --i)
{
i32 idx = entry_index_at(m_panels[i], mousePos);
if (idx >= 0) { hitPanel = i; hitEntry = idx; break; }
Rectangle panelRect { m_panels[i].position, m_panels[i].size };
if (panelRect.contains(mousePos, true)) { hitPanel = i; hitEntry = -1; break; }
} }
for (i32 i = 0; i < (i32)m_panels.size(); ++i)
m_panels[i].hoveredIndex = (i == hitPanel ? hitEntry : -1);
if (hitPanel < 0)
{
if (!m_hoverCloseTimerActive)
{
m_hoverCloseTimer.startCount(ostd::eTimeUnits::Milliseconds);
m_hoverCloseTimerActive = true;
}
if (m_hoverOpenTimerActive)
{
m_hoverOpenTimer.endCount();
m_hoverOpenTimerActive = false;
}
m_pendingOpenPanelDepth = -1;
event.handle();
return;
}
if ((i32)m_panels.size() - 1 > hitPanel)
{
if (hitEntry != m_panels[hitPanel].openedSubmenuIndex)
{
if (!m_hoverCloseTimerActive)
{
m_hoverCloseTimer.startCount(ostd::eTimeUnits::Milliseconds);
m_hoverCloseTimerActive = true;
}
}
else
{
if (m_hoverCloseTimerActive)
{
m_hoverCloseTimer.endCount();
m_hoverCloseTimerActive = false;
}
}
}
else
{
if (m_hoverCloseTimerActive)
{
m_hoverCloseTimer.endCount();
m_hoverCloseTimerActive = false;
}
}
if (hitEntry >= 0)
{
Entry& e = (*m_panels[hitPanel].entries)[hitEntry];
bool hasChildren = e.submenus.size() > 0;
bool alreadyOpen = (m_panels[hitPanel].openedSubmenuIndex == hitEntry) && ((i32)m_panels.size() - 1 > hitPanel);
if (hasChildren && !alreadyOpen)
{
if (m_pendingOpenPanelDepth != hitPanel || m_pendingOpenEntryIndex != hitEntry)
{
m_pendingOpenPanelDepth = hitPanel;
m_pendingOpenEntryIndex = hitEntry;
m_hoverOpenTimer.startCount(ostd::eTimeUnits::Milliseconds);
m_hoverOpenTimerActive = true;
}
}
else
{
if (m_hoverOpenTimerActive)
{
m_hoverOpenTimer.endCount();
m_hoverOpenTimerActive = false;
}
m_pendingOpenPanelDepth = -1;
}
}
event.handle(); event.handle();
} }
@ -128,49 +252,163 @@ namespace ogfx
event.handle(); event.handle();
} }
void ContextMenu::show(void)
{
m_visible = true;
}
void ContextMenu::show(const Vec2& pos) void ContextMenu::show(const Vec2& pos)
{ {
setPosition(pos); m_panels.clear();
push_panel(m_data.entries, pos, false);
m_visible = true; m_visible = true;
} }
void ContextMenu::hide(void) void ContextMenu::hide(void)
{ {
m_panels.clear();
m_hoverOpenTimer.endCount();
m_hoverCloseTimer.endCount();
m_hoverOpenTimerActive = false;
m_hoverCloseTimerActive = false;
m_pendingOpenPanelDepth = -1;
m_pendingOpenEntryIndex = -1;
m_visible = false; m_visible = false;
} }
void ContextMenu::setInstance(const Instance& instance) void ContextMenu::setInstance(const Instance& instance)
{ {
m_data = instance; m_data = instance;
update_size(); m_panels.clear();
m_pendingOpenPanelDepth = -1;
m_pendingOpenEntryIndex = -1;
m_hoverOpenTimer.endCount();
m_hoverCloseTimer.endCount();
if (m_visible)
push_panel(m_data.entries, getPosition(), false);
} }
void ContextMenu::setFontSize(i32 size) void ContextMenu::setFontSize(i32 size)
{ {
m_fontSize = size; m_fontSize = size;
update_size(); relayout_panels();
} }
void ContextMenu::update_size(void) void ContextMenu::push_panel(stdvec<Entry>& entries, const Vec2& anchorTopLeft, bool flipLeft)
{ {
setSize(0, 0); Panel p;
p.entries = &entries;
p.position = anchorTopLeft;
compute_panel_size(p);
f32 windowW = (f32)m_window.getWindowWidth();
f32 windowH = (f32)m_window.getWindowHeight();
if (flipLeft && !m_panels.empty())
{
// anchorTopLeft was computed assuming "open right"; flip to open left
p.position.x = m_panels.back().position.x - p.size.x;
}
else if (p.position.x + p.size.x > windowW && !m_panels.empty())
{
p.position.x = m_panels.back().position.x - p.size.x;
}
if (p.position.y + p.size.y > windowH)
p.position.y = windowH - p.size.y;
if (p.position.y < 0) p.position.y = 0;
if (p.position.x < 0) p.position.x = 0;
m_panels.push_back(p);
}
void ContextMenu::compute_panel_size(Panel& panel)
{
panel.size = { 0, 0 };
auto& gfx = m_window.getGFX(); auto& gfx = m_window.getGFX();
for (auto& entry : m_data.entries) for (auto& entry : *panel.entries)
{ {
auto s = gfx.getStringDimensions(entry.text, getFontSize()); auto s = gfx.getStringDimensions(entry.text, getFontSize());
f32 extra = (entry.submenus.size() > 0 ? s.y : 0); f32 extra = (entry.submenus.size() > 0 ? s.y : 0);
if (s.x + extra > getw()) if (s.x + extra > panel.size.x)
setw(s.x + extra); panel.size.x = s.x + extra;
addh(s.y + m_spacing); panel.size.y += s.y + m_spacing;
m_entryHeight = s.y + m_spacing; panel.entryHeight = s.y + m_spacing;
entry.update_size(gfx, getFontSize());
} }
addSize({ m_padding.x + m_padding.w, m_padding.y + m_padding.h }); panel.size.x += m_padding.x + m_padding.w;
panel.size.y += m_padding.y + m_padding.h;
}
void ContextMenu::pop_to_depth(size_t depth)
{
if (depth + 1 < m_panels.size())
m_panels.resize(depth + 1);
}
Rectangle ContextMenu::entry_rect(const Panel& panel, i32 index) const
{
return {
panel.position.x,
panel.position.y + m_padding.y + (index * panel.entryHeight),
panel.size.x,
panel.entryHeight
};
}
i32 ContextMenu::entry_index_at(const Panel& panel, const Vec2& mousePos) const
{
Rectangle panelRect { panel.position, panel.size };
if (!panelRect.contains(mousePos, true)) return -1;
for (i32 i = 0; i < (i32)panel.entries->size(); ++i)
{
if (entry_rect(panel, i).contains(mousePos, true))
return i;
}
return -1;
}
void ContextMenu::relayout_panels(void)
{
for (auto& panel : m_panels)
compute_panel_size(panel);
}
void ContextMenu::draw_panel(BasicRenderer2D& gfx, const Panel& panel)
{
gfx.fillRect({ panel.position, panel.size }, getBackgroundColor());
const f32 triPad = 14;
f32 y = 0;
i32 i = 0;
auto& entries = *panel.entries;
for (auto& entry : entries)
{
Vec2 entryPos = panel.position + Vec2 { m_padding.x, y + m_padding.y };
Rectangle rect = { entryPos - Vec2 { m_padding.x, 0 }, panel.size.x, panel.entryHeight };
bool highlighted = (panel.hoveredIndex == i)
|| (panel.openedSubmenuIndex == i);
if (highlighted)
{
if (isSelectionGradientEnabled())
gfx.fillGradientRect(rect, m_selectionGradient);
else
gfx.fillRect(rect, getSelectionColor());
gfx.drawRect(rect, getSeparatorLineColor(), 1, false, false, i != entries.size() , false);
gfx.drawVCenteredString(entry.text, { entryPos, panel.size.x, panel.entryHeight }, getSelectionTextColor(), getFontSize());
}
else
{
gfx.drawRect(rect, getSeparatorLineColor(), 1, false, false, i != entries.size() , false);
gfx.drawVCenteredString(entry.text, { entryPos, panel.size.x, panel.entryHeight }, getTextColor(), getFontSize());
}
if (entry.submenus.size() > 0)
{
Triangle tri {
{ rect.x + rect.w - panel.entryHeight + triPad, rect.y + (triPad * 0.5f) },
{ rect.x + rect.w - (triPad * 0.5f), rect.y + (rect.h * 0.5f) },
{ rect.x + rect.w - panel.entryHeight + triPad, rect.y + rect.h - (triPad * 0.5f) }
};
gfx.fillTriangle(tri, getSubmenuIndicatorColor());
}
y += panel.entryHeight;
i++;
}
gfx.drawRect({ panel.position, panel.size }, getBorderColor(), 2);
} }
} }
} }

View file

@ -30,30 +30,42 @@ namespace ogfx
{ {
public: struct Entry public: struct Entry
{ {
inline Entry(const String& t, const stdvec<Entry>& sub = {}) { text = t; submenus = sub; } inline Entry(const String& t, i32 id = -1, const stdvec<Entry>& sub = {}) { text = t; this->id = id; submenus = sub; }
inline Entry(const String& t, const stdvec<Entry>& sub) { text = t; id = -1; submenus = sub; }
String text { "" }; String text { "" };
i32 id { -1 };
stdvec<Entry> submenus; stdvec<Entry> submenus;
private: private:
Vec2 size; Vec2 size;
void update_size(BasicRenderer2D& gfx, i32 fontSize); void update_size(BasicRenderer2D& gfx, i32 fontSize);
friend class ContextMenu; friend class ContextMenu;
}; };
public: struct Instance public: struct Instance
{ {
using Callback = std::function<void(const Entry&)>;
stdvec<Entry> entries; stdvec<Entry> entries;
Callback onActivate { nullptr };
};
private: struct Panel
{
stdvec<Entry>* entries { nullptr };
Vec2 position { 0, 0 };
Vec2 size { 0, 0 };
f32 entryHeight { 0 };
i32 hoveredIndex { -1 };
i32 openedSubmenuIndex { -1 };
}; };
public: public:
inline ContextMenu(WindowCore& window) : m_window(window) { create(); } inline ContextMenu(WindowCore& window) : m_window(window) { create(); }
ContextMenu& create(void); ContextMenu& create(void);
void applyTheme(const ostd::Stylesheet& theme); void applyTheme(const ostd::Stylesheet& theme);
void draw(BasicRenderer2D& gfx); void draw(BasicRenderer2D& gfx);
void update(void);
void onMouseReleased(const Event& event); void onMouseReleased(const Event& event);
void onMouseMoved(const Event& event); void onMouseMoved(const Event& event);
void onMousePressed(const Event& event); void onMousePressed(const Event& event);
void onMouseScrolled(const Event& event); void onMouseScrolled(const Event& event);
void show(void);
void show(const Vec2& pos); void show(const Vec2& pos);
void hide(void); void hide(void);
void setInstance(const Instance& instance); void setInstance(const Instance& instance);
@ -71,9 +83,17 @@ namespace ogfx
OSTD_PARAM_GETSET(Color, TextColor, m_textColor); OSTD_PARAM_GETSET(Color, TextColor, m_textColor);
OSTD_PARAM_GETSET(Color, SubmenuIndicatorColor, m_submenuIndicatorColor); OSTD_PARAM_GETSET(Color, SubmenuIndicatorColor, m_submenuIndicatorColor);
OSTD_PARAM_GETSET(Color, BorderColor, m_borderColor); OSTD_PARAM_GETSET(Color, BorderColor, m_borderColor);
OSTD_PARAM_GETSET(ColorGradient, SelectionGradient, m_selectionGradient);
OSTD_BOOL_PARAM_GETSET_E(SelectionGradient, m_useSelectionGradient);
private: private:
void update_size(void); void push_panel(stdvec<Entry>& entries, const Vec2& anchorTopLeft, bool flipLeft = false);
void pop_to_depth(size_t depth);
void compute_panel_size(Panel& panel);
i32 entry_index_at(const Panel& panel, const Vec2& mousePos) const;
Rectangle entry_rect(const Panel& panel, i32 index) const;
void draw_panel(BasicRenderer2D& gfx, const Panel& panel);
void relayout_panels(void);
private: private:
WindowCore& m_window; WindowCore& m_window;
@ -82,6 +102,15 @@ namespace ogfx
f32 m_entryHeight { 0 }; f32 m_entryHeight { 0 };
Vec2 m_mousePos { 0, 0 }; Vec2 m_mousePos { 0, 0 };
stdvec<Panel> m_panels;
i32 m_pendingOpenPanelDepth { -1 };
i32 m_pendingOpenEntryIndex { -1 };
ostd::Timer m_hoverOpenTimer;
ostd::Timer m_hoverCloseTimer;
bool m_hoverOpenTimerActive { false };
bool m_hoverCloseTimerActive { false };
Rectangle m_padding { 16, 0, 16, 0 }; Rectangle m_padding { 16, 0, 16, 0 };
f32 m_spacing { 8 }; f32 m_spacing { 8 };
i32 m_fontSize { 18 }; i32 m_fontSize { 18 };
@ -92,8 +121,11 @@ namespace ogfx
Color m_textColor { "#F16A85FF" }; Color m_textColor { "#F16A85FF" };
Color m_submenuIndicatorColor { "#111111FF" }; Color m_submenuIndicatorColor { "#111111FF" };
Color m_borderColor { "#400000FF" }; Color m_borderColor { "#400000FF" };
bool m_useSelectionGradient { true };
ColorGradient m_selectionGradient { { "#C21135FF", "#820B23FF" }, { 1.0f } };
static constexpr u64 SubmenuOpenDelayMs { 300 };
static constexpr u64 SubmenuCloseDelayMs { 50 };
friend class Instance; friend class Instance;
}; };
} }

View file

@ -993,6 +993,7 @@ namespace ogfx
void Window::__on_update(f64 delta) void Window::__on_update(f64 delta)
{ {
m_cmenu.update();
onUpdate(delta); onUpdate(delta);
} }

View file

@ -183,6 +183,11 @@ class Window : public ogfx::gui::Window
return true; return true;
}); });
m_menu.onActivate = [this](const ogfx::gui::ContextMenu::Entry& e) {
std::cout << e.text << " - " << (i32)e.id << "\n";
};
setContextMenu(m_menu); setContextMenu(m_menu);
} }
@ -234,15 +239,23 @@ class Window : public ogfx::gui::Window
ListView m_list { *this }; ListView m_list { *this };
Label m_drawCallsLbl { *this }; Label m_drawCallsLbl { *this };
ogfx::gui::ContextMenu::Instance m_menu { enum MenuId { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };
{
{ "Update" }, ogfx::gui::ContextMenu::Instance m_menu { {
{ "File" }, { "File", -1, {
{ "Open RAW", { { "Image" }, { "Text File" }, { "Audio" } } }, { "New", MenuId::New },
{ "Mouse Settings", { { "Test" } } }, { "Open...", MenuId::Open },
{ "Terror" }, { "Save", MenuId::Save },
{ "Properties" } { "Save As", MenuId::SaveAs },
} }},
{ "Edit", -1, {
{ "Copy", -1, {
{ "As Text", MenuId::CopyRaw },
{ "As HTML", MenuId::CopyFormatted },
}},
}},
{ "Exit", MenuId::Exit } },
nullptr
}; };
ostd::StepTimer m_timer; ostd::StepTimer m_timer;