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
submenuIndicatorColor = $accentColorDark
borderColor = #400000FF
useSelectionGradient = true
selectionGradient = gradientV(#C21135FF - #820B23FF)
}
% ==========================

View file

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

View file

@ -1 +1 @@
2073
2074

View file

@ -39,9 +39,12 @@ namespace ogfx
}
}
ContextMenu& ContextMenu::create(void)
{
setSize(130, 300);
return *this;
}
@ -57,64 +60,185 @@ namespace ogfx
setTextColor(theme.get<Color>("context.textColor", getTextColor(), {}, {}));
setSubmenuIndicatorColor(theme.get<Color>("context.submenuIndicatorColor", getSubmenuIndicatorColor(), {}, {}));
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)
{
gfx.fillRect(*this, getBackgroundColor());
const f32 triPad = 14;
f32 y = 0;
i32 i = 0;
for (auto& entry : m_data.entries)
if (!m_visible) return;
for (const auto& panel : m_panels)
draw_panel(gfx, panel);
}
void ContextMenu::update(void)
{
if (!m_visible) return;
if (m_pendingOpenPanelDepth >= 0 && m_hoverOpenTimerActive && m_hoverOpenTimer.read() >= SubmenuOpenDelayMs)
{
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
{
gfx.drawRect(rect, getSeparatorLineColor(), 1, false, false, i != m_data.entries.size() , false);
gfx.drawVCenteredString(entry.text, { entryPos, getw(), m_entryHeight }, getTextColor(), getFontSize());
}
if (entry.submenus.size() > 0)
{
Triangle tri {
{ rect.x + rect.w - m_entryHeight + triPad, rect.y + (triPad * 0.5f) },
{ rect.x + rect.w - (triPad * 0.5f), rect.y + (rect.h * 0.5f) },
{ rect.x + rect.w - m_entryHeight + triPad, rect.y + rect.h - (triPad * 0.5f) }
};
gfx.fillTriangle(tri, getSubmenuIndicatorColor());
}
y += m_entryHeight;
i++;
Panel& parent = m_panels[m_pendingOpenPanelDepth];
i32 entryIdx = m_pendingOpenEntryIndex;
Entry& e = (*parent.entries)[entryIdx];
pop_to_depth(m_pendingOpenPanelDepth);
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;
}
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)
{
if (!m_visible) return;
Vec2 mousePos { event.mouse->position_x, event.mouse->position_y };
if (contains(mousePos, true))
{
}
else
for (i32 i = (i32)m_panels.size() - 1; i >= 0; --i)
{
hide();
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
{
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();
return;
}
}
hide();
event.handle();
}
void ContextMenu::onMouseMoved(const Event& event)
{
if (!m_visible) return;
Vec2 mousePos { event.mouse->position_x, event.mouse->position_y };
if (contains(mousePos, true))
m_mousePos = mousePos;
i32 hitPanel = -1;
i32 hitEntry = -1;
for (i32 i = (i32)m_panels.size() - 1; i >= 0; --i)
{
m_mousePos = mousePos;
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();
}
@ -128,49 +252,163 @@ namespace ogfx
event.handle();
}
void ContextMenu::show(void)
{
m_visible = true;
}
void ContextMenu::show(const Vec2& pos)
{
setPosition(pos);
m_panels.clear();
push_panel(m_data.entries, pos, false);
m_visible = true;
}
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;
}
void ContextMenu::setInstance(const Instance& 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)
{
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();
for (auto& entry : m_data.entries)
for (auto& entry : *panel.entries)
{
auto s = gfx.getStringDimensions(entry.text, getFontSize());
f32 extra = (entry.submenus.size() > 0 ? s.y : 0);
if (s.x + extra > getw())
setw(s.x + extra);
addh(s.y + m_spacing);
m_entryHeight = s.y + m_spacing;
entry.update_size(gfx, getFontSize());
if (s.x + extra > panel.size.x)
panel.size.x = s.x + extra;
panel.size.y += s.y + m_spacing;
panel.entryHeight = s.y + m_spacing;
}
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
{
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 { "" };
i32 id { -1 };
stdvec<Entry> submenus;
private:
Vec2 size;
void update_size(BasicRenderer2D& gfx, i32 fontSize);
friend class ContextMenu;
};
public: struct Instance
{
using Callback = std::function<void(const Entry&)>;
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:
inline ContextMenu(WindowCore& window) : m_window(window) { create(); }
ContextMenu& create(void);
void applyTheme(const ostd::Stylesheet& theme);
void draw(BasicRenderer2D& gfx);
void update(void);
void onMouseReleased(const Event& event);
void onMouseMoved(const Event& event);
void onMousePressed(const Event& event);
void onMouseScrolled(const Event& event);
void show(void);
void show(const Vec2& pos);
void hide(void);
void setInstance(const Instance& instance);
@ -71,9 +83,17 @@ namespace ogfx
OSTD_PARAM_GETSET(Color, TextColor, m_textColor);
OSTD_PARAM_GETSET(Color, SubmenuIndicatorColor, m_submenuIndicatorColor);
OSTD_PARAM_GETSET(Color, BorderColor, m_borderColor);
OSTD_PARAM_GETSET(ColorGradient, SelectionGradient, m_selectionGradient);
OSTD_BOOL_PARAM_GETSET_E(SelectionGradient, m_useSelectionGradient);
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:
WindowCore& m_window;
@ -82,6 +102,15 @@ namespace ogfx
f32 m_entryHeight { 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 };
f32 m_spacing { 8 };
i32 m_fontSize { 18 };
@ -92,8 +121,11 @@ namespace ogfx
Color m_textColor { "#F16A85FF" };
Color m_submenuIndicatorColor { "#111111FF" };
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;
};
}

View file

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

View file

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