Added ComboBox widget
This commit is contained in:
parent
4fb5e24bb9
commit
68bf336530
11 changed files with 219 additions and 56 deletions
|
|
@ -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
|
||||
}
|
||||
% ======================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,61 @@ namespace ogfx
|
|||
using Callback = std::function<void(const Entry&)>;
|
||||
stdvec<Entry> 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 <typename Pred>
|
||||
static bool remove_if(stdvec<Entry>& 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 <typename Pred>
|
||||
static size_t remove_all_if(stdvec<Entry>& 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -82,25 +82,19 @@ namespace ogfx
|
|||
void ToolBar::applyTheme(const ostd::Stylesheet& theme)
|
||||
{
|
||||
auto& win = cast<Window&>(getWindow());
|
||||
setHeight(theme.get<f32>("toolbar.height", getHeight(), {}, {}));
|
||||
setFontSize(theme.get<i32>("toolbar.fontSize", getFontSize(), {}, {}));
|
||||
setBackgroundColor(theme.get<Color>("toolbar.backgroundColor", getBackgroundColor(), {}, {}));
|
||||
setTextColor(theme.get<Color>("toolbar.textColor", getTextColor(), {}, {}));
|
||||
setBorderColor(theme.get<Color>("toolbar.borderColor", getBorderColor(), {}, {}));
|
||||
enableBottomBorder(theme.get<bool>("toolbar.showBorder", isBottomBorderEnabled(), {}, {}));
|
||||
setHeight(getThemeValue<f32>(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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
m_menu.entries.push_back({ text, m_nextItemID++ });
|
||||
if (m_menu.entries.size() == 1)
|
||||
{
|
||||
auto& entry = m_menu.entries.back();
|
||||
m_selectedEntry = entry;
|
||||
m_selectedEntryText = get_display_text(entry.text);
|
||||
}
|
||||
return m_nextItemID - 1;
|
||||
}
|
||||
|
||||
// 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 }
|
||||
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);
|
||||
};
|
||||
gfx.fillTriangle(tri, getTriangleIndicatorColor());
|
||||
}
|
||||
|
||||
void ComboBox::onUpdate(void)
|
||||
void ComboBox::setw(f32 w)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void ComboBox::onMouseReleased(const Event& event)
|
||||
{
|
||||
if (isMouseInside())
|
||||
{
|
||||
setThemeQualifier("active", true);
|
||||
m_dropDownShown = true;
|
||||
}
|
||||
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<f32>(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<Window&>(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<MouseEventData&>(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<MouseEventData&>(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) + "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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<typename T>
|
||||
inline T getThemeValue(const ostd::Stylesheet &theme, const String& key, const T& fallback)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue