diff --git a/extra/DefaultThemeDark.oss b/extra/DefaultThemeDark.oss index 273df15..b2f7624 100644 --- a/extra/DefaultThemeDark.oss +++ b/extra/DefaultThemeDark.oss @@ -318,6 +318,7 @@ const $backgroundColor = rgba(30, 30, 30, 255) selectionTextColor = $accentColorLight borderColor = $borderColor showBorder = true + showBackground = true } (@tool_button button) { fontSize = 18 @@ -326,7 +327,7 @@ const $backgroundColor = rgba(30, 30, 30, 255) showBackground = true backgroundColor = $darkColor useBackgroundGradient = false - showBorder = true + showBorder = false borderColor = color_black showIcon = true (icon) { @@ -336,12 +337,14 @@ const $backgroundColor = rgba(30, 30, 30, 255) (@tool_button button:hover) { borderColor = $textColor backgroundColor = $accentColorDark + showBorder = true (icon) { tint = $textColor } } (@tool_button button:pressed) { borderColor = $accentColorDark + showBorder = true (icon) { tint = $accentColorLight } @@ -359,13 +362,15 @@ const $backgroundColor = rgba(30, 30, 30, 255) showBorder = true triangleIndicatorColor = $accentColor triangleIndicatorPadding = 8.0 + textColor = $textColor + fontSize = 18 } (combo:hover) { triangleIndicatorColor = $accentColorLight borderColor = $accentColorLight } (combo:active) { - triangleIndicatorColor = $accentColorDark - borderColor = $accentColorDark + triangleIndicatorColor = $accentColorLight + borderColor = $accentColorLight } % ====================== diff --git a/other/TODO.txt b/other/TODO.txt index 96dadfd..eb0fa97 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -48,6 +48,7 @@ FIX: Widget Content extents increasing negatively not showing scrollbars FIX: Button state getting hung after showing an openFileDialog FIX: ListView last item is covered by scrollbar FIX: ListView last item not clickable even when no horizontal scrollbar present +FIX: Refresh display text in ComboBox when font size changes and also on theme reload diff --git a/src/ogfx/gui/ContextMenu.cpp b/src/ogfx/gui/ContextMenu.cpp index 071fd0a..211c213 100644 --- a/src/ogfx/gui/ContextMenu.cpp +++ b/src/ogfx/gui/ContextMenu.cpp @@ -345,6 +345,12 @@ namespace ogfx } panel.size.x += m_padding.x + m_padding.w; panel.size.y += m_padding.y + m_padding.h; + + // Enforce minimum width from the Instance — applies to root only. // NEW + // Submenus sit naturally to the right; forcing them wider tends to look // NEW + // odd and can cause needless edge-flipping. // NEW + if (panel.entries == &m_data.entries && panel.size.x < m_data.minWidth) // NEW + panel.size.x = m_data.minWidth; // NEW } void ContextMenu::pop_to_depth(size_t depth) diff --git a/src/ogfx/gui/ContextMenu.hpp b/src/ogfx/gui/ContextMenu.hpp index 38e9f54..9a0b62c 100644 --- a/src/ogfx/gui/ContextMenu.hpp +++ b/src/ogfx/gui/ContextMenu.hpp @@ -46,6 +46,61 @@ namespace ogfx using Callback = std::function; stdvec entries; Callback onActivate { nullptr }; + f32 minWidth { 0 }; + + // Remove first entry whose text matches. Returns true if something was removed. + inline bool removeByText(const String& text, bool recursive = true) + { + return remove_if(entries, [&](const Entry& e) { return e.text == text; }, recursive); + } + // Remove first entry whose id matches. ID -1 is treated as "no id" and is skipped. + inline bool removeById(i32 id, bool recursive = true) + { + if (id < 0) return false; + return remove_if(entries, [&](const Entry& e) { return e.id == id; }, recursive); + } + // Remove ALL matches (handy if you reuse text/ids across submenus). + inline size_t removeAllByText(const String& text, bool recursive = true) + { + return remove_all_if(entries, [&](const Entry& e) { return e.text == text; }, recursive); + } + inline size_t removeAllById(i32 id, bool recursive = true) + { + if (id < 0) return 0; + return remove_all_if(entries, [&](const Entry& e) { return e.id == id; }, recursive); + } + + private: + template + static bool remove_if(stdvec& list, Pred pred, bool recursive) + { + for (auto it = list.begin(); it != list.end(); ++it) + { + if (pred(*it)) { list.erase(it); return true; } + } + if (!recursive) return false; + for (auto& e : list) + { + if (remove_if(e.submenus, pred, true)) return true; + } + return false; + } + template + static size_t remove_all_if(stdvec& list, Pred pred, bool recursive) + { + size_t count = 0; + for (auto it = list.begin(); it != list.end(); ) + { + if (pred(*it)) { it = list.erase(it); ++count; } + else ++it; + } + if (recursive) + { + for (auto& e : list) + count += remove_all_if(e.submenus, pred, true); + } + return count; + } }; private: struct Panel { diff --git a/src/ogfx/gui/MenuBar.cpp b/src/ogfx/gui/MenuBar.cpp index 77a0117..95c80e5 100644 --- a/src/ogfx/gui/MenuBar.cpp +++ b/src/ogfx/gui/MenuBar.cpp @@ -104,8 +104,7 @@ namespace ogfx // 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_window.showContextMenu(slot.instance, anchor); m_openIndex = index; m_active = true; diff --git a/src/ogfx/gui/ToolBar.cpp b/src/ogfx/gui/ToolBar.cpp index 6897a87..fb1d2f9 100644 --- a/src/ogfx/gui/ToolBar.cpp +++ b/src/ogfx/gui/ToolBar.cpp @@ -82,25 +82,19 @@ namespace ogfx void ToolBar::applyTheme(const ostd::Stylesheet& theme) { auto& win = cast(getWindow()); - setHeight(theme.get("toolbar.height", getHeight(), {}, {})); - setFontSize(theme.get("toolbar.fontSize", getFontSize(), {}, {})); - setBackgroundColor(theme.get("toolbar.backgroundColor", getBackgroundColor(), {}, {})); - setTextColor(theme.get("toolbar.textColor", getTextColor(), {}, {})); - setBorderColor(theme.get("toolbar.borderColor", getBorderColor(), {}, {})); - enableBottomBorder(theme.get("toolbar.showBorder", isBottomBorderEnabled(), {}, {})); + setHeight(getThemeValue(theme, "height", getHeight())); setSize(win.getWindowWidth(), m_height); + enableBottomBorder(isBorderEnabled()); disableBorder(); } - void ToolBar::onDraw(BasicRenderer2D& gfx) + void ToolBar::afterDraw(BasicRenderer2D& gfx) { - gfx.fillRect(*this, m_backgroundColor); - // Bottom border line if (isBottomBorderEnabled()) { f32 lineOffset = (isStatusBar() ? 0.0f : m_height); - gfx.drawLine({ { getx(), gety() + lineOffset }, { getx() + getw(), gety() + lineOffset } }, m_borderColor); + gfx.drawLine({ { getx(), gety() + lineOffset }, { getx() + getw(), gety() + lineOffset } }, getBorderColor()); } } diff --git a/src/ogfx/gui/ToolBar.hpp b/src/ogfx/gui/ToolBar.hpp index 1f04d38..34ad057 100644 --- a/src/ogfx/gui/ToolBar.hpp +++ b/src/ogfx/gui/ToolBar.hpp @@ -37,7 +37,7 @@ namespace ogfx Button& addButton(const String& iconPath, const String& text = "", EventCallback callback = nullptr); void onWindowResized(const Event& event) override; void applyTheme(const ostd::Stylesheet& theme) override; - void onDraw(BasicRenderer2D& gfx) override; + void afterDraw(BasicRenderer2D& gfx) override; void onUpdate(void) override; inline void setw(f32 ww) override { } @@ -47,10 +47,6 @@ namespace ogfx inline void setAsStatusBar(bool set = true) { m_isStatusBar = set; } OSTD_PARAM_GETSET(f32, Height, m_height); - 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, BorderColor, m_borderColor); OSTD_BOOL_PARAM_GETSET_E_NEG(ButtonText, m_disableButtonText); OSTD_BOOL_PARAM_GETSET_E(BottomBorder, m_bottomBorder); @@ -64,10 +60,6 @@ namespace ogfx f32 m_height { 26 }; bool m_bottomBorder { false }; - i32 m_fontSize { 16 }; - Color m_backgroundColor { "#6B0A1DFF" }; - Color m_textColor { "#F16A85FF" }; - Color m_borderColor { "#400000FF" }; }; } } diff --git a/src/ogfx/gui/widgets/ComboBox.cpp b/src/ogfx/gui/widgets/ComboBox.cpp index c3a8e98..ba6a2d5 100644 --- a/src/ogfx/gui/widgets/ComboBox.cpp +++ b/src/ogfx/gui/widgets/ComboBox.cpp @@ -20,6 +20,7 @@ #include "ComboBox.hpp" #include "../../render/BasicRenderer.hpp" +#include "../Window.hpp" namespace ogfx { @@ -32,37 +33,48 @@ namespace ogfx disableChildren(); setStylesheetCategoryName("combo"); connectSignal(ostd::BuiltinSignals::MouseReleased); + connectSignal(ostd::BuiltinSignals::MouseMoved); validate(); return *this; } - void ComboBox::onDraw(ogfx::BasicRenderer2D& gfx) + i32 ComboBox::addMenuItem(const String& text) { - gfx.outlinedRect(*this, getBackgroundColor(), getBorderColor(), getBorderWidth()); - - // Triangle Indicator - const f32 triPad = getTriangleIndicatorPadding(); - const f32 triSide = geth() - (triPad * 2); - Triangle tri { - getGlobalPosition() + Vec2 { getw() - triPad - triSide, triPad }, - getGlobalPosition() + Vec2 { getw() - triPad, triPad }, - getGlobalPosition() + Vec2 { getw() - triPad - (triSide * 0.5f), geth() - triPad } - }; - gfx.fillTriangle(tri, getTriangleIndicatorColor()); - } - - void ComboBox::onUpdate(void) - { - - } - - void ComboBox::onMouseReleased(const Event& event) - { - if (isMouseInside()) + m_menu.entries.push_back({ text, m_nextItemID++ }); + if (m_menu.entries.size() == 1) { - setThemeQualifier("active", true); - m_dropDownShown = true; + auto& entry = m_menu.entries.back(); + m_selectedEntry = entry; + m_selectedEntryText = get_display_text(entry.text); } + return m_nextItemID - 1; + } + + bool ComboBox::removeMenuItem(const String& text) + { + return m_menu.removeByText(text, false); + } + + bool ComboBox::removeMenuItem(i32 id) + { + return m_menu.removeById(id, false); + } + + void ComboBox::setItemCallback(ContextMenu::Instance::Callback callback) + { + m_menu.onActivate = [this, cb = std::move(callback)] (const ContextMenu::Entry& entry) -> void { + m_selectedEntry = entry; + m_selectedEntryText = get_display_text(entry.text); // Caching because get_display_text is expensive + if (cb) cb(entry); + }; + } + + void ComboBox::setw(f32 w) + { + Rectangle::setw(w); + m_menu.minWidth = getw(); + if (m_selectedEntry.id >= 0) + m_selectedEntryText = get_display_text(m_selectedEntry.text); } void ComboBox::applyTheme(const ostd::Stylesheet& theme) @@ -71,16 +83,97 @@ namespace ogfx setTriangleIndicatorPadding(getThemeValue(theme, "triangleIndicatorPadding", getTriangleIndicatorPadding())); } - void ComboBox::handleSignal(ostd::Signal& signal) + void ComboBox::onDraw(ogfx::BasicRenderer2D& gfx) + { + gfx.outlinedRect(*this, getBackgroundColor(), getBorderColor(), getBorderWidth()); + + auto bounds = getGlobalBounds(); + gfx.drawCenteredString(m_selectedEntryText, bounds - Rectangle { 0, 0, geth(), 0 }, getTextColor(), getFontSize()); + + // Triangle Indicator + const f32 triPad = getTriangleIndicatorPadding(); + const f32 triSide = geth() - (triPad * 2); + Triangle tri { + bounds.getPosition() + Vec2 { getw() - triPad - triSide, triPad }, + bounds.getPosition() + Vec2 { getw() - triPad, triPad }, + bounds.getPosition() + Vec2 { getw() - triPad - (triSide * 0.5f), geth() - triPad } + }; + gfx.fillTriangle(tri, getTriangleIndicatorColor()); + } + + void ComboBox::onMouseReleased(const Event& event) + { + if (contains(event.mouse->position_x, event.mouse->position_y)) + { + if (m_dropDownShown) + { + setThemeQualifier("active", false); + m_dropDownShown = false; + return; + } + setThemeQualifier("active", true); + m_dropDownShown = true; + Vec2 anchor = getGlobalPosition() + Vec2 { 0, geth() }; + cast(getWindow()).showContextMenu(m_menu, anchor); + } + } + + void ComboBox::handleSignal(ostd::Signal& signal) // This is to get the global events, in order to bypass + // the fact that some components like MenuBar call + // event.handle() at the very beginning of the event chain { if (signal.ID == ostd::BuiltinSignals::MouseReleased) { - if (isMouseInside()) + auto& med = cast(signal.userData); + if (contains(med.position_x, med.position_y)) return; if (!m_dropDownShown) return; setThemeQualifier("active", false); + reloadTheme(); + m_dropDownShown = false; } + else if (signal.ID == ostd::BuiltinSignals::MouseMoved) + { + auto& med = cast(signal.userData); + if (contains(med.position_x, med.position_y)) + return; + setThemeQualifier("hover", false); + } + } + + String ComboBox::get_display_text(const String& entry) + { + auto& gfx = getWindow().getGFX(); + f32 _w = getw() - geth(); // Reserve space for HxH triangle indicator + + // Fast path: full text fits. + if (gfx.getStringDimensions(entry, getFontSize()).x <= _w) + return entry; + + // Reserve space for the ellipsis. + f32 ellipsisW = gfx.getStringDimensions("...", getFontSize()).x; + f32 budget = _w - ellipsisW; + + if (budget <= 0) + return ""; // Not even room for "..." — degenerate but possible if the combo is tiny + + // Binary search for the largest prefix length that fits within `budget`. + // Invariant: any length <= lo fits; any length > hi does not. + size_t lo = 0; + size_t hi = entry.len(); // adjust to your String's length method + + while (lo < hi) + { + size_t mid = lo + (hi - lo + 1) / 2; // upper-mid, avoids infinite loop when hi == lo+1 + String prefix = entry.new_substr(0, mid); + if (gfx.getStringDimensions(prefix, getFontSize()).x <= budget) + lo = mid; // mid fits, try longer + else + hi = mid - 1; // mid doesn't fit, must be shorter + } + + return entry.new_substr(0, lo) + "..."; } } } diff --git a/src/ogfx/gui/widgets/ComboBox.hpp b/src/ogfx/gui/widgets/ComboBox.hpp index d32e4fe..8600e0b 100644 --- a/src/ogfx/gui/widgets/ComboBox.hpp +++ b/src/ogfx/gui/widgets/ComboBox.hpp @@ -32,19 +32,29 @@ namespace ogfx public: inline ComboBox(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); } ComboBox& create(void); - - void onDraw(ogfx::BasicRenderer2D& gfx) override; - void onUpdate(void) override; - void onMouseReleased(const Event& event) override; + i32 addMenuItem(const String& text); + bool removeMenuItem(const String& text); + bool removeMenuItem(i32 id); + void setItemCallback(ContextMenu::Instance::Callback callback); + void setw(f32 w) override; void applyTheme(const ostd::Stylesheet& theme) override; + void onDraw(ogfx::BasicRenderer2D& gfx) override; + void onMouseReleased(const Event& event) override; void handleSignal(ostd::Signal& signal) override; + inline ContextMenu::Entry getSelectedEntry(void) const { return m_selectedEntry; } OSTD_PARAM_GETSET(Color, TriangleIndicatorColor, m_triangleColor); OSTD_PARAM_GETSET(f32, TriangleIndicatorPadding, m_trianglePadding); + private: + String get_display_text(const String& entry); + private: bool m_dropDownShown { false }; ContextMenu::Instance m_menu; + i32 m_nextItemID { 0 }; + ContextMenu::Entry m_selectedEntry { "", -1 }; + String m_selectedEntryText { "" }; // Truncated text, not always valid, so can't be exposed Color m_triangleColor { "#008000FF" }; f32 m_trianglePadding { 8.0f }; diff --git a/src/ogfx/gui/widgets/Widget.hpp b/src/ogfx/gui/widgets/Widget.hpp index 496cd2c..3813338 100644 --- a/src/ogfx/gui/widgets/Widget.hpp +++ b/src/ogfx/gui/widgets/Widget.hpp @@ -76,7 +76,7 @@ namespace ogfx bool addThemeID(const String& id); bool removeThemeID(const String& id); void setVisible(bool v); - void setCallback(eCallback type, EventCallback callback); + virtual void setCallback(eCallback type, EventCallback callback); using Rectangle::contains; bool contains(Vec2 p, bool includeBounds = false) const override; template inline T getThemeValue(const ostd::Stylesheet &theme, const String& key, const T& fallback) diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index db0b40b..9755b67 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -201,6 +201,14 @@ class TestWindow : public Window footer->layoutHint().preferred = { -1, 24 }; m_combo.setSize(200, 30); + m_combo.addMenuItem("Item 1"); + m_combo.addMenuItem("Item 2"); + m_combo.addMenuItem("Item 3"); + m_combo.addMenuItem("Item 4"); + m_combo.addMenuItem("ItemITEMITEMTIEMTIITEM"); + m_combo.setItemCallback([&](const ContextMenu::Entry& entry) -> void { + std::cout << "COMBO: " << entry.text << "\n"; + }); t2.addWidget(*header); // each addWidget() call triggers a relayout t2.addWidget(*body); @@ -448,7 +456,7 @@ i32 main(i32 argc, char** argv) ostd::Random::autoSeed(); ostd::initialize(); TestWindow window; - window.initialize(1200, 800, "OmniaFramework - Test Window"); + window.initialize(1200, 1100, "OmniaFramework - Test Window"); window.setClearColor({ 0, 0, 0 }); window.setPosition({ 50, 50 }); // window.setWindowState(ogfx::WindowCore::eWindowState::Maximized);