Added layouts

This commit is contained in:
OmniaX-Dev 2026-04-28 06:00:33 +02:00
parent a7265febdf
commit 3663ac69fd
33 changed files with 3168 additions and 2269 deletions

View file

@ -87,6 +87,7 @@ list(APPEND OGFX_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/ContextMenu.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/MenuBar.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/ToolBar.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/Layout.cpp
# gui/widgets
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Widget.cpp

View file

@ -23,7 +23,6 @@
***Add Dark Mode Default theme
***FIX: Optimize ListView
***FIX: Limit FPS even when continuous events happen
Add theme caching
Implement cursors in Stylesheet
FIX: Window getting exponentially bigger when Desktop scale changes
FIX: Refreshing scroll when content extent changes
@ -47,6 +46,8 @@ Add Hex Editor widget
Add Message Boxes
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
@ -73,13 +74,13 @@ Implement following widgets:
***MenuBar
***Toolbar
***StatusBar
***Layouts
***Vertical
***Horizontal
***Grid
***Fill
ComboBox
TreeView
Layouts
Vertical
Horizontal
Grid
Fill
Text Input
Text Area
Numeric Field

332
src/ogfx/gui/Layout.cpp Normal file
View file

@ -0,0 +1,332 @@
/*
OmniaFramework - A collection of useful functionality
Copyright (C) 2025 OmniaX-Dev
This file is part of OmniaFramework.
OmniaFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OmniaFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Layout.hpp"
#include "widgets/Widget.hpp"
#include <algorithm>
namespace ogfx
{
namespace gui
{
// ============================================================================
// Layout — shared helpers
// ============================================================================
Vec2 Layout::resolvePreferred(const Widget& child)
{
const auto& h = child.layoutHint();
Vec2 pref;
pref.x = (h.preferred.x < 0) ? child.getw() : h.preferred.x;
pref.y = (h.preferred.y < 0) ? child.geth() : h.preferred.y;
return pref;
}
f32 Layout::applyMinMax(f32 value, f32 minV, f32 maxV)
{
if (value < minV) value = minV;
if (maxV >= 0 && value > maxV) value = maxV;
return value;
}
f32 Layout::alignOffset(eAlign a, f32 available, f32 size)
{
switch (a)
{
case eAlign::Center: return (available - size) * 0.5f;
case eAlign::End: return available - size;
case eAlign::Start:
case eAlign::Stretch: // when stretched the caller already used `available`
default: return 0.0f;
}
}
// Iterate the children we actually care about: visible, valid, not ignored,
// not "ignore-scroll" (scrollbars and friends), and not manually drawn.
// This is a small inline helper to avoid repeating the predicate across
// every layout subclass below.
namespace
{
template<typename F>
void for_each_laid_out(const Widget& parent, F&& fn)
{
auto& mgr = const_cast<Widget&>(parent).getChildren();
for (auto* c : mgr.getWidgets())
{
if (!c) continue;
if (c->isInvalid()) continue;
if (!c->isVisible()) continue;
if (c->isIgnoreScrollEnabled()) continue; // scrollbars etc.
if (c->layoutHint().ignored) continue;
fn(*c);
}
}
}
// ============================================================================
// FillLayout
// ============================================================================
void FillLayout::arrange(Widget& parent)
{
// IMPORTANT: child positions are LOCAL to the parent's content frame.
// getGlobalPosition() automatically adds the parent's padding and
// contentOffset, so we must NOT include them here or they'd be
// applied twice. We only use the SIZE from getContentBounds().
const Vec2 size = parent.getContentBounds().getSize();
for_each_laid_out(parent, [&](Widget& c) {
c.setPosition({ 0, 0 });
c.setSize(size);
c.onBoundsChanged(size);
});
}
Vec2 FillLayout::measure(const Widget& parent) const
{
// Fill wants whatever the parent gives it; report the largest child
// preferred size as a hint for nested measure passes.
Vec2 result { 0, 0 };
for_each_laid_out(parent, [&](Widget& c) {
Vec2 pref = resolvePreferred(c);
result.x = std::max(result.x, pref.x);
result.y = std::max(result.y, pref.y);
});
return result;
}
// ============================================================================
// BoxLayout
// ============================================================================
void BoxLayout::arrange(Widget& parent)
{
const bool horiz = (m_orientation == Orientation::Horizontal);
// IMPORTANT: child positions are LOCAL to the parent's content frame.
// getGlobalPosition() auto-adds parent's padding and contentOffset,
// so we use only the SIZE from getContentBounds() and start child
// positions at (0, 0). Adding area.x/area.y here would double-count.
const Vec2 areaSize = parent.getContentBounds().getSize();
const f32 totalMain = horiz ? areaSize.x : areaSize.y;
const f32 totalCross = horiz ? areaSize.y : areaSize.x;
// --- Pass 1: gather children, classify as "fixed" vs "stretching",
// and accumulate fixed-size + total stretch weight.
struct Slot { Widget* w; f32 main; f32 cross; bool stretches; f32 stretch;
f32 mainMin, mainMax; f32 crossMin, crossMax; };
stdvec<Slot> slots;
slots.reserve(8);
f32 fixedMain = 0.0f;
f32 stretchTotal = 0.0f;
for_each_laid_out(parent, [&](Widget& c) {
const auto& h = c.layoutHint();
const Vec2 pref = resolvePreferred(c);
Slot s;
s.w = &c;
s.main = horiz ? pref.x : pref.y;
s.cross = horiz ? pref.y : pref.x;
s.stretches = (h.stretch > 0.0f);
s.stretch = h.stretch;
s.mainMin = horiz ? h.minSize.x : h.minSize.y;
s.mainMax = horiz ? h.maxSize.x : h.maxSize.y;
s.crossMin = horiz ? h.minSize.y : h.minSize.x;
s.crossMax = horiz ? h.maxSize.y : h.maxSize.x;
if (!s.stretches)
{
s.main = applyMinMax(s.main, s.mainMin, s.mainMax);
fixedMain += s.main;
}
else
{
stretchTotal += s.stretch;
}
slots.push_back(s);
});
if (slots.empty())
return;
const f32 spacingTotal = m_spacing * std::max<f32>(0, (f32)slots.size() - 1);
f32 leftover = std::max(0.0f, totalMain - fixedMain - spacingTotal);
// --- Pass 2: distribute leftover among stretching children.
// We may need multiple rounds: if a stretcher hits its maxSize cap,
// we lock it and redistribute the residual among the rest.
stdvec<bool> locked(slots.size(), false);
for (size_t i = 0; i < slots.size(); ++i)
if (!slots[i].stretches) locked[i] = true;
while (stretchTotal > 0.0f && leftover > 0.0f)
{
bool changed = false;
f32 newLeftover = leftover;
f32 newStretchTotal = stretchTotal;
for (size_t i = 0; i < slots.size(); ++i)
{
if (locked[i]) continue;
f32 share = leftover * (slots[i].stretch / stretchTotal);
f32 capped = applyMinMax(share, slots[i].mainMin, slots[i].mainMax);
if (capped < share) // hit the cap
{
slots[i].main = capped;
newLeftover -= capped;
newStretchTotal -= slots[i].stretch;
locked[i] = true;
changed = true;
}
else
{
slots[i].main = share; // tentative; may be re-revised next round
}
}
if (!changed) break; // no caps hit -> stable
leftover = newLeftover;
stretchTotal = newStretchTotal;
}
// --- Pass 3: place children.
f32 cursor = 0.0f; // local to parent's content frame
for (auto& s : slots)
{
const auto& h = s.w->layoutHint();
// Cross size: stretch fills, otherwise use child preferred (clamped).
f32 crossSize;
if (h.alignCross == eAlign::Stretch)
crossSize = totalCross;
else
crossSize = applyMinMax(s.cross, s.crossMin, s.crossMax);
crossSize = std::min(crossSize, totalCross);
const f32 crossOff = alignOffset(h.alignCross, totalCross, crossSize);
if (horiz)
{
s.w->setPosition(cursor, crossOff);
s.w->setSize(s.main, crossSize);
}
else
{
s.w->setPosition(crossOff, cursor);
s.w->setSize(crossSize, s.main);
}
s.w->onBoundsChanged(s.w->getSize());
cursor += s.main + m_spacing;
}
}
Vec2 BoxLayout::measure(const Widget& parent) const
{
const bool horiz = (m_orientation == Orientation::Horizontal);
f32 mainSum = 0, crossMax = 0;
i32 count = 0;
for_each_laid_out(parent, [&](Widget& c) {
Vec2 pref = resolvePreferred(c);
f32 m = horiz ? pref.x : pref.y;
f32 cr = horiz ? pref.y : pref.x;
mainSum += m;
crossMax = std::max(crossMax, cr);
++count;
});
mainSum += m_spacing * std::max(0, count - 1);
return horiz ? Vec2 { mainSum, crossMax } : Vec2 { crossMax, mainSum };
}
// ============================================================================
// GridLayout
// ============================================================================
void GridLayout::arrange(Widget& parent)
{
if (m_rows == 0 || m_cols == 0) return;
// IMPORTANT: child positions are LOCAL to the parent's content frame.
// getGlobalPosition() auto-adds parent's padding and contentOffset,
// so we use only the SIZE from getContentBounds() here.
const Vec2 areaSize = parent.getContentBounds().getSize();
const f32 cellW = (areaSize.x - m_spacing * (m_cols - 1)) / (f32)m_cols;
const f32 cellH = (areaSize.y - m_spacing * (m_rows - 1)) / (f32)m_rows;
u32 cellIndex = 0;
const u32 maxCells = m_rows * m_cols;
for_each_laid_out(parent, [&](Widget& c) {
if (cellIndex >= maxCells) return; // overflow — drop
const auto& h = c.layoutHint();
const u32 col = cellIndex % m_cols;
const u32 row = cellIndex / m_cols;
const u32 colSpan = std::max<u32>(1, h.colSpan);
const u32 rowSpan = std::max<u32>(1, h.rowSpan);
const f32 slotW = cellW * colSpan + m_spacing * (colSpan - 1);
const f32 slotH = cellH * rowSpan + m_spacing * (rowSpan - 1);
const f32 slotX = col * (cellW + m_spacing); // local to content frame
const f32 slotY = row * (cellH + m_spacing);
// Apply alignment within the cell slot if the child isn't stretching.
Vec2 size { slotW, slotH };
if (h.alignMain != eAlign::Stretch || h.alignCross != eAlign::Stretch)
{
Vec2 pref = resolvePreferred(c);
if (h.alignMain != eAlign::Stretch) size.x = std::min(pref.x, slotW);
if (h.alignCross != eAlign::Stretch) size.y = std::min(pref.y, slotH);
}
const f32 offX = (h.alignMain == eAlign::Stretch) ? 0 : alignOffset(h.alignMain, slotW, size.x);
const f32 offY = (h.alignCross == eAlign::Stretch) ? 0 : alignOffset(h.alignCross, slotH, size.y);
c.setPosition(slotX + offX, slotY + offY);
c.setSize(size);
c.onBoundsChanged(size);
cellIndex += colSpan;
});
}
Vec2 GridLayout::measure(const Widget& parent) const
{
// Find the largest preferred size among children; report grid total.
f32 maxW = 0, maxH = 0;
for_each_laid_out(parent, [&](Widget& c) {
Vec2 pref = resolvePreferred(c);
maxW = std::max(maxW, pref.x);
maxH = std::max(maxH, pref.y);
});
const f32 totalW = maxW * m_cols + m_spacing * (m_cols - 1);
const f32 totalH = maxH * m_rows + m_spacing * (m_rows - 1);
return { totalW, totalH };
}
}
}

145
src/ogfx/gui/Layout.hpp Normal file
View file

@ -0,0 +1,145 @@
/*
OmniaFramework - A collection of useful functionality
Copyright (C) 2025 OmniaX-Dev
This file is part of OmniaFramework.
OmniaFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OmniaFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <ogfx/gui/LayoutHint.hpp>
#include <ostd/math/Geometry.hpp>
#include <ostd/utils/Defines.hpp>
namespace ogfx
{
namespace gui
{
class Widget;
// ============================================================================
// Layout — abstract base.
//
// A Layout is owned by a Widget (the "container") and is responsible for
// positioning and sizing that widget's children inside the container's
// content bounds (i.e. inside padding, after content offset).
//
// Two responsibilities:
// - arrange(parent): set position + size on every laid-out child.
// - measure(parent): report how much space this layout *wants* given
// its current children. Used so a Layout can drive
// a parent's contentExtents (and therefore scrollbars),
// and so a parent Layout can ask a child container
// "how big would you like to be?".
// ============================================================================
class Layout
{
public:
virtual ~Layout(void) = default;
// Place children. Called from Widget::relayout().
virtual void arrange(Widget& parent) = 0;
// Compute desired size of this layout's content. Should NOT mutate
// children. Used by Widget::getContentExtents() override below and
// by parent layouts asking for preferred size.
virtual Vec2 measure(const Widget& parent) const = 0;
// Spacing between adjacent items along the main axis (Box) or
// between cells (Grid). FillLayout ignores it.
inline f32 getSpacing(void) const { return m_spacing; }
inline void setSpacing(f32 s) { m_spacing = s; }
protected:
// Helpers used by concrete layouts to honor a child's hint
// without each subclass re-implementing the same min/max/preferred
// resolution logic.
static Vec2 resolvePreferred(const Widget& child);
static f32 applyMinMax(f32 value, f32 minV, f32 maxV);
static f32 alignOffset(eAlign a, f32 available, f32 size);
f32 m_spacing { 0.0f };
};
// ============================================================================
// FillLayout — every laid-out child is sized to fill the parent's content
// area. Useful as a default (a "single slot" container) and as a building
// block for things like SplitView panels.
// ============================================================================
class FillLayout : public Layout
{
public:
void arrange(Widget& parent) override;
Vec2 measure(const Widget& parent) const override;
};
// ============================================================================
// BoxLayout — arrange children in a single row (Horizontal) or column
// (Vertical) with stretch factors and spacing.
//
// Algorithm (per axis = "main"):
// 1. Sum preferred sizes of non-stretching children + spacing.
// 2. The remainder is distributed among stretching children proportional
// to their stretch factor.
// 3. min/max are clamped per child; if a stretching child hits its max,
// the leftover is redistributed among the rest in a second pass.
//
// Cross axis: each child either fills (Stretch, default) or uses its
// preferred size aligned per alignCross.
// ============================================================================
class BoxLayout : public Layout
{
public:
enum class Orientation { Horizontal, Vertical };
explicit BoxLayout(Orientation orientation) : m_orientation(orientation) {}
void arrange(Widget& parent) override;
Vec2 measure(const Widget& parent) const override;
inline Orientation getOrientation(void) const { return m_orientation; }
inline void setOrientation(Orientation o) { m_orientation = o; }
private:
Orientation m_orientation;
};
// ============================================================================
// GridLayout — fixed rows × cols. Cell sizes are uniform (rows = h/N,
// cols = w/M). Children are placed in row-major insertion order.
// LayoutHint::colSpan / rowSpan let a child cover multiple cells.
// ============================================================================
class GridLayout : public Layout
{
public:
GridLayout(u32 rows, u32 cols) : m_rows(rows), m_cols(cols) {}
void arrange(Widget& parent) override;
Vec2 measure(const Widget& parent) const override;
inline u32 getRows(void) const { return m_rows; }
inline u32 getCols(void) const { return m_cols; }
inline void setDimensions(u32 rows, u32 cols) { m_rows = rows; m_cols = cols; }
private:
u32 m_rows;
u32 m_cols;
};
}
}

View file

@ -0,0 +1,80 @@
/*
OmniaFramework - A collection of useful functionality
Copyright (C) 2025 OmniaX-Dev
This file is part of OmniaFramework.
OmniaFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OmniaFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <ostd/math/Geometry.hpp>
#include <ostd/utils/Defines.hpp>
namespace ogfx
{
namespace gui
{
// How a child wants to align itself within the slot a Layout gives it.
enum class eAlign : u8
{
Start, // top / left
Center, // center along the relevant axis
End, // bottom / right
Stretch // fill the available space along that axis
};
// Per-child metadata read by the parent's Layout when it arranges children.
// All fields have sane defaults so unset hints behave like "use my current size,
// no stretching, top-left, span 1".
struct LayoutHint
{
// 0 = use the widget's current/preferred size on the main axis.
// > 0 = take a share of leftover space proportional to this value.
// e.g. two children with stretch=1.0 split leftover space 50/50;
// stretch=2 vs stretch=1 splits it 2/3 vs 1/3.
f32 stretch { 0.0f };
// Alignment along the layout's main axis (only meaningful when the child
// is smaller than the slot the layout assigned it — i.e. when stretch=0
// and the slot ended up larger than preferred). Mostly relevant for Grid
// and for FillLayout in single-child mode.
eAlign alignMain { eAlign::Start };
// Alignment perpendicular to the main axis. Stretch (default) makes the
// child fill the cross dimension of its slot — usually what you want.
eAlign alignCross { eAlign::Stretch };
// Hard limits the layout will respect. minSize is honored even if it
// breaks stretch ratios; maxSize component < 0 means "unbounded".
Vec2 minSize { 0, 0 };
Vec2 maxSize { -1, -1 };
// If a component is < 0, the layout uses the widget's current size
// (getw()/geth()) for that axis. Otherwise this overrides it.
Vec2 preferred { -1, -1 };
// Grid-only. How many cells the child occupies. Ignored by Box/Fill.
u8 colSpan { 1 };
u8 rowSpan { 1 };
// If true, the layout skips this child entirely. Useful for floating
// overlays, drag-ghosts, or scrollbars (which manage their own bounds).
// Note: ScrollableWidget already excludes its scrollbars via the
// IgnoreScroll flag, but this gives finer control for other cases.
bool ignored { false };
};
}
}

View file

@ -35,7 +35,7 @@ namespace ogfx
{
setRootChild();
setStylesheetCategoryName("toolbar");
setTypeName("ogfx::gui::widgets::ToolBar");
setTypeName("ogfx::gui::ToolBar");
auto& win = cast<Window&>(getWindow());
w = win.getWindowWidth();
h = m_height;
@ -51,19 +51,18 @@ namespace ogfx
return *this;
}
widgets::Button& ToolBar::addButton(const String& iconPath, const String& text, EventCallback callback)
Button& ToolBar::addButton(const String& iconPath, const String& text, EventCallback callback)
{
static f32 pad = 0;
m_buttons.push_back({ getWindow() });
auto& btn = m_buttons.back();
btn.addThemeID("tool_button");
btn.setIcon(iconPath);
btn.setIconSize({ m_height, m_height });
btn.setText(text);
btn.setSizeMode(eSizeMode::LayoutManaged);
btn.setCallback(ogfx::gui::Widget::eCallback::ActionPerformed, callback);
btn.enableIconOnly();
addWidget(btn, { m_height * (m_buttons.size() - 1) + pad, 0 });
pad += 15;
btn.layoutHint().preferred = { m_height, m_height };
addWidget(btn);
return btn;
}

View file

@ -34,7 +34,7 @@ namespace ogfx
public:
ToolBar(Window& window, bool statusbar = false);
ToolBar& create(bool statusbar = false);
widgets::Button& addButton(const String& iconPath, const String& text = "", EventCallback callback = nullptr);
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;
@ -59,7 +59,7 @@ namespace ogfx
private:
bool m_disableButtonText { true };
std::deque<widgets::Button> m_buttons;
std::deque<Button> m_buttons;
bool m_isStatusBar { false };
f32 m_height { 26 };

View file

@ -273,6 +273,7 @@ namespace ogfx
void addWidget(Widget& widget, const Vec2& position = { 0, 0 });
void setTheme(const ostd::Stylesheet& theme) override;
inline void showContextMenu(const Vec2& pos) { m_cmenu.show(pos); }
inline void showContextMenu(const ContextMenu::Instance& instance, const Vec2& pos) { setContextMenu(instance); showContextMenu(pos); }
inline void setContextMenu(const ContextMenu::Instance& instance) { m_cmenu.setInstance(instance); }
inline bool isContextMenuVisible(void) const { return m_cmenu.isVisible(); }
inline bool isMenuBarVisible(void) const { return m_menubar.isVisible(); }
@ -306,7 +307,7 @@ namespace ogfx
void __main_loop_cycle(void);
protected:
widgets::RootWidget m_rootWidget { *this };
RootWidget m_rootWidget { *this };
ostd::StepTimer m_fixedUpdateTimer;
ostd::StepTimer::TimePoint m_lastFrameTime;
ostd::StepTimer m_mainLoopTimer;
@ -315,7 +316,7 @@ namespace ogfx
ToolBar m_toolbar { *this };
ToolBar m_statusbar { *this, true };
friend class widgets::RootWidget;
friend class RootWidget;
};
}
}

View file

@ -22,236 +22,314 @@
#include "../../render/BasicRenderer.hpp"
#include "../../../ostd/io/FileSystem.hpp"
#include "../Window.hpp"
#include <algorithm>
namespace ogfx
{
namespace gui
{
namespace widgets
Button& Button::create(const String& text)
{
Button& Button::create(const String& text)
{
m_text = text;
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::Button");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("button");
validate();
invalidate_layout();
return *this;
}
void Button::onDraw(ogfx::BasicRenderer2D& gfx)
{
if (m_layoutDirty)
recompute_layout();
const Vec2 contentTL = getGlobalContentPosition();
// --- Icon ---
if (isIconEnabled() && m_drawIconSize.x > 0 && m_drawIconSize.y > 0)
{
const Vec2 iconPos = contentTL + m_drawIconOffset;
if (isAnimatedEnabled())
gfx.drawAnimation(m_anim, iconPos, m_drawIconSize, getIconTintColor());
else
gfx.drawImage(m_icon, iconPos, m_drawIconSize, getIconTintColor());
}
// --- Text ---
if (!isIconOnlyEnabled() && m_text.len() > 0)
{
gfx.drawString(
m_text,
contentTL + m_drawTextOffset,
getTextColor(),
getFontSize(),
m_contentScale
);
}
}
void Button::onUpdate(void)
m_text = text;
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::Button");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("button");
validate();
invalidate_layout();
return *this;
}
// ===================================================================
// Mode handling
// ===================================================================
void Button::setSizeMode(eSizeMode mode)
{
m_sizeMode = mode;
m_modeExplicitlySet = true;
invalidate_layout();
}
eSizeMode Button::effectiveSizeMode(void) const
{
// Auto-detect: if user never set a mode explicitly and our parent
// owns a layout, we run as LayoutManaged. Otherwise honor the
// stored mode (AutoSize by default).
if (!m_modeExplicitlySet && getParent() && getParent()->hasLayout())
return eSizeMode::LayoutManaged;
return m_sizeMode;
}
// ===================================================================
// Lifecycle hooks
// ===================================================================
void Button::onBoundsChanged(const Vec2& /*newSize*/)
{
// Layouts call this after they assign new bounds. Recompute the
// content placement so it stays centered inside the new size.
invalidate_layout();
}
void Button::onMouseReleased(const Event& event)
{
if (!isMouseInside())
return;
if (event.mouse->button == MouseEventData::eButton::Left && callback_onActionPerformed)
callback_onActionPerformed(event);
}
void Button::onUpdate(void)
{
if (isAnimatedEnabled())
m_anim.update();
}
void Button::onDraw(ogfx::BasicRenderer2D& gfx)
{
if (m_layoutDirty)
recompute_layout();
const Vec2 contentTL = getGlobalContentPosition();
// --- Icon ---
if (isIconEnabled() && m_drawIconSize.x > 0 && m_drawIconSize.y > 0)
{
const Vec2 iconPos = contentTL + m_drawIconOffset;
if (isAnimatedEnabled())
m_anim.update();
gfx.drawAnimation(m_anim, iconPos, m_drawIconSize, getIconTintColor());
else
gfx.drawImage(m_icon, iconPos, m_drawIconSize, getIconTintColor());
}
void Button::onMouseReleased(const Event& event)
// --- Text ---
if (!isIconOnlyEnabled() && m_text.len() > 0)
{
if (!isMouseInside())
return;
if (event.mouse->button == MouseEventData::eButton::Left && callback_onActionPerformed)
callback_onActionPerformed(event);
gfx.drawString(
m_text,
contentTL + m_drawTextOffset,
getTextColor(),
getFontSize()
);
}
}
void Button::setText(const String& text)
// ===================================================================
// Setters that trigger a recompute
// ===================================================================
void Button::setText(const String& text)
{
m_text = text;
invalidate_layout();
}
void Button::setIcon(const String& filePath)
{
disableIcon();
if (filePath == "") { invalidate_layout(); return; }
if (!ostd::FileSystem::fileExists(filePath)) { invalidate_layout(); return; }
m_icon.destroy();
m_icon.loadFromFile(filePath, getWindow().getGFX());
enableIcon();
invalidate_layout();
}
void Button::applyTheme(const ostd::Stylesheet& theme)
{
enableIcon(getThemeValue<bool>(theme, "showIcon", m_showIcon));
enableIconOnly(getThemeValue<bool>(theme, "iconOnly", m_iconOnly));
enableAnimated(getThemeValue<bool>(theme, "icon.animated", m_animated));
setAnimationData(getThemeValue<AnimationData>(theme, "icon.animation", m_animData));
setIconTintColor(getThemeValue<Color>(theme, "icon.tint", getIconTintColor()));
String filePath = getThemeValue<String>(theme, "icon.path", m_icon.getFilePath());
if (filePath != m_icon.getFilePath())
setIcon(filePath);
setIconSize(getThemeValue<Vec2>(theme, "icon.size", getIconSize()));
setIconSpacing(getThemeValue<f32>(theme, "icon.spacing", m_iconSpacing));
if (isAnimatedEnabled())
{
m_text = text;
invalidate_layout();
m_anim.create(m_animData);
m_anim.setSpriteSheet(m_icon);
}
invalidate_layout();
}
void Button::setIcon(const String& filePath)
// ===================================================================
// Icon size helpers
// ===================================================================
Vec2 Button::icon_floor(void) const
{
// Configurable hint floor: if the user set m_iconSize, use it as the
// minimum icon dimensions. Otherwise fall back to font size square.
const f32 fontEdge = (f32)getFontSize();
if (m_iconSize.x > 0 && m_iconSize.y > 0)
return m_iconSize;
if (m_iconSize.x > 0)
return { m_iconSize.x, m_iconSize.x };
if (m_iconSize.y > 0)
return { m_iconSize.y, m_iconSize.y };
return { fontEdge, fontEdge };
}
Vec2 Button::compute_icon_size(f32 textHeightHint) const
{
if (!isIconEnabled())
return { 0, 0 };
const Vec2 floor = icon_floor();
// Target height: max(text height, floor.y). Width derives from
// the icon's natural aspect ratio if the icon is loaded; otherwise
// match the floor.
f32 targetH = std::max(textHeightHint, floor.y);
if (m_icon.isLoaded())
{
disableIcon();
if (filePath == "") { invalidate_layout(); return; }
if (!ostd::FileSystem::fileExists(filePath)) { invalidate_layout(); return; }
m_icon.destroy();
m_icon.loadFromFile(filePath, getWindow().getGFX());
enableIcon();
invalidate_layout();
}
void Button::applyTheme(const ostd::Stylesheet& theme)
{
enableIcon(getThemeValue<bool>(theme, "showIcon", m_showIcon));
enableIconOnly(getThemeValue<bool>(theme, "iconOnly", m_iconOnly));
enableAutoSize(getThemeValue<bool>(theme, "autoSize", m_autoSize));
enableAnimated(getThemeValue<bool>(theme, "icon.animated", m_animated));
setAnimationData(getThemeValue<AnimationData>(theme, "icon.animation", m_animData));
setIconTintColor(getThemeValue<Color>(theme, "icon.tint", getIconTintColor()));
String filePath = getThemeValue<String>(theme, "icon.path", m_icon.getFilePath());
if (filePath != m_icon.getFilePath())
setIcon(filePath);
setIconSize(getThemeValue<Vec2>(theme, "icon.size", getIconSize()));
setIconSpacing(getThemeValue<f32>(theme, "icon.spacing", m_iconSpacing));
if (isAnimatedEnabled())
// When the icon is animated, the "natural size" is one frame,
// NOT the whole sprite sheet — the sheet's aspect ratio is
// meaningless (it depends on column/row count). Same logic
// as ImageLabel::compute_draw_geometry.
Vec2 nat;
if (m_animated)
{
m_anim.create(m_animData);
m_anim.setSpriteSheet(m_icon);
}
invalidate_layout();
}
Vec2 Button::native_icon_size(void) const
{
if (!isIconEnabled())
return { 0, 0 };
// Treat font size as the natural icon edge if the user didn't pick one.
const f32 fontEdge = cast<f32>(getFontSize());
Vec2 hint = m_iconSize;
if (hint.x <= 0 && hint.y <= 0)
{
if (m_icon.isLoaded())
{
// Fit the icon's natural aspect ratio into a font-sized square.
Vec2 nat = m_icon.getSize();
if (nat.x > 0 && nat.y > 0)
{
if (nat.x >= nat.y)
return nat.propx(fontEdge);
return nat.propy(fontEdge);
}
}
return { fontEdge, fontEdge };
}
// One axis specified: derive the other from the icon's aspect (if any),
// otherwise make it square.
if (hint.x <= 0)
{
if (m_icon.isLoaded())
{
Vec2 nat = m_icon.getSize();
if (nat.x > 0 && nat.y > 0)
return nat.propy(hint.y);
}
return { hint.y, hint.y };
}
if (hint.y <= 0)
{
if (m_icon.isLoaded())
{
Vec2 nat = m_icon.getSize();
if (nat.x > 0 && nat.y > 0)
return nat.propx(hint.x);
}
return { hint.x, hint.x };
}
return hint;
}
void Button::recompute_layout(void)
{
m_layoutDirty = false;
auto& gfx = getWindow().getGFX();
const Rectangle pad = getPadding();
const f32 padW = pad.left() + pad.right();
const f32 padH = pad.top() + pad.bottom();
// 1) Native (un-scaled) sizes of the pieces.
const Vec2 iconNative = native_icon_size();
const bool hasIcon = isIconEnabled() && (iconNative.x > 0 && iconNative.y > 0);
const bool hasText = !isIconOnlyEnabled() && m_text.len() > 0;
const Vec2 textNative = hasText
? gfx.getStringDimensions(m_text, getFontSize())
: Vec2 { 0, 0 };
// 2) Native content extents (icon + spacing + text), no padding yet.
f32 nativeW = 0;
f32 nativeH = 0;
if (hasIcon)
{
nativeW += iconNative.x;
nativeH = std::max(nativeH, iconNative.y);
}
if (hasText)
{
if (hasIcon) nativeW += m_iconSpacing;
nativeW += textNative.x;
nativeH = std::max(nativeH, textNative.y);
}
if (nativeW <= 0) nativeW = cast<f32>(getFontSize()); // degenerate fallback
if (nativeH <= 0) nativeH = cast<f32>(getFontSize());
// 3) Decide the widget size and the content scale.
if (isAutoSizeEnabled())
{
m_contentScale = 1.0f;
setSize({ nativeW + padW, nativeH + padH });
const auto frame = m_anim.getFrameRect();
nat = { frame.w, frame.h };
}
else
{
// Honor the size the user already set on the widget.
nat = m_icon.getSize();
}
if (nat.x > 0 && nat.y > 0)
{
const f32 aspect = nat.x / nat.y;
return { targetH * aspect, targetH };
}
}
// No natural size known — fall back to whichever is wider.
return { std::max(floor.x, targetH), targetH };
}
// ===================================================================
// Layout recompute
//
// Branches by effective mode:
//
// AutoSize:
// 1. Measure text at native font size.
// 2. Compute icon size: height = max(textH, floorH), aspect preserved.
// 3. Set widget size to fit padding + icon + spacing + text.
// 4. Place icon and text inside the content area.
//
// Manual / LayoutManaged:
// 1. Widget size is whatever it currently is (user / layout decided).
// 2. Measure text at native font size.
// 3. Icon size = m_iconSize hint (or font-size square if unset),
// aspect preserved against icon natural size.
// 4. Center the text+icon group inside the content area. Overflow
// is allowed and clipped at widget level (positions can go
// slightly outside if the user picked sizes that don't fit).
// ===================================================================
void Button::recompute_layout(void)
{
m_layoutDirty = false;
auto& gfx = getWindow().getGFX();
const Rectangle pad = getPadding();
const f32 padW = pad.left() + pad.right();
const f32 padH = pad.top() + pad.bottom();
// 1) Text dimensions (native font size in all modes).
const bool hasText = !isIconOnlyEnabled() && m_text.len() > 0;
const Vec2 textSize = hasText
? gfx.getStringDimensions(m_text, getFontSize())
: Vec2 { 0, 0 };
// 2) Icon dimensions.
const eSizeMode mode = effectiveSizeMode();
Vec2 iconSize { 0, 0 };
if (isIconEnabled())
{
if (mode == eSizeMode::AutoSize)
{
// Floor from m_iconSize hint (or font size); rise to text height.
iconSize = compute_icon_size(textSize.y);
}
else
{
// Manual / LayoutManaged: just use the configured hint, no
// floor-against-text logic. If unset, use font size square.
iconSize = compute_icon_size(0.0f);
// Cap the icon to the content area so padding is always
// respected. Aspect ratio is preserved during the cap.
// AutoSize doesn't need this because it grows the widget
// to accommodate the icon + padding.
const f32 availW = std::max(0.0f, getw() - padW);
const f32 availH = std::max(0.0f, geth() - padH);
if (availW <= 0 || availH <= 0)
m_contentScale = 0.0f;
else
if (iconSize.x > 0 && iconSize.y > 0
&& (iconSize.x > availW || iconSize.y > availH))
{
const f32 sx = availW / nativeW;
const f32 sy = availH / nativeH;
m_contentScale = std::min(sx, sy);
const f32 sx = (iconSize.x > 0) ? availW / iconSize.x : 1.0f;
const f32 sy = (iconSize.y > 0) ? availH / iconSize.y : 1.0f;
const f32 s = std::min(sx, sy);
iconSize = { iconSize.x * s, iconSize.y * s };
}
}
// 4) Compute scaled draw sizes and centered offsets within the content area.
const Vec2 contentSize = getContentBounds().getSize();
const Vec2 iconScaled = hasIcon ? iconNative * m_contentScale : Vec2 { 0, 0 };
const Vec2 textScaled = hasText ? textNative * m_contentScale : Vec2 { 0, 0 };
const f32 spacing = (hasIcon && hasText) ? (m_iconSpacing * m_contentScale) : 0.0f;
const f32 totalW = iconScaled.x + spacing + textScaled.x;
const f32 startX = std::max(0.0f, (contentSize.x - totalW) * 0.5f);
m_drawIconSize = iconScaled;
if (hasIcon)
m_drawIconOffset = { startX, std::max(0.0f, (contentSize.y - iconScaled.y) * 0.5f) };
else
m_drawIconOffset = { 0, 0 };
if (hasText)
m_drawTextOffset = {
startX + iconScaled.x + spacing,
std::max(0.0f, (contentSize.y - textScaled.y) * 0.5f)
};
else
m_drawTextOffset = { 0, 0 };
}
const bool hasIcon = iconSize.x > 0 && iconSize.y > 0;
// 3) Native content extents (icon + spacing + text), no padding yet.
f32 contentNativeW = 0;
f32 contentNativeH = 0;
if (hasIcon)
{
contentNativeW += iconSize.x;
contentNativeH = std::max(contentNativeH, iconSize.y);
}
if (hasText)
{
if (hasIcon) contentNativeW += m_iconSpacing;
contentNativeW += textSize.x;
contentNativeH = std::max(contentNativeH, textSize.y);
}
// 4) Decide widget size (AutoSize only) or honor the existing one.
if (mode == eSizeMode::AutoSize)
{
const f32 fallback = (f32)getFontSize();
const f32 wantW = (contentNativeW > 0 ? contentNativeW : fallback) + padW;
const f32 wantH = (contentNativeH > 0 ? contentNativeH : fallback) + padH;
// Only call setSize if the size actually changed — avoids the
// hover-flicker cycle (theme reload -> applyTheme ->
// invalidate_layout -> recompute -> setSize -> hover boundary
// crosses -> ... ).
if (std::abs(getw() - wantW) > 0.5f || std::abs(geth() - wantH) > 0.5f)
setSize({ wantW, wantH });
}
// else: Manual & LayoutManaged keep current widget size.
// 5) Place icon and text inside the content area, centered.
const Vec2 contentSize = getContentBounds().getSize();
const f32 spacing = (hasIcon && hasText) ? m_iconSpacing : 0.0f;
const f32 totalW = (hasIcon ? iconSize.x : 0) + spacing + (hasText ? textSize.x : 0);
const f32 startX = (contentSize.x - totalW) * 0.5f;
m_drawIconSize = iconSize;
if (hasIcon)
m_drawIconOffset = { startX, (contentSize.y - iconSize.y) * 0.5f };
else
m_drawIconOffset = { 0, 0 };
if (hasText)
m_drawTextOffset = {
startX + (hasIcon ? iconSize.x + spacing : 0.0f),
(contentSize.y - textSize.y) * 0.5f
};
else
m_drawTextOffset = { 0, 0 };
}
}
}

View file

@ -28,79 +28,115 @@ namespace ogfx
{
namespace gui
{
namespace widgets
// ===========================================================================
// eSizeMode — shared across Button / Label / (and analogous on ImageLabel).
//
// AutoSize: widget computes its own size to fit content. Used when
// there's no layout managing it, and the user wants the
// widget to "just be the right size".
//
// Manual: user controls the widget's size; user also controls the
// content sizes (text/icon). Nothing is computed automatically.
// Content is drawn at its user-specified size, centered, and
// clipped at the widget edge if it doesn't fit.
//
// LayoutManaged: parent's Layout controls the widget's size. Content
// (text/icon) is at its user-specified size, and the widget
// honors whatever bounds the layout assigns.
// This mode is auto-selected when added to a parent that
// has a layout — unless the user explicitly set Manual.
// ===========================================================================
enum class eSizeMode : u8
{
class Button : public Widget
{
public:
inline Button(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
inline Button(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
Button& create(const String& text);
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onUpdate(void) override;
void onMouseReleased(const Event& event) override;
void setText(const String& text);
void setIcon(const String& filePath);
void applyTheme(const ostd::Stylesheet& theme) override;
inline String getText(void) const { return m_text; }
inline Image& getIcon(void) { return m_icon; }
AutoSize,
Manual,
LayoutManaged
};
// When auto-size is enabled, the widget resizes itself to fit text+icon+padding
// at native font/icon size (text is drawn at scale 1.0).
// When auto-size is disabled, the widget keeps its user-defined size, and the
// content is scaled uniformly down/up to fit that size (text uses the renderer's
// scale parameter; icon is scaled proportionally).
OSTD_BOOL_PARAM_GETSET_E(AutoSize, m_autoSize);
class Button : public Widget
{
public:
inline Button(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
inline Button(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
Button& create(const String& text);
// Icon-only mode: text is ignored when laying out and drawing.
// Combined with auto-size, the button becomes a square based on font size.
// Combined with manual size, the icon scales to fit the content area.
OSTD_BOOL_PARAM_GETSET_E(IconOnly, m_iconOnly);
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onUpdate(void) override;
void onMouseReleased(const Event& event) override;
void onBoundsChanged(const Vec2& newSize) override;
void applyTheme(const ostd::Stylesheet& theme) override;
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon);
OSTD_PARAM_GETSET(ostd::AnimationData, AnimationData, m_animData);
OSTD_PARAM_GETSET(Vec2, IconSize, m_iconSize);
OSTD_PARAM_GETSET(f32, IconSpacing, m_iconSpacing);
OSTD_PARAM_GETSET(Color, IconTintColor, m_iconTint);
void setText(const String& text);
void setIcon(const String& filePath);
inline String getText(void) const { return m_text; }
inline Image& getIcon(void) { return m_icon; }
private:
// Force a layout recompute on the next draw.
inline void invalidate_layout(void) { m_layoutDirty = true; }
// =========================== SIZE MODE ============================
// Set the widget's size mode explicitly. After calling this, the
// auto-detection (LayoutManaged when parent has a layout) is
// disabled — the user's choice is respected.
void setSizeMode(eSizeMode mode);
inline eSizeMode getSizeMode(void) const { return m_sizeMode; }
// Recomputes layout. In auto-size mode this also resizes the widget.
// In manual-size mode this computes the scale factor that fits content
// into the current widget size.
void recompute_layout(void);
// The mode actually used by the layout/draw code. Resolves auto-
// detection: if the user hasn't explicitly set a mode and the
// parent has a layout, this returns LayoutManaged.
eSizeMode effectiveSizeMode(void) const;
// ==================================================================
// Returns the icon size at native (un-scaled) resolution. If m_iconSize is
// (0,0) and an icon is loaded, falls back to the icon's natural size; if no
// icon is loaded, falls back to font-size-based square. Aspect ratio of
// m_iconSize is preserved.
Vec2 native_icon_size(void) const;
OSTD_BOOL_PARAM_GETSET_E(IconOnly, m_iconOnly);
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon);
OSTD_PARAM_GETSET(ostd::AnimationData, AnimationData, m_animData);
OSTD_PARAM_GETSET(Vec2, IconSize, m_iconSize);
OSTD_PARAM_GETSET(f32, IconSpacing, m_iconSpacing);
OSTD_PARAM_GETSET(Color, IconTintColor, m_iconTint);
private:
String m_text { "" };
Image m_icon;
ostd::AnimationData m_animData;
Animation m_anim;
Vec2 m_iconSize { 0, 0 };
f32 m_iconSpacing { 10 };
Color m_iconTint { Colors::White };
private:
inline void invalidate_layout(void) { m_layoutDirty = true; }
// Computed layout state (refreshed by recompute_layout).
bool m_layoutDirty { true };
f32 m_contentScale { 1.0f }; // applied to text & icon when !m_autoSize
Vec2 m_drawIconSize { 0, 0 }; // final on-screen icon size
Vec2 m_drawIconOffset { 0, 0 }; // top-left of the icon inside the content area
Vec2 m_drawTextOffset { 0, 0 }; // top-left of the text inside the content area
// Re-runs the size + content computation. What this does depends
// entirely on effectiveSizeMode() — see the cpp for details.
void recompute_layout(void);
// Flags
bool m_autoSize { true };
bool m_iconOnly { false };
bool m_showIcon { false };
bool m_animated { false };
};
}
// Returns the icon size to draw, given the current effective text
// height. In AutoSize: max(text-height, hintFloor) with aspect
// preserved. In Manual/LayoutManaged: the user's m_iconSize hint
// (or fontSize fallback). Aspect ratio is always preserved when
// the icon is loaded with a known natural size.
Vec2 compute_icon_size(f32 textHeightHint) const;
// Native floor for the icon: m_iconSize if set, else font size.
// (User-configurable hint as discussed; the icon will not be
// drawn smaller than this in AutoSize mode.)
Vec2 icon_floor(void) const;
private:
String m_text { "" };
Image m_icon;
ostd::AnimationData m_animData;
Animation m_anim;
Vec2 m_iconSize { 0, 0 };
f32 m_iconSpacing { 10 };
Color m_iconTint { Colors::White };
// ===== Mode =====
eSizeMode m_sizeMode { eSizeMode::AutoSize };
bool m_modeExplicitlySet { false };
// ================
// ===== Computed draw state (refreshed by recompute_layout) =====
bool m_layoutDirty { true };
Vec2 m_drawIconSize { 0, 0 };
Vec2 m_drawIconOffset { 0, 0 };
Vec2 m_drawTextOffset { 0, 0 };
// ===============================================================
// ===== Flags =====
bool m_iconOnly { false };
bool m_showIcon { false };
bool m_animated { false };
// =================
};
}
}

View file

@ -25,73 +25,70 @@ namespace ogfx
{
namespace gui
{
namespace widgets
CheckBox& CheckBox::create(const String& text)
{
CheckBox& CheckBox::create(const String& text)
{
setText(text);
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::CheckBox");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("checkbox");
validate();
return *this;
}
setText(text);
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::CheckBox");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("checkbox");
validate();
return *this;
}
void CheckBox::applyTheme(const ostd::Stylesheet& theme)
{
setCheckBorderColor(getThemeValue<Color>(theme, "checkBorderColor", Colors::White));
setCheckBoxColor(getThemeValue<Color>(theme, "checkBoxColor", Colors::White));
m_checkBorderRadius = getThemeValue<i32>(theme, "checkBorderRadius", 5);
m_checkBorderWidth = getThemeValue<i32>(theme, "checkBorderWidth", 1);
}
void CheckBox::applyTheme(const ostd::Stylesheet& theme)
{
setCheckBorderColor(getThemeValue<Color>(theme, "checkBorderColor", Colors::White));
setCheckBoxColor(getThemeValue<Color>(theme, "checkBoxColor", Colors::White));
m_checkBorderRadius = getThemeValue<i32>(theme, "checkBorderRadius", 5);
m_checkBorderWidth = getThemeValue<i32>(theme, "checkBorderWidth", 1);
}
void CheckBox::onDraw(ogfx::BasicRenderer2D& gfx)
{
if (m_textChanged)
__update_size(gfx);
gfx.drawRoundRect({ getGlobalContentPosition(), m_checkSize }, getCheckBorderColor(), m_checkBorderRadius, m_checkBorderWidth);
if (isChecked())
gfx.fillRoundRect({ getGlobalContentPosition() + 5, m_checkSize - 10 }, getCheckBoxColor(), m_checkBorderRadius / 2.0f);
gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_checkSize.x + m_spacing, 0 }, getTextColor(), getFontSize());
}
void CheckBox::onDraw(ogfx::BasicRenderer2D& gfx)
{
if (m_textChanged)
__update_size(gfx);
gfx.drawRoundRect({ getGlobalContentPosition(), m_checkSize }, getCheckBorderColor(), m_checkBorderRadius, m_checkBorderWidth);
if (isChecked())
gfx.fillRoundRect({ getGlobalContentPosition() + 5, m_checkSize - 10 }, getCheckBoxColor(), m_checkBorderRadius / 2.0f);
gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_checkSize.x + m_spacing, 0 }, getTextColor(), getFontSize());
}
void CheckBox::onMouseReleased(const Event& event)
{
if (!isMouseInside())
return;
setChecked(!isChecked());
}
void CheckBox::onMouseReleased(const Event& event)
{
if (!isMouseInside())
return;
setChecked(!isChecked());
}
void CheckBox::setText(const String& text)
{
m_text = text;
m_textChanged = true;
}
void CheckBox::setText(const String& text)
{
m_text = text;
m_textChanged = true;
}
void CheckBox::setChecked(bool checked)
{
if (m_checked == checked)
return;
m_checked = checked;
setThemeQualifier("active", m_checked);
if (callback_onStateChanged)
callback_onStateChanged(*this, m_checked);
}
void CheckBox::setChecked(bool checked)
{
if (m_checked == checked)
return;
m_checked = checked;
setThemeQualifier("active", m_checked);
if (callback_onStateChanged)
callback_onStateChanged(*this, m_checked);
}
void CheckBox::__update_size(ogfx::BasicRenderer2D& gfx)
{
auto size = gfx.getStringDimensions(getText(), getFontSize());
m_checkSize = { cast<f32>(size.y), cast<f32>(size.y) };
size.x += m_spacing + m_checkSize.x;
size.x += getPadding().left();
size.x += getPadding().right();
size.y += getPadding().top();
size.y += getPadding().bottom();
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
m_textChanged = false;
}
void CheckBox::__update_size(ogfx::BasicRenderer2D& gfx)
{
auto size = gfx.getStringDimensions(getText(), getFontSize());
m_checkSize = { cast<f32>(size.y), cast<f32>(size.y) };
size.x += m_spacing + m_checkSize.x;
size.x += getPadding().left();
size.x += getPadding().right();
size.y += getPadding().top();
size.y += getPadding().bottom();
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
m_textChanged = false;
}
}
}

View file

@ -27,44 +27,40 @@ namespace ogfx
{
namespace gui
{
namespace widgets
class CheckBox : public Widget
{
public:
inline CheckBox(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
inline CheckBox(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
CheckBox& create(const String& text);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseReleased(const Event& event) override;
void setText(const String& text);
void setChecked(bool checked);
inline String getText(void) const { return m_text; }
inline Color getCheckBorderColor(void) const { return m_checkBorderColor; }
inline void setCheckBorderColor(const Color& color) { m_checkBorderColor = color; }
inline Color getCheckBoxColor(void) const { return m_checkBoxColor; }
inline void setCheckBoxColor(const Color& color) { m_checkBoxColor = color; }
inline bool isChecked(void) const { return m_checked; }
inline void setStateChangedCallback(std::function<void(CheckBox&, bool)> callback) { callback_onStateChanged = std::move(callback); }
class CheckBox : public Widget
{
public:
inline CheckBox(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
inline CheckBox(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
CheckBox& create(const String& text);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseReleased(const Event& event) override;
void setText(const String& text);
void setChecked(bool checked);
inline String getText(void) const { return m_text; }
inline Color getCheckBorderColor(void) const { return m_checkBorderColor; }
inline void setCheckBorderColor(const Color& color) { m_checkBorderColor = color; }
inline Color getCheckBoxColor(void) const { return m_checkBoxColor; }
inline void setCheckBoxColor(const Color& color) { m_checkBoxColor = color; }
inline bool isChecked(void) const { return m_checked; }
inline void setStateChangedCallback(std::function<void(CheckBox&, bool)> callback) { callback_onStateChanged = std::move(callback); }
private:
void __update_size(ogfx::BasicRenderer2D& gfx);
private:
void __update_size(ogfx::BasicRenderer2D& gfx);
private:
bool m_checked { false };
String m_text { "" };
bool m_textChanged { false };
f32 m_spacing { 10 };
Vec2 m_checkSize { 0, 0 };
i32 m_checkBorderRadius { 5 };
i32 m_checkBorderWidth { 1 };
Color m_checkBorderColor { 255, 255, 255 };
Color m_checkBoxColor { 255, 255, 255 };
private:
bool m_checked { false };
String m_text { "" };
bool m_textChanged { false };
f32 m_spacing { 10 };
Vec2 m_checkSize { 0, 0 };
i32 m_checkBorderRadius { 5 };
i32 m_checkBorderWidth { 1 };
Color m_checkBorderColor { 255, 255, 255 };
Color m_checkBoxColor { 255, 255, 255 };
std::function<void(CheckBox&, bool)> callback_onStateChanged { nullptr };
};
}
std::function<void(CheckBox&, bool)> callback_onStateChanged { nullptr };
};
}
}

View file

@ -26,344 +26,341 @@ namespace ogfx
{
namespace gui
{
namespace widgets
Panel& Panel::create(void)
{
Panel& Panel::create(void)
{
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::Panel");
disableFocus();
enableStopEvents();
setStylesheetCategoryName("panel");
validate();
return *this;
}
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::Panel");
disableFocus();
enableStopEvents();
setStylesheetCategoryName("panel");
validate();
return *this;
}
void Panel::applyTheme(const ostd::Stylesheet& theme)
{
setScrollSpeed(getThemeValue<f32>(theme, "scrollSpeed", 0.8f));
setScrollSmoothFactor(getThemeValue<f32>(theme, "scrollSmoothFactor", 0.7f));
m_titleColor = getThemeValue<Color>(theme, "titleColor", Colors::Black);
m_titlebarColor = getThemeValue<Color>(theme, "titlebarColor", Colors::Transparent);
m_titlebarBorderColor = getThemeValue<Color>(theme, "titlebarBorderColor", Colors::Black);
m_titlebarHeight = getThemeValue<f32>(theme, "titlebarHeight", 30);
m_titlebarBorderWidth = getThemeValue<i32>(theme, "titlebarBorderWidth", 1);
m_titlebarFontSize = getThemeValue<i32>(theme, "titlebarFontSize", 26);
m_titleTextAlign = getThemeValue<i32>(theme, "titlebarTextAlign", cast<i32>(WindowCore::eTextAlign::Left));
setTitlebarType(getThemeValue<String>(theme, "titlebarType", TitleBarTypes::None));
}
void Panel::applyTheme(const ostd::Stylesheet& theme)
{
setScrollSpeed(getThemeValue<f32>(theme, "scrollSpeed", 0.8f));
setScrollSmoothFactor(getThemeValue<f32>(theme, "scrollSmoothFactor", 0.7f));
m_titleColor = getThemeValue<Color>(theme, "titleColor", Colors::Black);
m_titlebarColor = getThemeValue<Color>(theme, "titlebarColor", Colors::Transparent);
m_titlebarBorderColor = getThemeValue<Color>(theme, "titlebarBorderColor", Colors::Black);
m_titlebarHeight = getThemeValue<f32>(theme, "titlebarHeight", 30);
m_titlebarBorderWidth = getThemeValue<i32>(theme, "titlebarBorderWidth", 1);
m_titlebarFontSize = getThemeValue<i32>(theme, "titlebarFontSize", 26);
m_titleTextAlign = getThemeValue<i32>(theme, "titlebarTextAlign", cast<i32>(WindowCore::eTextAlign::Left));
setTitlebarType(getThemeValue<String>(theme, "titlebarType", TitleBarTypes::None));
}
void Panel::onMousePressed(const Event& event)
void Panel::onMousePressed(const Event& event)
{
if (!isDraggable())
return;
if (m_titleBarBounds.contains({ event.mouse->position_x, event.mouse->position_y }, true))
{
if (!isDraggable())
return;
if (m_titleBarBounds.contains({ event.mouse->position_x, event.mouse->position_y }, true))
{
m_mousePressed = true;
m_mousePos = { event.mouse->position_x, event.mouse->position_y };
}
m_mousePressed = true;
m_mousePos = { event.mouse->position_x, event.mouse->position_y };
}
}
void Panel::onMouseReleased(const Event& event)
void Panel::onMouseReleased(const Event& event)
{
m_mousePressed = false;
}
void Panel::onMouseDragged(const Event& event)
{
if (m_mousePressed)
{
m_mousePressed = false;
Vec2 mpos { event.mouse->position_x, event.mouse->position_y };
addPos(mpos - m_mousePos);
m_mousePos = mpos;
}
}
void Panel::onMouseDragged(const Event& event)
{
if (m_mousePressed)
{
Vec2 mpos { event.mouse->position_x, event.mouse->position_y };
addPos(mpos - m_mousePos);
m_mousePos = mpos;
}
}
void Panel::afterDraw(ogfx::BasicRenderer2D& gfx)
{
draw_titlebar(gfx);
drawScrollbars(gfx);
}
void Panel::afterDraw(ogfx::BasicRenderer2D& gfx)
{
draw_titlebar(gfx);
drawScrollbars(gfx);
}
void Panel::onWindowResized(const Event& event)
{
updateScrollbarsSize();
}
void Panel::onWindowResized(const Event& event)
void Panel::setTitlebarType(const String& type)
{
String t = type.new_toLower().trim();
if (t == TitleBarTypes::None)
{
setContentOffset({ 0, 0 });
updateScrollbarsSize();
m_titlebarType = TitleBarTypes::NoneValue;
}
void Panel::setTitlebarType(const String& type)
else if (t == TitleBarTypes::Minimal)
{
String t = type.new_toLower().trim();
if (t == TitleBarTypes::None)
setContentOffset({ 0, m_titlebarHeight });
updateScrollbarsSize();
m_titlebarType = TitleBarTypes::MinimalValue;
}
else if (t == TitleBarTypes::Full)
{
setContentOffset({ 0, m_titlebarHeight });
updateScrollbarsSize();
m_titlebarType = TitleBarTypes::FullValue;
}
}
String Panel::getTitlebarType(void) const
{
switch (m_titlebarType)
{
case TitleBarTypes::FullValue: return TitleBarTypes::Full;
case TitleBarTypes::MinimalValue: return TitleBarTypes::Minimal;
case TitleBarTypes::NoneValue: return TitleBarTypes::None;
default: return TitleBarTypes::None;
}
}
void Panel::draw_titlebar(BasicRenderer2D& gfx)
{
f32 br = cast<f32>(getBorderWidth());
br /= 2;
Rectangle titleBarBounds {
getGlobalPosition() + Vec2 { br, br },
Vec2 { getw(), m_titlebarHeight } - Vec2 { br * 2, br * 2 }
};
m_titleBarBounds = titleBarBounds;
switch (m_titlebarType)
{
case TitleBarTypes::FullValue:
{
setContentOffset({ 0, 0 });
updateScrollbarsSize();
m_titlebarType = TitleBarTypes::NoneValue;
}
else if (t == TitleBarTypes::Minimal)
{
setContentOffset({ 0, m_titlebarHeight });
updateScrollbarsSize();
m_titlebarType = TitleBarTypes::MinimalValue;
}
else if (t == TitleBarTypes::Full)
{
setContentOffset({ 0, m_titlebarHeight });
updateScrollbarsSize();
m_titlebarType = TitleBarTypes::FullValue;
}
}
String Panel::getTitlebarType(void) const
{
switch (m_titlebarType)
{
case TitleBarTypes::FullValue: return TitleBarTypes::Full;
case TitleBarTypes::MinimalValue: return TitleBarTypes::Minimal;
case TitleBarTypes::NoneValue: return TitleBarTypes::None;
default: return TitleBarTypes::None;
}
}
void Panel::draw_titlebar(BasicRenderer2D& gfx)
{
f32 br = cast<f32>(getBorderWidth());
br /= 2;
Rectangle titleBarBounds {
getGlobalPosition() + Vec2 { br, br },
Vec2 { getw(), m_titlebarHeight } - Vec2 { br * 2, br * 2 }
};
m_titleBarBounds = titleBarBounds;
switch (m_titlebarType)
{
case TitleBarTypes::FullValue:
{
gfx.outlinedRoundRect(titleBarBounds, m_titlebarColor, m_titlebarColor, { cast<f32>(getBorderRadius()), cast<f32>(getBorderRadius()), 0, 0 }, getBorderWidth());
gfx.drawLine({ getGlobalPosition() + Vec2 { 0, m_titlebarHeight - 2 }, getGlobalPosition() + Vec2 { getGlobalBounds().w, m_titlebarHeight - 2 } }, m_titlebarBorderColor, m_titlebarBorderWidth);
if (m_titleTextAlign == cast<i32>(WindowCore::eTextAlign::Center))
gfx.drawCenteredString(m_title, titleBarBounds, m_titleColor, m_titlebarFontSize);
else
gfx.drawString(m_title, titleBarBounds.getPosition() + Vec2 { 10, 5 }, m_titleColor, m_titlebarFontSize);
break;
}
case TitleBarTypes::MinimalValue:
{
if (m_titleTextAlign == cast<i32>(WindowCore::eTextAlign::Center))
gfx.drawCenteredString(m_title, titleBarBounds, m_titleColor, m_titlebarFontSize);
else
gfx.drawString(m_title, titleBarBounds.getPosition() + Vec2 { 10, 5 }, m_titleColor, m_titlebarFontSize);
break;
}
case TitleBarTypes::NoneValue:
default: break;
}
}
TabPanel& TabPanel::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::TabPanel");
disableFocus();
enableStopEvents();
setStylesheetCategoryName("tabPanel");
enableBackground();
enableBorder();
setTabBarHeight(m_tabBarHeight);
validate();
return *this;
}
void TabPanel::applyTheme(const ostd::Stylesheet& theme)
{
setTabBarHeight(getThemeValue<f32>(theme, "tabBar.height", m_tabBarHeight));
setTabBarBackgroundColor(getThemeValue<Color>(theme, "tabBar.backgroundColor", m_tabBarBackgroundColor));
setTabBarBorderColor(getThemeValue<Color>(theme, "tabBar.borderColor", m_tabBarBorderColor));
setTabBarBorderColor(getThemeValue<i32>(theme, "tabBar.borderWidth", m_tabBarBorderWidth));
setTabBarSidePadding(getThemeValue<f32>(theme, "tabBar.sidePadding", m_tabSidePadding));
}
void TabPanel::onMousePressed(const Event& event)
{
if (!isMouseInsideTabBar({ event.mouse->position_x, event.mouse->position_y }))
return;
const f32 extra_offset = 5;
i32 index = 0;
for (auto& b : m_tabBoundsList)
{
if (b.contains(event.mouse->position_x, event.mouse->position_y))
{
setCurrentTab(*m_tabs[index]);
Rectangle tabBounds = m_tabBoundsList[index];
f32 tabLocalLeft = tabBounds.x - getGlobalPosition().x;
f32 tabLocalRight = tabLocalLeft + tabBounds.w;
if (tabLocalLeft < 0)
m_tabScrollOffset += tabLocalLeft - extra_offset;
else if (tabLocalRight > getw())
m_tabScrollOffset += (tabLocalRight - getw()) + extra_offset;
break;
}
index++;
}
}
void TabPanel::onMouseScrolled(const Event& event)
{
if (!isMouseInsideTabBar({ event.mouse->position_x, event.mouse->position_y }))
return;
if (m_tabs.size() == 0)
return;
const f32 extra_offset = 5;
m_tabScrollOffset += (event.mouse->scrollAmount.y * -40.0f);
m_tabScrollOffset = std::clamp(m_tabScrollOffset, -extra_offset, m_totalTabWidth - getw() + extra_offset);
}
void TabPanel::onDraw(ogfx::BasicRenderer2D& gfx)
{
gfx.outlinedRect({ getGlobalPosition(), { getw(), getTabBarHeight() } }, getTabBarBackgroundColor(), getTabBarBorderColor(), getTabBarBorderWidth(), false, false, true, false);
draw_tabs(gfx);
}
Panel& TabPanel::addTab(const String& title)
{
m_tabs.push_back(std::make_unique<Panel>(getWindow()));
auto& tab = *m_tabs.back();
tab.setTitle(title);
tab.setTitlebarType(Panel::TitleBarTypes::None);
tab.addThemeOverride("@panel_tab.panel.titlebarType", "none");
tab.addThemeOverride("@panel_tab.panel.showBorder", false);
tab.addThemeOverride("@panel_tab.panel.borderRadius", 0);
tab.setBackgroundColor(getBackgroundColor());
if (m_currentTab == nullptr && m_tabs.size() == 1)
setCurrentTab(tab);
tab.addThemeID("panel_tab");
tab.reloadTheme(true);
return tab;
}
bool TabPanel::removeTab(Panel& tab)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
if (it == m_tabs.end())
return false;
if (m_currentTab == it->get())
prepare_for_current_tab_removal();
m_tabs.erase(it);
return true;
}
bool TabPanel::removeTab(i32 index)
{
if (index < 0 || index >= (i32)m_tabs.size())
return false;
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
if (std::distance(m_tabs.begin(), it) == index)
prepare_for_current_tab_removal();
m_tabs.erase(m_tabs.begin() + index);
return true;
}
bool TabPanel::removeTab(const String& title)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p->getTitle() == title; });
if (it == m_tabs.end())
return false;
m_tabs.erase(it);
return true;
}
bool TabPanel::setCurrentTab(Panel& tab)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
if (it == m_tabs.end())
return false;
i32 index = it - m_tabs.begin();
return setCurrentTab(index);
}
bool TabPanel::setCurrentTab(i32 index, bool ignore_same_tab)
{
if (index < 0 || index >= (i32)m_tabs.size())
return false;
auto curr = m_tabs[index].get();
if (!ignore_same_tab && curr == m_currentTab && m_currentTab != nullptr)
return false;
removeWidget(*m_currentTab);
m_currentTabIndex = index;
m_currentTab = curr;
m_currentTab->setMargin({ 0, 0, 0, 0 });
m_currentTab->setSize(getPureContentBounds().getSize());
m_currentTab->setPosition({ 0, 0 });
m_currentTab->reloadTheme(true);
m_currentTab->resetScroll();
m_currentTab->updateScrollbarsSize();
return addWidget(*m_currentTab);
}
void TabPanel::setTabBarHeight(f32 height)
{
m_tabBarHeight = height;
m_tabBarBorderRadii = { cast<f32>(getBorderRadius()), cast<f32>(getBorderRadius()), 0, 0 };
setContentOffset({ 0, m_tabBarHeight });
}
bool TabPanel::isMouseInsideTabBar(const Vec2& mousePos)
{
Rectangle bounds { getGlobalPosition(), { getw(), getTabBarHeight() } };
return bounds.contains(mousePos, true);
}
void TabPanel::prepare_for_current_tab_removal(void)
{
if (m_currentTab == nullptr)
return;
if (m_tabs.size() < 2)
{
m_currentTab = nullptr;
return;
}
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
if (it == m_tabs.end())
return; // shouldn't happen but defensive
if (it != m_tabs.begin())
m_currentTab = (it - 1)->get(); // tab to the left
else
m_currentTab = (it + 1)->get(); // first tab being removed, go right
}
void TabPanel::draw_tabs(ogfx::BasicRenderer2D& gfx)
{
f32 nextTabX = 1 - m_tabScrollOffset;
m_tabBoundsList.clear();
i32 index = -1;
for (const auto& _tab : m_tabs)
{
index++;
if (_tab == nullptr) continue;
const auto& tab = *_tab;
auto titleBounds = gfx.getStringDimensions(tab.getTitle(), getFontSize());
auto glob = getGlobalPosition();
Rectangle tabBounds { glob + Vec2 { nextTabX, 2}, { titleBounds.x + (m_tabSidePadding * 2), m_tabBarHeight } };
if (m_currentTab == _tab.get())
{
gfx.outlinedRect(tabBounds, tab.getBackgroundColor(), getTabBarBorderColor(), getTabBarBorderWidth(), true, true, true, true);
}
gfx.outlinedRoundRect(titleBarBounds, m_titlebarColor, m_titlebarColor, { cast<f32>(getBorderRadius()), cast<f32>(getBorderRadius()), 0, 0 }, getBorderWidth());
gfx.drawLine({ getGlobalPosition() + Vec2 { 0, m_titlebarHeight - 2 }, getGlobalPosition() + Vec2 { getGlobalBounds().w, m_titlebarHeight - 2 } }, m_titlebarBorderColor, m_titlebarBorderWidth);
if (m_titleTextAlign == cast<i32>(WindowCore::eTextAlign::Center))
gfx.drawCenteredString(m_title, titleBarBounds, m_titleColor, m_titlebarFontSize);
else
{
bool draw_right_edge = !(m_currentTabIndex == (index + 1));
gfx.drawRect(tabBounds - Rectangle { 0, 0, 0, 2 }, getTabBarBorderColor(), getTabBarBorderWidth(), false, draw_right_edge, true, false);
}
gfx.drawCenteredString(tab.getTitle(), tabBounds, getTextColor(), getFontSize());
nextTabX += titleBounds.x + (m_tabSidePadding * 2);
m_tabBoundsList.push_back(tabBounds);
gfx.drawString(m_title, titleBarBounds.getPosition() + Vec2 { 10, 5 }, m_titleColor, m_titlebarFontSize);
break;
}
m_totalTabWidth = nextTabX + m_tabScrollOffset - 1;
case TitleBarTypes::MinimalValue:
{
if (m_titleTextAlign == cast<i32>(WindowCore::eTextAlign::Center))
gfx.drawCenteredString(m_title, titleBarBounds, m_titleColor, m_titlebarFontSize);
else
gfx.drawString(m_title, titleBarBounds.getPosition() + Vec2 { 10, 5 }, m_titleColor, m_titlebarFontSize);
break;
}
case TitleBarTypes::NoneValue:
default: break;
}
}
TabPanel& TabPanel::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::TabPanel");
disableFocus();
enableStopEvents();
setStylesheetCategoryName("tabPanel");
enableBackground();
enableBorder();
setTabBarHeight(m_tabBarHeight);
validate();
return *this;
}
void TabPanel::applyTheme(const ostd::Stylesheet& theme)
{
setTabBarHeight(getThemeValue<f32>(theme, "tabBar.height", m_tabBarHeight));
setTabBarBackgroundColor(getThemeValue<Color>(theme, "tabBar.backgroundColor", m_tabBarBackgroundColor));
setTabBarBorderColor(getThemeValue<Color>(theme, "tabBar.borderColor", m_tabBarBorderColor));
setTabBarBorderColor(getThemeValue<i32>(theme, "tabBar.borderWidth", m_tabBarBorderWidth));
setTabBarSidePadding(getThemeValue<f32>(theme, "tabBar.sidePadding", m_tabSidePadding));
}
void TabPanel::onMousePressed(const Event& event)
{
if (!isMouseInsideTabBar({ event.mouse->position_x, event.mouse->position_y }))
return;
const f32 extra_offset = 5;
i32 index = 0;
for (auto& b : m_tabBoundsList)
{
if (b.contains(event.mouse->position_x, event.mouse->position_y))
{
setCurrentTab(*m_tabs[index]);
Rectangle tabBounds = m_tabBoundsList[index];
f32 tabLocalLeft = tabBounds.x - getGlobalPosition().x;
f32 tabLocalRight = tabLocalLeft + tabBounds.w;
if (tabLocalLeft < 0)
m_tabScrollOffset += tabLocalLeft - extra_offset;
else if (tabLocalRight > getw())
m_tabScrollOffset += (tabLocalRight - getw()) + extra_offset;
break;
}
index++;
}
}
void TabPanel::onMouseScrolled(const Event& event)
{
if (!isMouseInsideTabBar({ event.mouse->position_x, event.mouse->position_y }))
return;
if (m_tabs.size() == 0)
return;
const f32 extra_offset = 5;
m_tabScrollOffset += (event.mouse->scrollAmount.y * -40.0f);
m_tabScrollOffset = std::clamp(m_tabScrollOffset, -extra_offset, m_totalTabWidth - getw() + extra_offset);
}
void TabPanel::onDraw(ogfx::BasicRenderer2D& gfx)
{
gfx.outlinedRect({ getGlobalPosition(), { getw(), getTabBarHeight() } }, getTabBarBackgroundColor(), getTabBarBorderColor(), getTabBarBorderWidth(), false, false, true, false);
draw_tabs(gfx);
}
Panel& TabPanel::addTab(const String& title)
{
m_tabs.push_back(std::make_unique<Panel>(getWindow()));
auto& tab = *m_tabs.back();
tab.setTitle(title);
tab.setTitlebarType(Panel::TitleBarTypes::None);
tab.addThemeOverride("@panel_tab.panel.titlebarType", "none");
tab.addThemeOverride("@panel_tab.panel.showBorder", false);
tab.addThemeOverride("@panel_tab.panel.borderRadius", 0);
tab.setBackgroundColor(getBackgroundColor());
if (m_currentTab == nullptr && m_tabs.size() == 1)
setCurrentTab(tab);
tab.addThemeID("panel_tab");
tab.reloadTheme(true);
return tab;
}
bool TabPanel::removeTab(Panel& tab)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
if (it == m_tabs.end())
return false;
if (m_currentTab == it->get())
prepare_for_current_tab_removal();
m_tabs.erase(it);
return true;
}
bool TabPanel::removeTab(i32 index)
{
if (index < 0 || index >= (i32)m_tabs.size())
return false;
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
if (std::distance(m_tabs.begin(), it) == index)
prepare_for_current_tab_removal();
m_tabs.erase(m_tabs.begin() + index);
return true;
}
bool TabPanel::removeTab(const String& title)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p->getTitle() == title; });
if (it == m_tabs.end())
return false;
m_tabs.erase(it);
return true;
}
bool TabPanel::setCurrentTab(Panel& tab)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
if (it == m_tabs.end())
return false;
i32 index = it - m_tabs.begin();
return setCurrentTab(index);
}
bool TabPanel::setCurrentTab(i32 index, bool ignore_same_tab)
{
if (index < 0 || index >= (i32)m_tabs.size())
return false;
auto curr = m_tabs[index].get();
if (!ignore_same_tab && curr == m_currentTab && m_currentTab != nullptr)
return false;
removeWidget(*m_currentTab);
m_currentTabIndex = index;
m_currentTab = curr;
m_currentTab->setMargin({ 0, 0, 0, 0 });
m_currentTab->setSize(getPureContentBounds().getSize());
m_currentTab->setPosition({ 0, 0 });
m_currentTab->reloadTheme(true);
m_currentTab->resetScroll();
m_currentTab->updateScrollbarsSize();
return addWidget(*m_currentTab);
}
void TabPanel::setTabBarHeight(f32 height)
{
m_tabBarHeight = height;
m_tabBarBorderRadii = { cast<f32>(getBorderRadius()), cast<f32>(getBorderRadius()), 0, 0 };
setContentOffset({ 0, m_tabBarHeight });
}
bool TabPanel::isMouseInsideTabBar(const Vec2& mousePos)
{
Rectangle bounds { getGlobalPosition(), { getw(), getTabBarHeight() } };
return bounds.contains(mousePos, true);
}
void TabPanel::prepare_for_current_tab_removal(void)
{
if (m_currentTab == nullptr)
return;
if (m_tabs.size() < 2)
{
m_currentTab = nullptr;
return;
}
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
if (it == m_tabs.end())
return; // shouldn't happen but defensive
if (it != m_tabs.begin())
m_currentTab = (it - 1)->get(); // tab to the left
else
m_currentTab = (it + 1)->get(); // first tab being removed, go right
}
void TabPanel::draw_tabs(ogfx::BasicRenderer2D& gfx)
{
f32 nextTabX = 1 - m_tabScrollOffset;
m_tabBoundsList.clear();
i32 index = -1;
for (const auto& _tab : m_tabs)
{
index++;
if (_tab == nullptr) continue;
const auto& tab = *_tab;
auto titleBounds = gfx.getStringDimensions(tab.getTitle(), getFontSize());
auto glob = getGlobalPosition();
Rectangle tabBounds { glob + Vec2 { nextTabX, 2}, { titleBounds.x + (m_tabSidePadding * 2), m_tabBarHeight } };
if (m_currentTab == _tab.get())
{
gfx.outlinedRect(tabBounds, tab.getBackgroundColor(), getTabBarBorderColor(), getTabBarBorderWidth(), true, true, true, true);
}
else
{
bool draw_right_edge = !(m_currentTabIndex == (index + 1));
gfx.drawRect(tabBounds - Rectangle { 0, 0, 0, 2 }, getTabBarBorderColor(), getTabBarBorderWidth(), false, draw_right_edge, true, false);
}
gfx.drawCenteredString(tab.getTitle(), tabBounds, getTextColor(), getFontSize());
nextTabX += titleBounds.x + (m_tabSidePadding * 2);
m_tabBoundsList.push_back(tabBounds);
}
m_totalTabWidth = nextTabX + m_tabScrollOffset - 1;
}
}
}

View file

@ -28,99 +28,96 @@ namespace ogfx
{
namespace gui
{
namespace widgets
class Panel : public ScrollableWidget
{
class Panel : public ScrollableWidget
public: struct TitleBarTypes
{
public: struct TitleBarTypes
{
inline static const String None { "none" };
inline static const String Full { "full" };
inline static const String Minimal { "minimal" };
inline static const String None { "none" };
inline static const String Full { "full" };
inline static const String Minimal { "minimal" };
inline static constexpr i32 NoneValue = 0;
inline static constexpr i32 FullValue = 1;
inline static constexpr i32 MinimalValue = 2;
};
public:
inline Panel(WindowCore& window) : ScrollableWidget(window) { create(); }
Panel& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMousePressed(const Event& event) override;
void onMouseReleased(const Event& event) override;
void onMouseDragged(const Event& event) override;
void onWindowResized(const Event& event) override;
void setTitlebarType(const String& type);
String getTitlebarType(void) const;
inline String getTitle(void) const { return m_title; }
inline void setTitle(const String& title) { m_title = title; }
inline f32 getTitlebarHeight(void) const { return m_titlebarHeight; }
OSTD_BOOL_PARAM_GETSET_I(Draggable, m_draggable);
private:
void draw_titlebar(BasicRenderer2D& gfx);
private:
String m_title { "Panel" };
Vec2 m_mousePos { 0, 0 };
bool m_mousePressed { false };
bool m_draggable { false };
Rectangle m_titleBarBounds;
Color m_titleColor { Colors::Black };
i32 m_titlebarType = TitleBarTypes::NoneValue;
f32 m_titlebarHeight { 30 };
i32 m_titlebarBorderWidth { 1 };
i32 m_titlebarFontSize { 26 };
Color m_titlebarColor { Colors::Transparent };
Color m_titlebarBorderColor { Colors::Black };
i32 m_titleTextAlign { 0 };
inline static constexpr i32 NoneValue = 0;
inline static constexpr i32 FullValue = 1;
inline static constexpr i32 MinimalValue = 2;
};
class TabPanel : public Widget
{
public:
inline TabPanel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
TabPanel& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onMousePressed(const Event& event) override;
void onMouseScrolled(const Event& event) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
Panel& addTab(const String& title);
bool removeTab(Panel& tab);
bool removeTab(i32 index);
bool removeTab(const String& title);
bool setCurrentTab(Panel& tab);
bool setCurrentTab(i32 index, bool ignore_same_tab = false);
void setTabBarHeight(f32 height);
bool isMouseInsideTabBar(const Vec2& mousePos);
inline void refreshCurrentTab(void) { setCurrentTab(m_currentTabIndex, true); }
public:
inline Panel(WindowCore& window) : ScrollableWidget(window) { create(); }
Panel& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMousePressed(const Event& event) override;
void onMouseReleased(const Event& event) override;
void onMouseDragged(const Event& event) override;
void onWindowResized(const Event& event) override;
void setTitlebarType(const String& type);
String getTitlebarType(void) const;
inline String getTitle(void) const { return m_title; }
inline void setTitle(const String& title) { m_title = title; }
inline f32 getTitlebarHeight(void) const { return m_titlebarHeight; }
OSTD_BOOL_PARAM_GETSET_I(Draggable, m_draggable);
inline f32 getTabBarHeight(void) const { return m_tabBarHeight; }
OSTD_PARAM_GETSET(Color, TabBarBackgroundColor, m_tabBarBackgroundColor);
OSTD_PARAM_GETSET(Color, TabBarBorderColor, m_tabBarBorderColor);
OSTD_PARAM_GETSET(f32, TabBarSidePadding, m_tabSidePadding);
OSTD_PARAM_GETSET(i32, TabBarBorderWidth, m_tabBarBorderWidth);
private:
void draw_titlebar(BasicRenderer2D& gfx);
private:
void prepare_for_current_tab_removal(void);
void draw_tabs(ogfx::BasicRenderer2D& gfx);
private:
String m_title { "Panel" };
Vec2 m_mousePos { 0, 0 };
bool m_mousePressed { false };
bool m_draggable { false };
Rectangle m_titleBarBounds;
private:
stdvec<std::unique_ptr<Panel>> m_tabs;
stdvec<Rectangle> m_tabBoundsList;
Panel* m_currentTab { nullptr };
i32 m_currentTabIndex { -1 };
f32 m_tabScrollOffset { 0.0f };
f32 m_totalTabWidth { 0.0f };
Rectangle m_tabBarBorderRadii { 0, 0, 0, 0 };
Color m_titleColor { Colors::Black };
i32 m_titlebarType = TitleBarTypes::NoneValue;
f32 m_titlebarHeight { 30 };
i32 m_titlebarBorderWidth { 1 };
i32 m_titlebarFontSize { 26 };
Color m_titlebarColor { Colors::Transparent };
Color m_titlebarBorderColor { Colors::Black };
i32 m_titleTextAlign { 0 };
};
class TabPanel : public Widget
{
public:
inline TabPanel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
TabPanel& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onMousePressed(const Event& event) override;
void onMouseScrolled(const Event& event) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
Panel& addTab(const String& title);
bool removeTab(Panel& tab);
bool removeTab(i32 index);
bool removeTab(const String& title);
bool setCurrentTab(Panel& tab);
bool setCurrentTab(i32 index, bool ignore_same_tab = false);
void setTabBarHeight(f32 height);
bool isMouseInsideTabBar(const Vec2& mousePos);
inline void refreshCurrentTab(void) { setCurrentTab(m_currentTabIndex, true); }
f32 m_tabBarHeight { 35 };
Color m_tabBarBackgroundColor { 120, 120, 120 };
Color m_tabBarBorderColor { 170, 170, 170 };
f32 m_tabSidePadding { 20 };
i32 m_tabBarBorderWidth { 2 };
};
}
inline f32 getTabBarHeight(void) const { return m_tabBarHeight; }
OSTD_PARAM_GETSET(Color, TabBarBackgroundColor, m_tabBarBackgroundColor);
OSTD_PARAM_GETSET(Color, TabBarBorderColor, m_tabBarBorderColor);
OSTD_PARAM_GETSET(f32, TabBarSidePadding, m_tabSidePadding);
OSTD_PARAM_GETSET(i32, TabBarBorderWidth, m_tabBarBorderWidth);
private:
void prepare_for_current_tab_removal(void);
void draw_tabs(ogfx::BasicRenderer2D& gfx);
private:
stdvec<std::unique_ptr<Panel>> m_tabs;
stdvec<Rectangle> m_tabBoundsList;
Panel* m_currentTab { nullptr };
i32 m_currentTabIndex { -1 };
f32 m_tabScrollOffset { 0.0f };
f32 m_totalTabWidth { 0.0f };
Rectangle m_tabBarBorderRadii { 0, 0, 0, 0 };
f32 m_tabBarHeight { 35 };
Color m_tabBarBackgroundColor { 120, 120, 120 };
Color m_tabBarBorderColor { 170, 170, 170 };
f32 m_tabSidePadding { 20 };
i32 m_tabBarBorderWidth { 2 };
};
}
}

View file

@ -22,105 +22,227 @@
#include "../../render/BasicRenderer.hpp"
#include "../../../ostd/io/FileSystem.hpp"
#include "../Window.hpp"
#include <algorithm>
#include <cmath>
namespace ogfx
{
namespace gui
{
namespace widgets
// ===================================================================
// Label
// ===================================================================
Label& Label::create(const String& text)
{
Label& Label::create(const String& text)
m_text = text;
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::Label");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("label");
validate();
invalidate_layout();
return *this;
}
void Label::setSizeMode(eSizeMode mode)
{
m_sizeMode = mode;
m_modeExplicitlySet = true;
invalidate_layout();
}
eSizeMode Label::effectiveSizeMode(void) const
{
if (!m_modeExplicitlySet && getParent() && getParent()->hasLayout())
return eSizeMode::LayoutManaged;
return m_sizeMode;
}
void Label::onBoundsChanged(const Vec2& /*newSize*/)
{
invalidate_layout();
}
void Label::setText(const String& text)
{
m_text = text;
invalidate_layout();
}
void Label::onDraw(ogfx::BasicRenderer2D& gfx)
{
if (m_layoutDirty)
recompute_layout();
gfx.drawString(
m_text,
getGlobalContentPosition() + m_drawTextOffset,
getTextColor(),
getFontSize()
);
}
void Label::recompute_layout(void)
{
m_layoutDirty = false;
auto& gfx = getWindow().getGFX();
const Vec2 textSize = gfx.getStringDimensions(m_text, getFontSize());
const Rectangle pad = getPadding();
const eSizeMode mode = effectiveSizeMode();
if (mode == eSizeMode::AutoSize)
{
setText(text);
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::Label");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("label");
validate();
return *this;
const f32 wantW = textSize.x + pad.left() + pad.right();
const f32 wantH = textSize.y + pad.top() + pad.bottom();
// Avoid feedback loop on hover/focus theme reloads — only resize
// if the desired size actually changed by a meaningful amount.
if (std::abs(getw() - wantW) > 0.5f || std::abs(geth() - wantH) > 0.5f)
setSize({ wantW, wantH });
// Auto-size means the content fills the content box exactly,
// so the text offset is (0, 0).
m_drawTextOffset = { 0, 0 };
}
void Label::onDraw(ogfx::BasicRenderer2D& gfx)
else
{
if (m_textChanged)
__update_size(gfx);
gfx.drawString(getText(), getGlobalContentPosition(), getTextColor(), getFontSize());
}
void Label::setText(const String& text)
{
m_text = text;
m_textChanged = true;
}
void Label::__update_size(ogfx::BasicRenderer2D& gfx)
{
auto size = gfx.getStringDimensions(getText(), getFontSize());
size.x += getPadding().left();
size.x += getPadding().right();
size.y += getPadding().top();
size.y += getPadding().bottom();
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
m_textChanged = false;
// Manual / LayoutManaged: center text inside the (user/layout-given)
// content area. Overflow is clipped at the widget edge.
const Vec2 contentSize = getContentBounds().getSize();
m_drawTextOffset = {
(contentSize.x - textSize.x) * 0.5f,
(contentSize.y - textSize.y) * 0.5f
};
}
}
// ===================================================================
// ImageLabel
// ===================================================================
ImageLabel& ImageLabel::create(const String& filePath)
{
setImage(filePath);
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::ImageLabel");
disableChildren();
disableBackground();
disableBorder();
setStylesheetCategoryName("image");
setSize(64, 64);
validate();
return *this;
}
ImageLabel& ImageLabel::create(const String& filePath)
{
void ImageLabel::applyTheme(const ostd::Stylesheet& theme)
{
enableAnimated(getThemeValue<bool>(theme, "animated", m_animated));
setTintColor(getThemeValue<Color>(theme, "tint", getTintColor()));
setAnimationData(getThemeValue<AnimationData>(theme, "animation", m_animData));
String filePath = getThemeValue<String>(theme, "path", m_image.getFilePath());
if (filePath != m_image.getFilePath())
setImage(filePath);
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::ImageLabel");
disableChildren();
disableBackground();
disableBorder();
setStylesheetCategoryName("image");
setSize(64, 64);
validate();
return *this;
}
void ImageLabel::applyTheme(const ostd::Stylesheet& theme)
// NOTE: the old code set widget size from theme here, which is wrong
// once layouts come into play. The theme's "size" affects m_imageSize
// (which is meaningful in Native mode). Widget size stays under
// user/layout control.
setImageSize(getThemeValue<Vec2>(theme, "size", getImageSize()));
if (isAnimatedEnabled())
{
enableAnimated(getThemeValue<bool>(theme, "animated", m_animated));
setTintColor(getThemeValue<Color>(theme, "tint", getTintColor()));
setAnimationData(getThemeValue<AnimationData>(theme, "animation", m_animData));
String filePath = getThemeValue<String>(theme, "path", m_image.getFilePath());
if (filePath != m_image.getFilePath())
setImage(filePath);
setSize(getThemeValue<Vec2>(theme, "size", getSize()));
if (isAnimatedEnabled())
m_anim.create(m_animData);
m_anim.setSpriteSheet(m_image);
}
}
void ImageLabel::setImage(const String& filePath)
{
if (filePath == "") return;
if (!ostd::FileSystem::fileExists(filePath))
return;
m_image.destroy();
m_image.loadFromFile(filePath, getWindow().getGFX());
}
void ImageLabel::onUpdate(void)
{
if (isAnimatedEnabled())
m_anim.update();
}
void ImageLabel::onDraw(ogfx::BasicRenderer2D& gfx)
{
Vec2 drawPos, drawSize;
compute_draw_geometry(drawPos, drawSize);
if (drawSize.x <= 0 || drawSize.y <= 0)
return;
if (isAnimatedEnabled())
gfx.drawAnimation(m_anim, drawPos, drawSize, getTintColor());
else
gfx.drawImage(m_image, drawPos, drawSize, getTintColor());
}
void ImageLabel::compute_draw_geometry(Vec2& outPos, Vec2& outSize) const
{
const Vec2 contentTL = getGlobalContentPosition();
const Vec2 contentSize = getContentBounds().getSize();
// Natural size: when the image is a sprite sheet driven by an
// Animation, one frame is the natural unit, not the whole sheet.
// (Otherwise letterboxing would respect the sheet's aspect ratio,
// which is meaningless — it depends on column/row count.)
Vec2 nat { 0, 0 };
if (m_image.isLoaded())
{
if (m_animated)
{
m_anim.create(m_animData);
m_anim.setSpriteSheet(m_image);
const auto frame = m_anim.getFrameRect();
nat = { frame.w, frame.h };
}
else
{
nat = m_image.getSize();
}
}
void ImageLabel::onDraw(ogfx::BasicRenderer2D& gfx)
if (m_fit == eImageFit::Fit)
{
if (isAnimatedEnabled())
gfx.drawAnimation(m_anim, getGlobalPosition() + getPadding().topLeft(), getSize() - getPadding().bottomRight(), getTintColor());
else
gfx.drawImage(m_image, getGlobalPosition() + getPadding().topLeft(), getSize() - getPadding().bottomRight(), getTintColor());
}
void ImageLabel::onUpdate(void)
{
if (isAnimatedEnabled())
m_anim.update();
}
void ImageLabel::setImage(const String& filePath)
{
if (filePath == "") return;
if (!ostd::FileSystem::fileExists(filePath))
if (!m_keepAspect)
{
// Stretch to fill, no aspect preservation.
outPos = contentTL;
outSize = contentSize;
return;
m_image.destroy();
m_image.loadFromFile(filePath, getWindow().getGFX());
}
// Letterbox: scale uniformly to fit, then center.
if (nat.x <= 0 || nat.y <= 0)
{
// No natural size known — fall back to filling.
outPos = contentTL;
outSize = contentSize;
return;
}
const f32 sx = contentSize.x / nat.x;
const f32 sy = contentSize.y / nat.y;
const f32 s = std::min(sx, sy);
outSize = { nat.x * s, nat.y * s };
outPos = contentTL + Vec2 {
(contentSize.x - outSize.x) * 0.5f,
(contentSize.y - outSize.y) * 0.5f
};
return;
}
// Native: draw at m_imageSize, centered. Overflow is clipped by
// the widget's automatic clipping at the renderer level.
outSize = m_imageSize;
outPos = contentTL + Vec2 {
(contentSize.x - outSize.x) * 0.5f,
(contentSize.y - outSize.y) * 0.5f
};
}
}
}

View file

@ -21,6 +21,7 @@
#pragma once
#include <ogfx/gui/widgets/Widget.hpp>
#include <ogfx/gui/widgets/Button.hpp> // for eSizeMode
#include <ogfx/resources/Image.hpp>
#include <ogfx/utils/Animation.hpp>
@ -28,47 +29,115 @@ namespace ogfx
{
namespace gui
{
namespace widgets
// ===========================================================================
// Label — text-only widget.
//
// Modes (using the shared eSizeMode):
// AutoSize: widget shrinks/grows to fit text + padding.
// Manual: widget keeps user-set size; text drawn inside, clipped
// if it doesn't fit.
// LayoutManaged: parent layout decides size; otherwise behaves like Manual.
// ===========================================================================
class Label : public Widget
{
class Label : public Widget
{
public:
inline Label(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
inline Label(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
Label& create(const String& text);
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void setText(const String& text);
inline String getText(void) const { return m_text; }
public:
inline Label(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
inline Label(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
Label& create(const String& text);
private:
void __update_size(ogfx::BasicRenderer2D& gfx);
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onBoundsChanged(const Vec2& newSize) override;
private:
String m_text { "" };
bool m_textChanged { false };
};
class ImageLabel : public Widget
{
public:
inline ImageLabel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
inline ImageLabel(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
ImageLabel& create(const String& filePath);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onUpdate(void) override;
void setImage(const String& filePath);
inline Image& getImage(void) { return m_image; }
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
OSTD_PARAM_GETSET(AnimationData, AnimationData, m_animData);
OSTD_PARAM_GETSET(Color, TintColor, m_tintColor);
void setText(const String& text);
inline String getText(void) const { return m_text; }
private:
Image m_image;
Animation m_anim;
AnimationData m_animData;
Color m_tintColor { Colors::White };
bool m_animated { false };
};
}
// Same semantics as Button::setSizeMode / effectiveSizeMode.
void setSizeMode(eSizeMode mode);
inline eSizeMode getSizeMode(void) const { return m_sizeMode; }
eSizeMode effectiveSizeMode(void) const;
private:
inline void invalidate_layout(void) { m_layoutDirty = true; }
void recompute_layout(void);
private:
String m_text { "" };
eSizeMode m_sizeMode { eSizeMode::AutoSize };
bool m_modeExplicitlySet { false };
bool m_layoutDirty { true };
Vec2 m_drawTextOffset { 0, 0 };
};
// ===========================================================================
// ImageLabel — image-only widget.
//
// Two image modes (separate from widget size — the widget's size always
// comes from the user or from the parent layout):
//
// eImageFit::Fit: image scales to fill the widget's content area.
// Optional keepAspectRatio: if true, image is
// scaled uniformly to fit and centered (letterbox);
// if false, image stretches non-uniformly to fill.
//
// eImageFit::Native: image drawn at user-specified m_imageSize,
// centered inside the widget bounds. Crops naturally
// if m_imageSize > widget content size — the widget
// clips at its own bounds anyway.
// ===========================================================================
enum class eImageFit : u8
{
Fit,
Native
};
class ImageLabel : public Widget
{
public:
inline ImageLabel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
inline ImageLabel(WindowCore& window, const String& filePath) : Widget({ 0, 0, 0, 0 }, window) { create(filePath); }
ImageLabel& create(const String& filePath);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onUpdate(void) override;
void setImage(const String& filePath);
inline Image& getImage(void) { return m_image; }
// Mode selection. Default is Fit + keepAspectRatio=true.
inline void setImageFit(eImageFit fit) { m_fit = fit; }
inline eImageFit getImageFit(void) const { return m_fit; }
// Only meaningful when fit == Fit. If true, image keeps its
// aspect ratio and is centered with letterbox bars; if false,
// image stretches non-uniformly to fill.
OSTD_BOOL_PARAM_GETSET_E(KeepAspectRatio, m_keepAspect);
// Only meaningful when fit == Native. The size to draw the image at,
// regardless of widget size. The image is drawn centered.
OSTD_PARAM_GETSET(Vec2, ImageSize, m_imageSize);
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
OSTD_PARAM_GETSET(AnimationData, AnimationData, m_animData);
OSTD_PARAM_GETSET(Color, TintColor, m_tintColor);
private:
// Compute (drawPosition, drawSize) for the current frame.
// drawPosition is in global coords. drawSize is the on-screen size.
void compute_draw_geometry(Vec2& outPos, Vec2& outSize) const;
private:
Image m_image;
Animation m_anim;
AnimationData m_animData;
Color m_tintColor { Colors::White };
eImageFit m_fit { eImageFit::Fit };
bool m_keepAspect { true };
Vec2 m_imageSize { 64, 64 }; // user-specified size for Native mode
bool m_animated { false };
};
}
}

View file

@ -27,249 +27,246 @@ namespace ogfx
{
namespace gui
{
namespace widgets
void ListView::Item::setText(const String& text)
{
void ListView::Item::setText(const String& text)
if (m_text == text)
return;
m_text = text;
update_dimensions();
if (m_parent) m_parent->m_extentsDirty = true;
}
void ListView::Item::setFontSize(i32 fontSize)
{
if (m_fontSize == fontSize)
return;
m_fontSize = fontSize;
update_dimensions();
if (m_parent) m_parent->m_extentsDirty = true;
}
void ListView::Item::update_dimensions(void)
{
if (!isValid())
return;
m_dimensions = m_parent->getWindow().getGFX().getStringDimensions(m_text, m_fontSize);
m_dimensions += Vec2 { m_padding.x + m_padding.w, m_padding.y + m_padding.h };
}
void ListView::Item::set_selected(stdvec<Item*>& selectionList)
{
for (auto& sel : selectionList)
sel->m_selected = false;
selectionList.clear();
selectionList.push_back(this);
m_selected = true;
}
void ListView::Item::add_selected(stdvec<Item*>& selectionList)
{
if (STDVEC_CONTAINS(selectionList, this))
return;
selectionList.push_back(this);
m_selected = true;
}
void ListView::Item::remove_selected(stdvec<Item*>& selectionList)
{
if (!STDVEC_CONTAINS(selectionList, this))
return;
STDVEC_REMOVE(selectionList, this);
m_selected = false;
}
ListView& ListView::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::ListView");
enableStopEvents();
enableBackground();
enableBorder();
setBackgroundColor({ 160, 160, 160 });
setBorderColor({ 50, 50, 50 });
setStylesheetCategoryName("list");
validate();
return *this;
}
void ListView::applyTheme(const ostd::Stylesheet& theme)
{
setSeparatorLineColor(getThemeValue<Color>(theme, "separatorLineColor", getSeparatorLineColor()));
enableShowSeparatorLine(getThemeValue<bool>(theme, "showSeparatorLine", isShowSeparatorLineEnabled()));
setDefaultLineTextColor(getThemeValue<Color>(theme, "item.defaultTextColor", getDefaultLineTextColor()));
setDefaultLineFontSize(getThemeValue<i32>(theme, "item.defaultFontSize", getDefaultLineFontSize()));
setDefaultLinePadding(getThemeValue<Rectangle>(theme, "item.defaultPadding", getDefaultLinePadding()));
setDefaultLineSelectionColor(getThemeValue<Color>(theme, "item.defaultSelectionColor", getDefaultLineSelectionColor()));
setDefaultLineSelectionTextColor(getThemeValue<Color>(theme, "item.defaultSelectionTextColor", getDefaultLineSelectionTextColor()));
}
void ListView::onDraw(ogfx::BasicRenderer2D& gfx)
{
const auto& bounds = getGlobalBounds();
const auto& content = getContentExtents();
const f32 scrollY = -getScrollOffset().y;
const f32 visibleH = getContentBounds().h;
const f32 visibleStart = scrollY;
const f32 visibleEnd = scrollY + visibleH;
Color textColor;
f32 y = 0;
i32 startIdx = 0;
for (auto& item : m_list)
{
if (m_text == text)
return;
m_text = text;
update_dimensions();
if (m_parent) m_parent->m_extentsDirty = true;
if (!item.isValid()) { startIdx++; continue; }
f32 itemH = item.getDimensions().y;
if (y + itemH > visibleStart) break;
y += itemH;
startIdx++;
}
void ListView::Item::setFontSize(i32 fontSize)
for (i32 i = startIdx; i < (i32)m_list.size(); i++)
{
if (m_fontSize == fontSize)
return;
m_fontSize = fontSize;
update_dimensions();
if (m_parent) m_parent->m_extentsDirty = true;
}
void ListView::Item::update_dimensions(void)
{
if (!isValid())
return;
m_dimensions = m_parent->getWindow().getGFX().getStringDimensions(m_text, m_fontSize);
m_dimensions += Vec2 { m_padding.x + m_padding.w, m_padding.y + m_padding.h };
}
void ListView::Item::set_selected(stdvec<Item*>& selectionList)
{
for (auto& sel : selectionList)
sel->m_selected = false;
selectionList.clear();
selectionList.push_back(this);
m_selected = true;
}
void ListView::Item::add_selected(stdvec<Item*>& selectionList)
{
if (STDVEC_CONTAINS(selectionList, this))
return;
selectionList.push_back(this);
m_selected = true;
}
void ListView::Item::remove_selected(stdvec<Item*>& selectionList)
{
if (!STDVEC_CONTAINS(selectionList, this))
return;
STDVEC_REMOVE(selectionList, this);
m_selected = false;
}
ListView& ListView::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::ListView");
enableStopEvents();
enableBackground();
enableBorder();
setBackgroundColor({ 160, 160, 160 });
setBorderColor({ 50, 50, 50 });
setStylesheetCategoryName("list");
validate();
return *this;
}
void ListView::applyTheme(const ostd::Stylesheet& theme)
{
setSeparatorLineColor(getThemeValue<Color>(theme, "separatorLineColor", getSeparatorLineColor()));
enableShowSeparatorLine(getThemeValue<bool>(theme, "showSeparatorLine", isShowSeparatorLineEnabled()));
setDefaultLineTextColor(getThemeValue<Color>(theme, "item.defaultTextColor", getDefaultLineTextColor()));
setDefaultLineFontSize(getThemeValue<i32>(theme, "item.defaultFontSize", getDefaultLineFontSize()));
setDefaultLinePadding(getThemeValue<Rectangle>(theme, "item.defaultPadding", getDefaultLinePadding()));
setDefaultLineSelectionColor(getThemeValue<Color>(theme, "item.defaultSelectionColor", getDefaultLineSelectionColor()));
setDefaultLineSelectionTextColor(getThemeValue<Color>(theme, "item.defaultSelectionTextColor", getDefaultLineSelectionTextColor()));
}
void ListView::onDraw(ogfx::BasicRenderer2D& gfx)
{
const auto& bounds = getGlobalBounds();
const auto& content = getContentExtents();
const f32 scrollY = -getScrollOffset().y;
const f32 visibleH = getContentBounds().h;
const f32 visibleStart = scrollY;
const f32 visibleEnd = scrollY + visibleH;
Color textColor;
f32 y = 0;
i32 startIdx = 0;
for (auto& item : m_list)
auto& item = m_list[i];
if (!item.isValid()) continue;
f32 itemH = item.getDimensions().y;
if (y > visibleEnd) break;
f32 lineW = content.w;
if (content.w < bounds.w)
lineW = bounds.w;
if (item.isSelected())
{
if (!item.isValid()) { startIdx++; continue; }
f32 itemH = item.getDimensions().y;
if (y + itemH > visibleStart) break;
y += itemH;
startIdx++;
}
for (i32 i = startIdx; i < (i32)m_list.size(); i++)
{
auto& item = m_list[i];
if (!item.isValid()) continue;
f32 itemH = item.getDimensions().y;
if (y > visibleEnd) break;
f32 lineW = content.w;
if (content.w < bounds.w)
lineW = bounds.w;
if (item.isSelected())
{
textColor = item.getSelectedTextColor();
gfx.fillRect({ Vec2 { bounds.x, bounds.y + y + item.getPadding().h } + getScrollOffset(), { lineW, itemH } }, item.getSelectedColor());
}
else
textColor = item.getTextColor();
gfx.drawLine({ Vec2 { bounds.x, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset(), Vec2 { bounds.x + lineW, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset() }, getSeparatorLineColor(), 1);
gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), textColor, item.getFontSize());
y += itemH;
textColor = item.getSelectedTextColor();
gfx.fillRect({ Vec2 { bounds.x, bounds.y + y + item.getPadding().h } + getScrollOffset(), { lineW, itemH } }, item.getSelectedColor());
}
else
textColor = item.getTextColor();
gfx.drawLine({ Vec2 { bounds.x, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset(), Vec2 { bounds.x + lineW, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset() }, getSeparatorLineColor(), 1);
gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), textColor, item.getFontSize());
y += itemH;
}
}
void ListView::afterDraw(ogfx::BasicRenderer2D& gfx)
void ListView::afterDraw(ogfx::BasicRenderer2D& gfx)
{
drawScrollbars(gfx);
}
void ListView::onMouseReleased(const Event& event)
{
if (!isMouseInside())
return;
if (event.mouse->button != ogfx::MouseEventData::eButton::Left)
return;
if (isMouseInsideAnyScrollbar())
return;
const Vec2 mousePos { event.mouse->position_x, event.mouse->position_y };
const Vec2 origin = getGlobalBounds().getPosition();
const Vec2 scroll = getScrollOffset();
const f32 localY = mousePos.y - origin.y - scroll.y;
f32 y = 0;
for (auto& item : m_list)
{
drawScrollbars(gfx);
}
void ListView::onMouseReleased(const Event& event)
{
if (!isMouseInside())
return;
if (event.mouse->button != ogfx::MouseEventData::eButton::Left)
return;
if (isMouseInsideAnyScrollbar())
return;
const Vec2 mousePos { event.mouse->position_x, event.mouse->position_y };
const Vec2 origin = getGlobalBounds().getPosition();
const Vec2 scroll = getScrollOffset();
const f32 localY = mousePos.y - origin.y - scroll.y;
f32 y = 0;
for (auto& item : m_list)
if (!item.isValid()) continue;
const f32 itemH = item.getDimensions().y;
if (localY >= y && localY < y + itemH)
{
if (!item.isValid()) continue;
const f32 itemH = item.getDimensions().y;
if (localY >= y && localY < y + itemH)
{
bool wasSelected = item.isSelected();
item.set_selected(m_selectedList);
if (!wasSelected && callback_onSelectionChanged)
callback_onSelectionChanged(m_selectedList);
event.handle();
break;
}
y += itemH;
bool wasSelected = item.isSelected();
item.set_selected(m_selectedList);
if (!wasSelected && callback_onSelectionChanged)
callback_onSelectionChanged(m_selectedList);
event.handle();
break;
}
y += itemH;
}
}
Rectangle ListView::getContentExtents(void) const
{
Rectangle ListView::getContentExtents(void) const
{
if (!m_extentsDirty)
return m_cachedExtents;
f32 maxX = 0, maxY = 0;
for (auto& item : m_list)
{
if (!item.isValid()) continue;
Vec2 size = item.getDimensions();
maxX = std::max(maxX, size.x);
maxY += size.y;
}
m_cachedExtents = { 0, 0, maxX, maxY };
m_extentsDirty = false;
if (!m_extentsDirty)
return m_cachedExtents;
}
ListView::Item& ListView::getLine(const String& text)
f32 maxX = 0, maxY = 0;
for (auto& item : m_list)
{
for (auto& item : m_list)
if (item.getText() == text)
return item;
return InvalidItem;
if (!item.isValid()) continue;
Vec2 size = item.getDimensions();
maxX = std::max(maxX, size.x);
maxY += size.y;
}
m_cachedExtents = { 0, 0, maxX, maxY };
m_extentsDirty = false;
return m_cachedExtents;
}
ListView::Item& ListView::getLine(u32 index)
{
if (index < m_list.size())
return m_list[index];
return InvalidItem;
}
ListView::Item& ListView::getLine(const String& text)
{
for (auto& item : m_list)
if (item.getText() == text)
return item;
return InvalidItem;
}
ListView::Item& ListView::addLine(const String& text)
{
Item item { *this };
item.setTextColor(m_defaultLinetextColor);
item.setPadding(m_defaultLinepadding);
item.setFontSize(m_defaultLinefontSize);
item.setSelectedColor(m_defaultSelectionColor);
item.setSelectedTextColor(m_defaultSelectionTextColor);
item.setText(text);
m_list.push_back(item);
if (m_list.size() > 0)
m_list[0].set_selected(m_selectedList);
m_extentsDirty = true;
return m_list.back();
}
ListView::Item& ListView::getLine(u32 index)
{
if (index < m_list.size())
return m_list[index];
return InvalidItem;
}
bool ListView::removeLine(const String& text)
{
auto it = std::find_if(m_list.begin(), m_list.end(), [&](const Item& item) {
return item.getText() == text;
});
if (it != m_list.end())
{
m_list.erase(it);
return true;
}
return false;
}
ListView::Item& ListView::addLine(const String& text)
{
Item item { *this };
item.setTextColor(m_defaultLinetextColor);
item.setPadding(m_defaultLinepadding);
item.setFontSize(m_defaultLinefontSize);
item.setSelectedColor(m_defaultSelectionColor);
item.setSelectedTextColor(m_defaultSelectionTextColor);
item.setText(text);
m_list.push_back(item);
if (m_list.size() > 0)
m_list[0].set_selected(m_selectedList);
m_extentsDirty = true;
return m_list.back();
}
bool ListView::removeLine(u32 index)
bool ListView::removeLine(const String& text)
{
auto it = std::find_if(m_list.begin(), m_list.end(), [&](const Item& item) {
return item.getText() == text;
});
if (it != m_list.end())
{
if (!hasLine(index))
return false;
m_list.erase(m_list.begin() + index);
m_list.erase(it);
return true;
}
return false;
}
bool ListView::hasLine(const String& text)
{
for (auto& item : m_list)
if (item.getText() == text)
return true;
bool ListView::removeLine(u32 index)
{
if (!hasLine(index))
return false;
}
m_list.erase(m_list.begin() + index);
return true;
}
bool ListView::hasLine(u32 index)
{
return m_list.size() > index;
}
bool ListView::hasLine(const String& text)
{
for (auto& item : m_list)
if (item.getText() == text)
return true;
return false;
}
bool ListView::hasLine(u32 index)
{
return m_list.size() > index;
}
}
}

View file

@ -28,92 +28,89 @@ namespace ogfx
{
namespace gui
{
namespace widgets
class ListView : public ScrollableWidget
{
class ListView : public ScrollableWidget
public: class Item : public ostd::__i_stringeable
{
public: class Item : public ostd::__i_stringeable
{
public:
inline Item(ListView& parent) { m_parent = &parent; }
void setText(const String& text);
void setFontSize(i32 fontSize);
inline String toString(void) const override { return m_text; }
inline bool isValid(void) const { return m_parent != nullptr && m_text.new_trim() != ""; }
inline Vec2 getDimensions(void) const { return m_dimensions; }
inline i32 getFontSize(void) const { return m_fontSize; }
inline String getText(void) const { return m_text; }
inline bool isSelected(void) const { return m_selected; }
OSTD_PARAM_GETSET(Color, TextColor, m_textColor);
OSTD_PARAM_GETSET(Rectangle, Padding, m_padding);
OSTD_PARAM_GETSET(Color, SelectedTextColor, m_selectedTextColor);
OSTD_PARAM_GETSET(Color, SelectedColor, m_selectedColor);
private:
void update_dimensions(void);
void set_selected(stdvec<Item*>& selectionList);
void add_selected(stdvec<Item*>& selectionList);
void remove_selected(stdvec<Item*>& selectionList);
private:
Vec2 m_dimensions { 0, 0 };
bool m_selected { false };
ListView* m_parent { nullptr };
String m_text { "" };
i32 m_fontSize { 16 };
Color m_textColor { 30, 30, 30 };
Color m_selectedTextColor { Colors::White };
Color m_selectedColor { Colors::Crimson };
Rectangle m_padding { 5, 5, 20, 0 };
friend class ListView;
};
public:
inline ListView(WindowCore& window) : ScrollableWidget(window) { create(); }
ListView& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseReleased(const Event& event) override;
Rectangle getContentExtents(void) const override;
Item& getLine(const String& text);
Item& getLine(u32 index);
Item& addLine(const String& text);
bool removeLine(const String& text);
bool removeLine(u32 index);
bool hasLine(const String& text);
bool hasLine(u32 index);
inline void addLine(const Item& line) { m_list.push_back(line); if (m_list.size() == 1) m_list.back().set_selected(m_selectedList); }
inline stdvec<Item*>& getSelection(void) { return m_selectedList; }
inline void setSelectionChangedCallback(std::function<void(stdvec<Item*>& selection)> callback) { callback_onSelectionChanged = std::move(callback); }
inline Item(ListView& parent) { m_parent = &parent; }
void setText(const String& text);
void setFontSize(i32 fontSize);
OSTD_PARAM_GETSET(Color, SeparatorLineColor, m_lineColor);
OSTD_BOOL_PARAM_GETSET_E(ShowSeparatorLine, m_showLine);
OSTD_PARAM_GETSET(i32, DefaultLineFontSize, m_defaultLinefontSize);
OSTD_PARAM_GETSET(Color, DefaultLineTextColor, m_defaultLinetextColor);
OSTD_PARAM_GETSET(Rectangle, DefaultLinePadding, m_defaultLinepadding);
OSTD_PARAM_GETSET(Color, DefaultLineSelectionColor, m_defaultSelectionColor);
OSTD_PARAM_GETSET(Color, DefaultLineSelectionTextColor, m_defaultSelectionTextColor);
inline String toString(void) const override { return m_text; }
inline bool isValid(void) const { return m_parent != nullptr && m_text.new_trim() != ""; }
inline Vec2 getDimensions(void) const { return m_dimensions; }
inline i32 getFontSize(void) const { return m_fontSize; }
inline String getText(void) const { return m_text; }
inline bool isSelected(void) const { return m_selected; }
OSTD_PARAM_GETSET(Color, TextColor, m_textColor);
OSTD_PARAM_GETSET(Rectangle, Padding, m_padding);
OSTD_PARAM_GETSET(Color, SelectedTextColor, m_selectedTextColor);
OSTD_PARAM_GETSET(Color, SelectedColor, m_selectedColor);
private:
Item InvalidItem { *this };
std::deque<Item> m_list;
stdvec<Item*> m_selectedList;
std::function<void(stdvec<Item*>& selection)> callback_onSelectionChanged { nullptr };
mutable Rectangle m_cachedExtents { 0, 0, 0, 0 };
mutable bool m_extentsDirty { true };
void update_dimensions(void);
void set_selected(stdvec<Item*>& selectionList);
void add_selected(stdvec<Item*>& selectionList);
void remove_selected(stdvec<Item*>& selectionList);
Color m_lineColor { 40, 40, 40 };
bool m_showLine { true };
i32 m_defaultLinefontSize { 16 };
Color m_defaultLinetextColor { 30, 30, 30 };
Rectangle m_defaultLinepadding { 5, 5, 20, 0 };
Color m_defaultSelectionColor { Colors::Crimson };
Color m_defaultSelectionTextColor { Colors::White };
private:
Vec2 m_dimensions { 0, 0 };
bool m_selected { false };
ListView* m_parent { nullptr };
String m_text { "" };
i32 m_fontSize { 16 };
Color m_textColor { 30, 30, 30 };
Color m_selectedTextColor { Colors::White };
Color m_selectedColor { Colors::Crimson };
Rectangle m_padding { 5, 5, 20, 0 };
friend class ListView;
};
}
public:
inline ListView(WindowCore& window) : ScrollableWidget(window) { create(); }
ListView& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseReleased(const Event& event) override;
Rectangle getContentExtents(void) const override;
Item& getLine(const String& text);
Item& getLine(u32 index);
Item& addLine(const String& text);
bool removeLine(const String& text);
bool removeLine(u32 index);
bool hasLine(const String& text);
bool hasLine(u32 index);
inline void addLine(const Item& line) { m_list.push_back(line); if (m_list.size() == 1) m_list.back().set_selected(m_selectedList); }
inline stdvec<Item*>& getSelection(void) { return m_selectedList; }
inline void setSelectionChangedCallback(std::function<void(stdvec<Item*>& selection)> callback) { callback_onSelectionChanged = std::move(callback); }
OSTD_PARAM_GETSET(Color, SeparatorLineColor, m_lineColor);
OSTD_BOOL_PARAM_GETSET_E(ShowSeparatorLine, m_showLine);
OSTD_PARAM_GETSET(i32, DefaultLineFontSize, m_defaultLinefontSize);
OSTD_PARAM_GETSET(Color, DefaultLineTextColor, m_defaultLinetextColor);
OSTD_PARAM_GETSET(Rectangle, DefaultLinePadding, m_defaultLinepadding);
OSTD_PARAM_GETSET(Color, DefaultLineSelectionColor, m_defaultSelectionColor);
OSTD_PARAM_GETSET(Color, DefaultLineSelectionTextColor, m_defaultSelectionTextColor);
private:
Item InvalidItem { *this };
std::deque<Item> m_list;
stdvec<Item*> m_selectedList;
std::function<void(stdvec<Item*>& selection)> callback_onSelectionChanged { nullptr };
mutable Rectangle m_cachedExtents { 0, 0, 0, 0 };
mutable bool m_extentsDirty { true };
Color m_lineColor { 40, 40, 40 };
bool m_showLine { true };
i32 m_defaultLinefontSize { 16 };
Color m_defaultLinetextColor { 30, 30, 30 };
Rectangle m_defaultLinepadding { 5, 5, 20, 0 };
Color m_defaultSelectionColor { Colors::Crimson };
Color m_defaultSelectionTextColor { Colors::White };
};
}
}

View file

@ -25,73 +25,70 @@ namespace ogfx
{
namespace gui
{
namespace widgets
ProgressBar& ProgressBar::create(void)
{
ProgressBar& ProgressBar::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::ProgressBar");
disableChildren();
enableBackground();
enableBorder();
setStylesheetCategoryName("progressbar");
validate();
return *this;
}
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::ProgressBar");
disableChildren();
enableBackground();
enableBorder();
setStylesheetCategoryName("progressbar");
validate();
return *this;
}
void ProgressBar::applyTheme(const ostd::Stylesheet& theme)
{
setProgressColor(getThemeValue<Color>(theme, "progressColor", getProgressColor()));
enableShowDecimal(getThemeValue<bool>(theme, "showDecimal", isShowDecimalEnabled()));
enableShowText(getThemeValue<bool>(theme, "showText", isShowTextEnabled()));
enableProgressGradient(getThemeValue<bool>(theme, "useProgressGradient", isProgressGradientEnabled()));
setProgressGradient(getThemeValue<ColorGradient>(theme, "progressGradient", getProgressGradient()));
}
void ProgressBar::applyTheme(const ostd::Stylesheet& theme)
{
setProgressColor(getThemeValue<Color>(theme, "progressColor", getProgressColor()));
enableShowDecimal(getThemeValue<bool>(theme, "showDecimal", isShowDecimalEnabled()));
enableShowText(getThemeValue<bool>(theme, "showText", isShowTextEnabled()));
enableProgressGradient(getThemeValue<bool>(theme, "useProgressGradient", isProgressGradientEnabled()));
setProgressGradient(getThemeValue<ColorGradient>(theme, "progressGradient", getProgressGradient()));
}
void ProgressBar::onDraw(ogfx::BasicRenderer2D& gfx)
void ProgressBar::onDraw(ogfx::BasicRenderer2D& gfx)
{
f32 prog = getProgressNormalized();
f32 progressw = (prog * getw());
if (isProgressGradientEnabled())
gfx.fillGradientRect({ getGlobalPosition(), { progressw, geth() } }, m_progressGradient);
else
gfx.fillRect({ getGlobalPosition(), { progressw, geth() } }, getProgressColor());
if (isShowTextEnabled())
{
f32 prog = getProgressNormalized();
f32 progressw = (prog * getw());
if (isProgressGradientEnabled())
gfx.fillGradientRect({ getGlobalPosition(), { progressw, geth() } }, m_progressGradient);
String text = "";
if (isShowDecimalEnabled())
text.add(prog * 100, 2).add("%");
else
gfx.fillRect({ getGlobalPosition(), { progressw, geth() } }, getProgressColor());
if (isShowTextEnabled())
{
String text = "";
if (isShowDecimalEnabled())
text.add(prog * 100, 2).add("%");
else
text.add(cast<i32>(std::round(prog * 100))).add("%");
gfx.drawCenteredString(text, getGlobalBounds(), getTextColor(), getFontSize());
}
text.add(cast<i32>(std::round(prog * 100))).add("%");
gfx.drawCenteredString(text, getGlobalBounds(), getTextColor(), getFontSize());
}
}
void ProgressBar::setProgressNormalized(f32 normalized_value)
{
normalized_value = std::clamp(normalized_value, 0.0f, 1.0f);
f32 next = m_min + normalized_value * (m_max - m_min);
m_progress.store(next, std::memory_order_relaxed);
}
void ProgressBar::setProgressNormalized(f32 normalized_value)
{
normalized_value = std::clamp(normalized_value, 0.0f, 1.0f);
f32 next = m_min + normalized_value * (m_max - m_min);
m_progress.store(next, std::memory_order_relaxed);
}
void ProgressBar::setProgress(f32 value)
{
f32 current = m_progress.load(std::memory_order_relaxed);
f32 next = current;
next = std::clamp(value, m_min, m_max);
m_progress.store(next, std::memory_order_relaxed);
}
void ProgressBar::setProgress(f32 value)
{
f32 current = m_progress.load(std::memory_order_relaxed);
f32 next = current;
next = std::clamp(value, m_min, m_max);
m_progress.store(next, std::memory_order_relaxed);
}
f32 ProgressBar::getProgress(void) const
{
return m_progress.load(std::memory_order_relaxed);
}
f32 ProgressBar::getProgress(void) const
{
return m_progress.load(std::memory_order_relaxed);
}
f32 ProgressBar::getProgressNormalized(void) const
{
f32 s = m_progress.load(std::memory_order_relaxed);
return (s - m_min) / (m_max - m_min);
}
f32 ProgressBar::getProgressNormalized(void) const
{
f32 s = m_progress.load(std::memory_order_relaxed);
return (s - m_min) / (m_max - m_min);
}
}
}

View file

@ -27,38 +27,35 @@ namespace ogfx
{
namespace gui
{
namespace widgets
class ProgressBar : public Widget
{
class ProgressBar : public Widget
{
public:
inline ProgressBar(WindowCore& window, f32 min = 0.0f, f32 max = 1.0f, f32 start = 0.0f) : Widget({ 0, 0, 0, 0 }, window), m_progress(std::clamp(start, min, max)), m_min(min), m_max(max) { create(); }
ProgressBar& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void setProgressNormalized(f32 normalized_value);
void setProgress(f32 value);
f32 getProgress(void) const;
f32 getProgressNormalized(void) const;
OSTD_PARAM_GETSET(Color, ProgressColor, m_progressColor);
OSTD_BOOL_PARAM_GETSET_E(ShowDecimal, m_showDecimal);
OSTD_BOOL_PARAM_GETSET_E(ShowText, m_showText);
OSTD_BOOL_PARAM_GETSET_E(ProgressGradient, m_useProgressGradient);
OSTD_PARAM_GETSET(ColorGradient, ProgressGradient, m_progressGradient);
public:
inline ProgressBar(WindowCore& window, f32 min = 0.0f, f32 max = 1.0f, f32 start = 0.0f) : Widget({ 0, 0, 0, 0 }, window), m_progress(std::clamp(start, min, max)), m_min(min), m_max(max) { create(); }
ProgressBar& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void setProgressNormalized(f32 normalized_value);
void setProgress(f32 value);
f32 getProgress(void) const;
f32 getProgressNormalized(void) const;
OSTD_PARAM_GETSET(Color, ProgressColor, m_progressColor);
OSTD_BOOL_PARAM_GETSET_E(ShowDecimal, m_showDecimal);
OSTD_BOOL_PARAM_GETSET_E(ShowText, m_showText);
OSTD_BOOL_PARAM_GETSET_E(ProgressGradient, m_useProgressGradient);
OSTD_PARAM_GETSET(ColorGradient, ProgressGradient, m_progressGradient);
private:
Color m_progressColor { 255, 255, 255 };
f32 m_min { 0.0f };
f32 m_max { 1.0f };
std::atomic<f32> m_progress;
bool m_showDecimal { false };
bool m_showText { true };
bool m_useProgressGradient { true };
ColorGradient m_progressGradient {
{ Colors::Crimson.darkened(0.1f), Colors::Crimson.darkened(0.35f) },
{ 1.0f }
};
};
}
private:
Color m_progressColor { 255, 255, 255 };
f32 m_min { 0.0f };
f32 m_max { 1.0f };
std::atomic<f32> m_progress;
bool m_showDecimal { false };
bool m_showText { true };
bool m_useProgressGradient { true };
ColorGradient m_progressGradient {
{ Colors::Crimson.darkened(0.1f), Colors::Crimson.darkened(0.35f) },
{ 1.0f }
};
};
}
}

View file

@ -25,123 +25,120 @@ namespace ogfx
{
namespace gui
{
namespace widgets
RadioButton& RadioButton::create(RadioButtonGroup& group, const String& text)
{
RadioButton& RadioButton::create(RadioButtonGroup& group, const String& text)
{
m_radioGroup = &group;
setText(text);
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::RadioButton");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("radioButton");
validate();
return *this;
}
m_radioGroup = &group;
setText(text);
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::RadioButton");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("radioButton");
validate();
return *this;
}
void RadioButton::applyTheme(const ostd::Stylesheet& theme)
{
setInnerCircleColor(getThemeValue<Color>(theme, "innerCircleColor", m_innerCircleColor));
setOuterCircleColor(getThemeValue<Color>(theme, "outerCircleColor", m_outerCircleColor));
m_outerCircleBorderWidth = getThemeValue<i32>(theme, "outerCircleBorderWidth", m_outerCircleBorderWidth);
}
void RadioButton::applyTheme(const ostd::Stylesheet& theme)
{
setInnerCircleColor(getThemeValue<Color>(theme, "innerCircleColor", m_innerCircleColor));
setOuterCircleColor(getThemeValue<Color>(theme, "outerCircleColor", m_outerCircleColor));
m_outerCircleBorderWidth = getThemeValue<i32>(theme, "outerCircleBorderWidth", m_outerCircleBorderWidth);
}
void RadioButton::onMouseReleased(const Event& event)
void RadioButton::onMouseReleased(const Event& event)
{
if (!isMouseInside())
return;
if (m_radioGroup == nullptr)
return;
m_radioGroup->set_selected(*this);
}
void RadioButton::onDraw(ogfx::BasicRenderer2D& gfx)
{
if (m_textChanged)
__update_size(gfx);
gfx.drawCircle({ getGlobalContentPosition() + Vec2 { 0, 4 }, m_circleSize }, getOuterCircleColor(), getOuterCircleBorderWidth());
if (isSelected())
gfx.fillCircle({ getGlobalContentPosition() + Vec2 { 3, 7 }, m_circleSize - 6 }, getInnerCircleColor());
gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_circleSize.x + m_spacing, 0 }, getTextColor(), getFontSize());
}
void RadioButton::setText(const String& text)
{
m_text = text;
m_textChanged = true;
}
void RadioButton::__update_size(ogfx::BasicRenderer2D& gfx)
{
auto size = gfx.getStringDimensions(getText(), getFontSize());
m_circleSize = { cast<f32>(size.y - 6), cast<f32>(size.y - 6) };
size.x += m_spacing + m_circleSize.x;
size.x += getPadding().left();
size.x += getPadding().right();
size.y += getPadding().top();
size.y += getPadding().bottom();
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
m_textChanged = false;
}
void RadioButton::__set_selected(bool selected)
{
if (m_selected == selected)
return;
m_selected = selected;
setThemeQualifier("active", m_selected);
}
RadioButtonGroup::RadioButtonGroup(void)
{
}
RadioButton& RadioButtonGroup::addButton(Widget& parent, const String& text, const Vec2& position)
{
m_buttons.push_back(std::unique_ptr<RadioButton>(
new RadioButton(parent.getWindow(), *this, text)
));
auto& btn = *m_buttons.back();
parent.addWidget(btn, position);
if (m_buttons.size() == 1)
set_selected(btn);
return btn;
}
void RadioButtonGroup::set_selected(RadioButton& sender)
{
if (is_button_present(sender))
{
if (!isMouseInside())
if (m_selected != nullptr && m_selected == &sender)
return;
if (m_radioGroup == nullptr)
return;
m_radioGroup->set_selected(*this);
unselect_all();
auto previous = m_selected;
m_selected = &sender;
m_selected->__set_selected(true);
if (previous != nullptr && callback_onSelectionChanged != nullptr)
callback_onSelectionChanged(*previous, sender);
return;
}
}
void RadioButton::onDraw(ogfx::BasicRenderer2D& gfx)
{
if (m_textChanged)
__update_size(gfx);
gfx.drawCircle({ getGlobalContentPosition() + Vec2 { 0, 4 }, m_circleSize }, getOuterCircleColor(), getOuterCircleBorderWidth());
if (isSelected())
gfx.fillCircle({ getGlobalContentPosition() + Vec2 { 3, 7 }, m_circleSize - 6 }, getInnerCircleColor());
gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_circleSize.x + m_spacing, 0 }, getTextColor(), getFontSize());
}
bool RadioButtonGroup::is_button_present(RadioButton& button)
{
for (auto& btn : m_buttons)
if (btn.get() == &button)
return true;
return false;
}
void RadioButton::setText(const String& text)
{
m_text = text;
m_textChanged = true;
}
void RadioButton::__update_size(ogfx::BasicRenderer2D& gfx)
{
auto size = gfx.getStringDimensions(getText(), getFontSize());
m_circleSize = { cast<f32>(size.y - 6), cast<f32>(size.y - 6) };
size.x += m_spacing + m_circleSize.x;
size.x += getPadding().left();
size.x += getPadding().right();
size.y += getPadding().top();
size.y += getPadding().bottom();
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
m_textChanged = false;
}
void RadioButton::__set_selected(bool selected)
{
if (m_selected == selected)
return;
m_selected = selected;
setThemeQualifier("active", m_selected);
}
RadioButtonGroup::RadioButtonGroup(void)
{
}
RadioButton& RadioButtonGroup::addButton(Widget& parent, const String& text, const Vec2& position)
{
m_buttons.push_back(std::unique_ptr<RadioButton>(
new RadioButton(parent.getWindow(), *this, text)
));
auto& btn = *m_buttons.back();
parent.addWidget(btn, position);
if (m_buttons.size() == 1)
set_selected(btn);
return btn;
}
void RadioButtonGroup::set_selected(RadioButton& sender)
{
if (is_button_present(sender))
{
if (m_selected != nullptr && m_selected == &sender)
return;
unselect_all();
auto previous = m_selected;
m_selected = &sender;
m_selected->__set_selected(true);
if (previous != nullptr && callback_onSelectionChanged != nullptr)
callback_onSelectionChanged(*previous, sender);
return;
}
}
bool RadioButtonGroup::is_button_present(RadioButton& button)
{
for (auto& btn : m_buttons)
if (btn.get() == &button)
return true;
return false;
}
void RadioButtonGroup::unselect_all(void)
{
for (auto& btn : m_buttons)
btn->__set_selected(false);
}
void RadioButtonGroup::unselect_all(void)
{
for (auto& btn : m_buttons)
btn->__set_selected(false);
}
}
}

View file

@ -26,60 +26,57 @@ namespace ogfx
{
namespace gui
{
namespace widgets
class RadioButtonGroup;
class RadioButton : public Widget
{
class RadioButtonGroup;
class RadioButton : public Widget
{
public:
void applyTheme(const ostd::Stylesheet& theme) override;
void onMouseReleased(const Event& event) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void setText(const String& text);
inline String getText(void) const { return m_text; }
inline bool isSelected(void) const { return m_selected; }
OSTD_PARAM_GETSET(i32, OuterCircleBorderWidth, m_outerCircleBorderWidth);
OSTD_PARAM_GETSET(Color, InnerCircleColor, m_innerCircleColor);
OSTD_PARAM_GETSET(Color, OuterCircleColor, m_outerCircleColor);
public:
void applyTheme(const ostd::Stylesheet& theme) override;
void onMouseReleased(const Event& event) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void setText(const String& text);
inline String getText(void) const { return m_text; }
inline bool isSelected(void) const { return m_selected; }
OSTD_PARAM_GETSET(i32, OuterCircleBorderWidth, m_outerCircleBorderWidth);
OSTD_PARAM_GETSET(Color, InnerCircleColor, m_innerCircleColor);
OSTD_PARAM_GETSET(Color, OuterCircleColor, m_outerCircleColor);
private:
inline RadioButton(WindowCore& window, RadioButtonGroup& group, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(group, text); }
RadioButton& create(RadioButtonGroup& group, const String& text);
void __update_size(ogfx::BasicRenderer2D& gfx);
void __set_selected(bool selected);
private:
inline RadioButton(WindowCore& window, RadioButtonGroup& group, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(group, text); }
RadioButton& create(RadioButtonGroup& group, const String& text);
void __update_size(ogfx::BasicRenderer2D& gfx);
void __set_selected(bool selected);
private:
RadioButtonGroup* m_radioGroup { nullptr };
Vec2 m_circleSize { 0, 0 };
bool m_selected { false };
String m_text { "" };
bool m_textChanged { false };
f32 m_spacing { 6 };
i32 m_outerCircleBorderWidth { 1 };
Color m_innerCircleColor { 255, 255, 255 };
Color m_outerCircleColor { 255, 255, 255 };
private:
RadioButtonGroup* m_radioGroup { nullptr };
Vec2 m_circleSize { 0, 0 };
bool m_selected { false };
String m_text { "" };
bool m_textChanged { false };
f32 m_spacing { 6 };
i32 m_outerCircleBorderWidth { 1 };
Color m_innerCircleColor { 255, 255, 255 };
Color m_outerCircleColor { 255, 255, 255 };
friend class RadioButtonGroup;
};
class RadioButtonGroup
{
public:
RadioButtonGroup(void);
RadioButton& addButton(Widget& parent, const String& text, const Vec2& position = { 0, 0 });
inline void setSelectionChangedCallback(std::function<void(RadioButton& previous, RadioButton& sender)> callback) { callback_onSelectionChanged = std::move(callback); }
friend class RadioButtonGroup;
};
class RadioButtonGroup
{
public:
RadioButtonGroup(void);
RadioButton& addButton(Widget& parent, const String& text, const Vec2& position = { 0, 0 });
inline void setSelectionChangedCallback(std::function<void(RadioButton& previous, RadioButton& sender)> callback) { callback_onSelectionChanged = std::move(callback); }
private:
void set_selected(RadioButton& sender);
bool is_button_present(RadioButton& button);
void unselect_all(void);
private:
void set_selected(RadioButton& sender);
bool is_button_present(RadioButton& button);
void unselect_all(void);
private:
stdvec<std::unique_ptr<RadioButton>> m_buttons;
RadioButton* m_selected { nullptr };
std::function<void(RadioButton& previous, RadioButton& sender)> callback_onSelectionChanged { nullptr };
private:
stdvec<std::unique_ptr<RadioButton>> m_buttons;
RadioButton* m_selected { nullptr };
std::function<void(RadioButton& previous, RadioButton& sender)> callback_onSelectionChanged { nullptr };
friend class RadioButton;
};
}
friend class RadioButton;
};
}
}

View file

@ -26,43 +26,40 @@ namespace ogfx
{
namespace gui
{
namespace widgets
RootWidget::RootWidget(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window)
{
RootWidget::RootWidget(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window)
{
setRootChild();
setSize(cast<f32>(window.getWindowWidth()), cast<f32>(window.getWindowHeight()));
setStylesheetCategoryName("window");
setTypeName("ogfx::gui::widgets::RootWidget");
}
setRootChild();
setSize(cast<f32>(window.getWindowWidth()), cast<f32>(window.getWindowHeight()));
setStylesheetCategoryName("window");
setTypeName("ogfx::gui::RootWidget");
}
void RootWidget::onWindowResized(const Event& event)
{
Window& win = cast<Window&>(getWindow()); //TODO: Potentially unsage?
f32 offset_y = 0;
if (win.m_menubar.isVisible())
offset_y += win.m_menubar.geth();
if (win.m_toolbar.isVisible())
offset_y += win.m_toolbar.geth();
sety(offset_y);
if (win.m_statusbar.isVisible())
offset_y += win.m_statusbar.geth();
setSize(cast<f32>(event.windowResized->new_width), cast<f32>(event.windowResized->new_height) - offset_y);
}
void RootWidget::onWindowResized(const Event& event)
{
Window& win = cast<Window&>(getWindow()); //TODO: Potentially unsage?
f32 offset_y = 0;
if (win.m_menubar.isVisible())
offset_y += win.m_menubar.geth();
if (win.m_toolbar.isVisible())
offset_y += win.m_toolbar.geth();
sety(offset_y);
if (win.m_statusbar.isVisible())
offset_y += win.m_statusbar.geth();
setSize(cast<f32>(event.windowResized->new_width), cast<f32>(event.windowResized->new_height) - offset_y);
}
void RootWidget::applyTheme(const ostd::Stylesheet& theme)
{
setTooltipBackgroundColor(getThemeValue<Color>(theme, "tooltip.backgroundColor", getTooltipBackgroundColor()));
setTooltipBorderColor(getThemeValue<Color>(theme, "tooltip.borderColor", getTooltipBorderColor()));
setTooltipTextColor(getThemeValue<Color>(theme, "tooltip.textColor", getTooltipTextColor()));
setTooltipBorderWidth(getThemeValue<i32>(theme, "tooltip.borderWidth", getTooltipBorderWidth()));
setTooltipFontSize(getThemeValue<i32>(theme, "tooltip.fontSize", getTooltipFontSize()));
}
void RootWidget::applyTheme(const ostd::Stylesheet& theme)
{
setTooltipBackgroundColor(getThemeValue<Color>(theme, "tooltip.backgroundColor", getTooltipBackgroundColor()));
setTooltipBorderColor(getThemeValue<Color>(theme, "tooltip.borderColor", getTooltipBorderColor()));
setTooltipTextColor(getThemeValue<Color>(theme, "tooltip.textColor", getTooltipTextColor()));
setTooltipBorderWidth(getThemeValue<i32>(theme, "tooltip.borderWidth", getTooltipBorderWidth()));
setTooltipFontSize(getThemeValue<i32>(theme, "tooltip.fontSize", getTooltipFontSize()));
}
void RootWidget::onDraw(ogfx::BasicRenderer2D& gfx)
{
gfx.fillRect({ 0, 0, cast<f32>(getWindow().getWindowWidth()), cast<f32>(getWindow().getWindowHeight()) }, getBackgroundColor());
}
void RootWidget::onDraw(ogfx::BasicRenderer2D& gfx)
{
gfx.fillRect({ 0, 0, cast<f32>(getWindow().getWindowWidth()), cast<f32>(getWindow().getWindowHeight()) }, getBackgroundColor());
}
}
}

View file

@ -26,28 +26,25 @@ namespace ogfx
{
namespace gui
{
namespace widgets
class RootWidget : public Widget
{
class RootWidget : public Widget
{
public:
RootWidget(WindowCore& window);
void onWindowResized(const Event& event) override;
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
OSTD_PARAM_GETSET(Color, TooltipBackgroundColor, m_tooltipBackgroundColor);
OSTD_PARAM_GETSET(Color, TooltipBorderColor, m_tooltipBorderColor);
OSTD_PARAM_GETSET(Color, TooltipTextColor, m_tooltipTextColor);
OSTD_PARAM_GETSET(i32, TooltipBorderWidth, m_tooltipBorderWidth);
OSTD_PARAM_GETSET(i32, TooltipFontSize, m_tooltipFontSize);
public:
RootWidget(WindowCore& window);
void onWindowResized(const Event& event) override;
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
OSTD_PARAM_GETSET(Color, TooltipBackgroundColor, m_tooltipBackgroundColor);
OSTD_PARAM_GETSET(Color, TooltipBorderColor, m_tooltipBorderColor);
OSTD_PARAM_GETSET(Color, TooltipTextColor, m_tooltipTextColor);
OSTD_PARAM_GETSET(i32, TooltipBorderWidth, m_tooltipBorderWidth);
OSTD_PARAM_GETSET(i32, TooltipFontSize, m_tooltipFontSize);
private:
Color m_tooltipBackgroundColor { "#FFF7D6FF" };
Color m_tooltipBorderColor { 50, 50, 50 };
Color m_tooltipTextColor { 50, 50, 50 };
i32 m_tooltipBorderWidth { 1 };
i32 m_tooltipFontSize { 20 };
};
}
private:
Color m_tooltipBackgroundColor { "#FFF7D6FF" };
Color m_tooltipBorderColor { 50, 50, 50 };
Color m_tooltipTextColor { 50, 50, 50 };
i32 m_tooltipBorderWidth { 1 };
i32 m_tooltipFontSize { 20 };
};
}
}

View file

@ -25,443 +25,440 @@ namespace ogfx
{
namespace gui
{
namespace widgets
VerticalScrollBar& VerticalScrollBar::create(void)
{
VerticalScrollBar& VerticalScrollBar::create(void)
setPadding({ 0, 0, 0, 0 });
setMargin({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::Label");
disableChildren();
enableBackground(true);
enableBorder(false);
enableTopMost(true);
enableIgnoreScroll(true);
setStylesheetCategoryName("scrollbar");
validate();
return *this;
}
void VerticalScrollBar::applyTheme(const ostd::Stylesheet& theme)
{
w = getThemeValue<f32>(theme, "width", 15);
m_thumbColor = getThemeValue<Color>(theme, "thumb.color", { 120, 120, 120 });
m_thumbBorderRadius = getThemeValue<f32>(theme, "thumb.borderRadius", 16);
m_thumbBorderColor = getThemeValue<Color>(theme, "thumb.borderColor", { 150, 150, 150 });
m_thumbShowBorder = getThemeValue<bool>(theme, "thumb.showBorder", true);
m_trackColor = getThemeValue<Color>(theme, "track.color", { 70, 70, 70 });
}
void VerticalScrollBar::afterDraw(ogfx::BasicRenderer2D& gfx)
{
if (!getParent()->needsVScroll())
return;
gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii);
gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1);
}
void VerticalScrollBar::onMouseDragged(const Event& event)
{
if (!m_mousePressed)
return;
if (isMouseInsideThumb({ event.mouse->position_x, event.mouse->position_y }))
m_mouseDragged = true;
if (m_mouseDragged)
{
setPadding({ 0, 0, 0, 0 });
setMargin({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::Label");
disableChildren();
enableBackground(true);
enableBorder(false);
enableTopMost(true);
enableIgnoreScroll(true);
setStylesheetCategoryName("scrollbar");
validate();
return *this;
}
void VerticalScrollBar::applyTheme(const ostd::Stylesheet& theme)
{
w = getThemeValue<f32>(theme, "width", 15);
m_thumbColor = getThemeValue<Color>(theme, "thumb.color", { 120, 120, 120 });
m_thumbBorderRadius = getThemeValue<f32>(theme, "thumb.borderRadius", 16);
m_thumbBorderColor = getThemeValue<Color>(theme, "thumb.borderColor", { 150, 150, 150 });
m_thumbShowBorder = getThemeValue<bool>(theme, "thumb.showBorder", true);
m_trackColor = getThemeValue<Color>(theme, "track.color", { 70, 70, 70 });
}
void VerticalScrollBar::afterDraw(ogfx::BasicRenderer2D& gfx)
{
if (!getParent()->needsVScroll())
return;
gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii);
gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1);
}
void VerticalScrollBar::onMouseDragged(const Event& event)
{
if (!m_mousePressed)
return;
if (isMouseInsideThumb({ event.mouse->position_x, event.mouse->position_y }))
m_mouseDragged = true;
if (m_mouseDragged)
{
auto bounds = getGlobalBounds() - m_correctionOffset;
f32 newThumbY = (event.mouse->position_y - bounds.y) - m_dragGrabOffset;
set_thumb_y(newThumbY);
}
}
void VerticalScrollBar::onMousePressed(const Event& event)
{
m_mousePressed = true;
m_dragGrabOffset = event.mouse->position_y - m_thumbGlobalBounds.y;
}
void VerticalScrollBar::onMouseReleased(const Event& event)
{
m_mousePressed = false;
if (event.mouse->mousePressedOnWidget != this)
return;
if (!m_mouseDragged)
{
auto bounds = getGlobalBounds() - m_correctionOffset;
f32 newThumbY = (event.mouse->position_y - bounds.y);
newThumbY -= m_thumbHeight / 2.0f;
set_thumb_y(newThumbY);
}
m_mouseDragged = false;
}
void VerticalScrollBar::onUpdate(void)
{
update_thumb();
}
bool VerticalScrollBar::isMouseInsideThumb(const Vec2& mouse_pos)
{
return m_thumbGlobalBounds.contains(mouse_pos, true);
}
void VerticalScrollBar::update_thumb(void)
{
x = getParent()->getSize().x - w;
y = 0;
h = getParent()->getGlobalPureContentBounds().h;
auto ext = getParent()->getContentExtents();
auto cont = getParent()->getContentBounds();
auto scrollOfset = getParent()->getScrollOffset();
// Thumb size is proportional to visible/total ratio
f32 visibleRatio = cont.h / ext.h; // e.g. 0.4 means 40% visible
m_thumbHeight = std::max(20.0f, geth() * visibleRatio); // min 20px for usability
// Thumb position is proportional to scroll progress
f32 scrollProgress = -scrollOfset.y / (ext.h - cont.h); // 0 to 1
m_thumbY = gety() + ((geth() - m_thumbHeight) * scrollProgress);
auto bounds = getGlobalBounds() - m_correctionOffset;
m_thumbGlobalBounds = { bounds.x + 2, bounds.y + gety() + m_thumbY, getw() - 4, m_thumbHeight };
f32 newThumbY = (event.mouse->position_y - bounds.y) - m_dragGrabOffset;
set_thumb_y(newThumbY);
}
}
void VerticalScrollBar::set_thumb_y(f32 thumby)
void VerticalScrollBar::onMousePressed(const Event& event)
{
m_mousePressed = true;
m_dragGrabOffset = event.mouse->position_y - m_thumbGlobalBounds.y;
}
void VerticalScrollBar::onMouseReleased(const Event& event)
{
m_mousePressed = false;
if (event.mouse->mousePressedOnWidget != this)
return;
if (!m_mouseDragged)
{
if (!getParent()->needsVScroll())
return;
auto bounds = getGlobalBounds() - m_correctionOffset;
auto ext = getParent()->getContentExtents();
auto cont = getParent()->getContentBounds();
thumby = std::clamp(thumby, 0.0f, h - m_thumbHeight);
m_thumbY = thumby;
f32 scrollProgress = (bounds.y + gety() + m_thumbY - bounds.y) / (bounds.h - m_thumbHeight);
f32 maxScroll = -(ext.h - cont.h);
auto parentScrollOffset = getParent()->getScrollOffset();
parentScrollOffset.y = maxScroll * scrollProgress;
getParent()->setScrollOffset(parentScrollOffset);
f32 newThumbY = (event.mouse->position_y - bounds.y);
newThumbY -= m_thumbHeight / 2.0f;
set_thumb_y(newThumbY);
}
m_mouseDragged = false;
}
void VerticalScrollBar::onUpdate(void)
{
update_thumb();
}
bool VerticalScrollBar::isMouseInsideThumb(const Vec2& mouse_pos)
{
return m_thumbGlobalBounds.contains(mouse_pos, true);
}
void VerticalScrollBar::update_thumb(void)
{
x = getParent()->getSize().x - w;
y = 0;
h = getParent()->getGlobalPureContentBounds().h;
auto ext = getParent()->getContentExtents();
auto cont = getParent()->getContentBounds();
auto scrollOfset = getParent()->getScrollOffset();
// Thumb size is proportional to visible/total ratio
f32 visibleRatio = cont.h / ext.h; // e.g. 0.4 means 40% visible
m_thumbHeight = std::max(20.0f, geth() * visibleRatio); // min 20px for usability
// Thumb position is proportional to scroll progress
f32 scrollProgress = -scrollOfset.y / (ext.h - cont.h); // 0 to 1
m_thumbY = gety() + ((geth() - m_thumbHeight) * scrollProgress);
auto bounds = getGlobalBounds() - m_correctionOffset;
m_thumbGlobalBounds = { bounds.x + 2, bounds.y + gety() + m_thumbY, getw() - 4, m_thumbHeight };
}
void VerticalScrollBar::set_thumb_y(f32 thumby)
{
if (!getParent()->needsVScroll())
return;
auto bounds = getGlobalBounds() - m_correctionOffset;
auto ext = getParent()->getContentExtents();
auto cont = getParent()->getContentBounds();
thumby = std::clamp(thumby, 0.0f, h - m_thumbHeight);
m_thumbY = thumby;
f32 scrollProgress = (bounds.y + gety() + m_thumbY - bounds.y) / (bounds.h - m_thumbHeight);
f32 maxScroll = -(ext.h - cont.h);
auto parentScrollOffset = getParent()->getScrollOffset();
parentScrollOffset.y = maxScroll * scrollProgress;
getParent()->setScrollOffset(parentScrollOffset);
}
HorizontalScrollbar& HorizontalScrollbar::create(void)
HorizontalScrollbar& HorizontalScrollbar::create(void)
{
setPadding({ 0, 0, 0, 0 });
setMargin({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::Label");
disableChildren();
enableBackground(true);
enableBorder(false);
enableTopMost(true);
enableIgnoreScroll(true);
setStylesheetCategoryName("scrollbar");
validate();
return *this;
}
void HorizontalScrollbar::applyTheme(const ostd::Stylesheet& theme)
{
h = getThemeValue<f32>(theme, "width", 15);
m_thumbColor = getThemeValue<Color>(theme, "thumb.color", { 120, 120, 120 });
m_thumbBorderRadius = getThemeValue<f32>(theme, "thumb.borderRadius", 16);
m_thumbBorderColor = getThemeValue<Color>(theme, "thumb.borderColor", { 150, 150, 150 });
m_thumbShowBorder = getThemeValue<bool>(theme, "thumb.showBorder", true);
m_trackColor = getThemeValue<Color>(theme, "track.color", { 70, 70, 70 });
}
void HorizontalScrollbar::afterDraw(ogfx::BasicRenderer2D& gfx)
{
if (!getParent()->needsHScroll())
return;
gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii);
gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1);
}
void HorizontalScrollbar::onMouseDragged(const Event& event)
{
if (!m_mousePressed)
return;
if (isMouseInsideThumb({ event.mouse->position_x, event.mouse->position_y }))
m_mouseDragged = true;
if (m_mouseDragged)
{
setPadding({ 0, 0, 0, 0 });
setMargin({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::Label");
disableChildren();
enableBackground(true);
enableBorder(false);
enableTopMost(true);
enableIgnoreScroll(true);
setStylesheetCategoryName("scrollbar");
validate();
return *this;
}
void HorizontalScrollbar::applyTheme(const ostd::Stylesheet& theme)
{
h = getThemeValue<f32>(theme, "width", 15);
m_thumbColor = getThemeValue<Color>(theme, "thumb.color", { 120, 120, 120 });
m_thumbBorderRadius = getThemeValue<f32>(theme, "thumb.borderRadius", 16);
m_thumbBorderColor = getThemeValue<Color>(theme, "thumb.borderColor", { 150, 150, 150 });
m_thumbShowBorder = getThemeValue<bool>(theme, "thumb.showBorder", true);
m_trackColor = getThemeValue<Color>(theme, "track.color", { 70, 70, 70 });
}
void HorizontalScrollbar::afterDraw(ogfx::BasicRenderer2D& gfx)
{
if (!getParent()->needsHScroll())
return;
gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii);
gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1);
}
void HorizontalScrollbar::onMouseDragged(const Event& event)
{
if (!m_mousePressed)
return;
if (isMouseInsideThumb({ event.mouse->position_x, event.mouse->position_y }))
m_mouseDragged = true;
if (m_mouseDragged)
{
auto bounds = getGlobalBounds() - m_correctionOffset;
f32 newThumbX = (event.mouse->position_x - bounds.x) - m_dragGrabOffset;
set_thumb_x(newThumbX);
}
}
void HorizontalScrollbar::onMousePressed(const Event& event)
{
m_mousePressed = true;
m_dragGrabOffset = event.mouse->position_x - m_thumbGlobalBounds.x;
}
void HorizontalScrollbar::onMouseReleased(const Event& event)
{
m_mousePressed = false;
if (event.mouse->mousePressedOnWidget != this)
return;
if (!m_mouseDragged)
{
auto bounds = getGlobalBounds() - m_correctionOffset;
f32 newThumbX = (event.mouse->position_x - bounds.x);
newThumbX -= m_thumbWidth / 2.0f;
set_thumb_x(newThumbX);
}
m_mouseDragged = false;
}
void HorizontalScrollbar::onUpdate(void)
{
update_thumb();
}
bool HorizontalScrollbar::isMouseInsideThumb(const Vec2& mouse_pos)
{
return m_thumbGlobalBounds.contains(mouse_pos, true);
}
void HorizontalScrollbar::update_thumb(void)
{
x = 0;
y = getParent()->getSize().y - h;
w = getParent()->getGlobalBounds().w - getParent()->getVScrollbarSize();
auto ext = getParent()->getContentExtents();
auto cont = getParent()->getContentBounds();
auto scrollOfset = getParent()->getScrollOffset();
// Thumb size is proportional to visible/total ratio
f32 visibleRatio = cont.w / ext.w; // e.g. 0.4 means 40% visible
m_thumbWidth = std::max(20.0f, getw() * visibleRatio); // min 20px for usability
// Thumb position is proportional to scroll progress
f32 scrollProgress = -scrollOfset.x / (ext.w - cont.w); // 0 to 1
m_thumbX = getx() + ((getw() - m_thumbWidth) * scrollProgress);
auto bounds = getGlobalBounds() - m_correctionOffset;
m_thumbGlobalBounds = { bounds.x + 2 + m_thumbX, bounds.y + gety(), m_thumbWidth, geth() - 4 };
m_thumbGlobalBounds = { bounds.x + getx() + m_thumbX, bounds.y + 2, m_thumbWidth, geth() - 4 };
f32 newThumbX = (event.mouse->position_x - bounds.x) - m_dragGrabOffset;
set_thumb_x(newThumbX);
}
}
void HorizontalScrollbar::set_thumb_x(f32 thumbx)
void HorizontalScrollbar::onMousePressed(const Event& event)
{
m_mousePressed = true;
m_dragGrabOffset = event.mouse->position_x - m_thumbGlobalBounds.x;
}
void HorizontalScrollbar::onMouseReleased(const Event& event)
{
m_mousePressed = false;
if (event.mouse->mousePressedOnWidget != this)
return;
if (!m_mouseDragged)
{
if (!getParent()->needsHScroll())
return;
auto bounds = getGlobalBounds() - m_correctionOffset;
auto ext = getParent()->getContentExtents();
auto cont = getParent()->getContentBounds();
thumbx = std::clamp(thumbx, 0.0f, w - m_thumbWidth);
m_thumbX = thumbx;
f32 scrollProgress = (bounds.x + getx() + m_thumbX - bounds.x) / (bounds.w - m_thumbWidth);
f32 maxScroll = -(ext.w - cont.w);
auto parentScrollOffset = getParent()->getScrollOffset();
parentScrollOffset.x = maxScroll * scrollProgress;
getParent()->setScrollOffset(parentScrollOffset);
f32 newThumbX = (event.mouse->position_x - bounds.x);
newThumbX -= m_thumbWidth / 2.0f;
set_thumb_x(newThumbX);
}
m_mouseDragged = false;
}
void HorizontalScrollbar::onUpdate(void)
{
update_thumb();
}
bool HorizontalScrollbar::isMouseInsideThumb(const Vec2& mouse_pos)
{
return m_thumbGlobalBounds.contains(mouse_pos, true);
}
void HorizontalScrollbar::update_thumb(void)
{
x = 0;
y = getParent()->getSize().y - h;
w = getParent()->getGlobalBounds().w - getParent()->getVScrollbarSize();
auto ext = getParent()->getContentExtents();
auto cont = getParent()->getContentBounds();
auto scrollOfset = getParent()->getScrollOffset();
// Thumb size is proportional to visible/total ratio
f32 visibleRatio = cont.w / ext.w; // e.g. 0.4 means 40% visible
m_thumbWidth = std::max(20.0f, getw() * visibleRatio); // min 20px for usability
// Thumb position is proportional to scroll progress
f32 scrollProgress = -scrollOfset.x / (ext.w - cont.w); // 0 to 1
m_thumbX = getx() + ((getw() - m_thumbWidth) * scrollProgress);
auto bounds = getGlobalBounds() - m_correctionOffset;
m_thumbGlobalBounds = { bounds.x + 2 + m_thumbX, bounds.y + gety(), m_thumbWidth, geth() - 4 };
m_thumbGlobalBounds = { bounds.x + getx() + m_thumbX, bounds.y + 2, m_thumbWidth, geth() - 4 };
}
void HorizontalScrollbar::set_thumb_x(f32 thumbx)
{
if (!getParent()->needsHScroll())
return;
auto bounds = getGlobalBounds() - m_correctionOffset;
auto ext = getParent()->getContentExtents();
auto cont = getParent()->getContentBounds();
thumbx = std::clamp(thumbx, 0.0f, w - m_thumbWidth);
m_thumbX = thumbx;
f32 scrollProgress = (bounds.x + getx() + m_thumbX - bounds.x) / (bounds.w - m_thumbWidth);
f32 maxScroll = -(ext.w - cont.w);
auto parentScrollOffset = getParent()->getScrollOffset();
parentScrollOffset.x = maxScroll * scrollProgress;
getParent()->setScrollOffset(parentScrollOffset);
}
ScrollableWidget& ScrollableWidget::create(void)
{
setTypeName("ogfx::gui::widgets::ScrollableWidget");
enableVScroll(true);
enableHScroll(true);
m_vScrollbar.enableManualDraw(true);
addWidget(m_vScrollbar);
m_hScrollbar.setMargin({ 0, 0, 0, 0 });
m_hScrollbar.enableManualDraw(true);
addWidget(m_hScrollbar);
m_smoothScrollTimer.create(60.0f, [&](f64 dt) -> void {
f32 stepX = m_scrollVelocity.x * (1.0f - m_scrollSmoothFactor);
f32 stepY = m_scrollVelocity.y * (1.0f - m_scrollSmoothFactor);
ScrollableWidget& ScrollableWidget::create(void)
{
setTypeName("ogfx::gui::ScrollableWidget");
enableVScroll(true);
enableHScroll(true);
m_vScrollbar.enableManualDraw(true);
addWidget(m_vScrollbar);
m_hScrollbar.setMargin({ 0, 0, 0, 0 });
m_hScrollbar.enableManualDraw(true);
addWidget(m_hScrollbar);
m_smoothScrollTimer.create(60.0f, [&](f64 dt) -> void {
f32 stepX = m_scrollVelocity.x * (1.0f - m_scrollSmoothFactor);
f32 stepY = m_scrollVelocity.y * (1.0f - m_scrollSmoothFactor);
addScrollOffset({ stepX, stepY });
m_scrollVelocity -= { stepX, stepY };
addScrollOffset({ stepX, stepY });
m_scrollVelocity -= { stepX, stepY };
if (std::abs(m_scrollVelocity.x) < 0.5f) m_scrollVelocity.x = 0;
if (std::abs(m_scrollVelocity.y) < 0.5f) m_scrollVelocity.y = 0;
}, true);
if (std::abs(m_scrollVelocity.x) < 0.5f) m_scrollVelocity.x = 0;
if (std::abs(m_scrollVelocity.y) < 0.5f) m_scrollVelocity.y = 0;
}, true);
m_smoothScrollTimer.setStopCondition([&](void) -> bool {
return m_scrollVelocity.x == 0 && m_scrollVelocity.y == 0;
});
updateScrollbarsSize();
validate();
return *this;
}
m_smoothScrollTimer.setStopCondition([&](void) -> bool {
return m_scrollVelocity.x == 0 && m_scrollVelocity.y == 0;
});
updateScrollbarsSize();
validate();
return *this;
}
void ScrollableWidget::onUpdate(void)
{
m_smoothScrollTimer.update();
if (!needsVScroll() && m_vScrollbarAdded)
{
removeWidget(m_vScrollbar);
m_vScrollbarAdded = false;
}
else if (!m_vScrollbarAdded)
{
addWidget(m_vScrollbar);
m_vScrollbarAdded = true;
}
if (!needsHScroll() && m_hScrollbarAdded)
{
removeWidget(m_hScrollbar);
m_hScrollbarAdded = false;
}
else if (!m_hScrollbarAdded)
{
addWidget(m_hScrollbar);
m_hScrollbarAdded = true;
}
}
void ScrollableWidget::drawScrollbars(ogfx::BasicRenderer2D& gfx)
{
m_vScrollbar.__draw(gfx);
m_hScrollbar.__draw(gfx);
}
void ScrollableWidget::updateScrollbarsSize(void)
{
m_vScrollbar.setMargin({ 0, getPureContentBounds().y, 0, 0 });
}
void ScrollableWidget::resetScroll(bool horizontal, bool vertical, bool propagate)
{
if (vertical)
m_scrollOffset.y = 0;
if (horizontal)
m_scrollOffset.x = 0;
if (propagate)
{
for (auto& w : getChildren().getWidgets())
{
if (w == nullptr) continue;
w->resetScroll(horizontal, vertical, propagate);
}
}
}
bool ScrollableWidget::isMouseInsideVScrollbar(void) const
{
return m_vScrollbar.isMouseInside();
}
bool ScrollableWidget::isMouseInsideHScrollbar(void) const
{
return m_hScrollbar.isMouseInside();
}
void ScrollableWidget::onMouseScrolled(const Event& event)
{
if (isVScrollEnabled())
{
bool mouseInsideHScrollbar = m_hScrollbar.isMouseInside() && needsHScroll();
if (std::abs(event.mouse->scrollAmount.y) > 0 && !mouseInsideHScrollbar)
{
m_scrollVelocity.y += m_scrollSpeed * event.mouse->scrollAmount.y * m_scrollSpeedMultiplier;
if (m_smoothScrollTimer.isStopped())
m_smoothScrollTimer.restart();
event.handle();
}
else if (std::abs(event.mouse->scrollAmount.y) > 0)
{
m_scrollVelocity.x += m_scrollSpeed * event.mouse->scrollAmount.y * m_scrollSpeedMultiplier;
if (m_smoothScrollTimer.isStopped())
m_smoothScrollTimer.restart();
event.handle();
}
}
if (isHScrollEnabled())
{
if (std::abs(event.mouse->scrollAmount.x) > 0)
{
m_scrollVelocity.x += m_scrollSpeed * event.mouse->scrollAmount.x * m_scrollSpeedMultiplier;
if (m_smoothScrollTimer.isStopped())
m_smoothScrollTimer.restart();
event.handle();
}
}
}
void ScrollableWidget::setScrollOffset(const Vec2& offset)
{
auto ext = getContentExtents();
auto cont = getContentBounds();
f32 maxScrollY = -(ext.h - cont.h);
f32 maxScrollX = -(ext.w - cont.w);
m_scrollOffset = offset;
if (m_scrollOffset.y < maxScrollY) m_scrollOffset.y = maxScrollY;
if (m_scrollOffset.y > 0) m_scrollOffset.y = 0;
if (m_scrollOffset.x < maxScrollX) m_scrollOffset.x = maxScrollX;
if (m_scrollOffset.x > 0) m_scrollOffset.x = 0;
}
void ScrollableWidget::addScrollOffset(const Vec2& offset)
{
auto ext = getContentExtents();
auto cont = getContentBounds();
f32 maxScrollY = -(ext.h - cont.h);
f32 maxScrollX = -(ext.w - cont.w);
m_scrollOffset += offset;
if (m_scrollOffset.y < maxScrollY) m_scrollOffset.y = maxScrollY;
if (m_scrollOffset.y > 0) m_scrollOffset.y = 0;
if (m_scrollOffset.x < maxScrollX) m_scrollOffset.x = maxScrollX;
if (m_scrollOffset.x > 0) m_scrollOffset.x = 0;
}
bool ScrollableWidget::needsVScroll(void) const
{
return isVScrollEnabled() && getContentExtents().h > getContentBounds().h;
}
bool ScrollableWidget::needsHScroll(void) const
{
return isHScrollEnabled() && getContentExtents().w > getContentBounds().w;
}
void ScrollableWidget::onWidgetAdded(Widget& child)
void ScrollableWidget::onUpdate(void)
{
m_smoothScrollTimer.update();
if (!needsVScroll() && m_vScrollbarAdded)
{
removeWidget(m_vScrollbar);
m_vScrollbarAdded = false;
}
else if (!m_vScrollbarAdded)
{
addWidget(m_vScrollbar);
m_vScrollbarAdded = true;
}
if (!needsHScroll() && m_hScrollbarAdded)
{
removeWidget(m_hScrollbar);
addWidget(m_vScrollbar, { 0, 0 }, true);
addWidget(m_hScrollbar, { 0, 0 }, true);
m_hScrollbarAdded = false;
}
f32 ScrollableWidget::getVScrollbarSize(void) const
else if (!m_hScrollbarAdded)
{
if (!isVScrollEnabled())
return 0;
if (!needsVScroll())
return 0;
return m_vScrollbar.getw();
addWidget(m_hScrollbar);
m_hScrollbarAdded = true;
}
}
f32 ScrollableWidget::getHScrollbarSize(void) const
void ScrollableWidget::drawScrollbars(ogfx::BasicRenderer2D& gfx)
{
m_vScrollbar.__draw(gfx);
m_hScrollbar.__draw(gfx);
}
void ScrollableWidget::updateScrollbarsSize(void)
{
m_vScrollbar.setMargin({ 0, getPureContentBounds().y, 0, 0 });
}
void ScrollableWidget::resetScroll(bool horizontal, bool vertical, bool propagate)
{
if (vertical)
m_scrollOffset.y = 0;
if (horizontal)
m_scrollOffset.x = 0;
if (propagate)
{
if (!isHScrollEnabled())
return 0;
if (!needsHScroll())
return 0;
return m_hScrollbar.geth();
for (auto& w : getChildren().getWidgets())
{
if (w == nullptr) continue;
w->resetScroll(horizontal, vertical, propagate);
}
}
}
bool ScrollableWidget::isMouseInsideVScrollbar(void) const
{
return m_vScrollbar.isMouseInside();
}
bool ScrollableWidget::isMouseInsideHScrollbar(void) const
{
return m_hScrollbar.isMouseInside();
}
void ScrollableWidget::onMouseScrolled(const Event& event)
{
if (isVScrollEnabled())
{
bool mouseInsideHScrollbar = m_hScrollbar.isMouseInside() && needsHScroll();
if (std::abs(event.mouse->scrollAmount.y) > 0 && !mouseInsideHScrollbar)
{
m_scrollVelocity.y += m_scrollSpeed * event.mouse->scrollAmount.y * m_scrollSpeedMultiplier;
if (m_smoothScrollTimer.isStopped())
m_smoothScrollTimer.restart();
event.handle();
}
else if (std::abs(event.mouse->scrollAmount.y) > 0)
{
m_scrollVelocity.x += m_scrollSpeed * event.mouse->scrollAmount.y * m_scrollSpeedMultiplier;
if (m_smoothScrollTimer.isStopped())
m_smoothScrollTimer.restart();
event.handle();
}
}
if (isHScrollEnabled())
{
if (std::abs(event.mouse->scrollAmount.x) > 0)
{
m_scrollVelocity.x += m_scrollSpeed * event.mouse->scrollAmount.x * m_scrollSpeedMultiplier;
if (m_smoothScrollTimer.isStopped())
m_smoothScrollTimer.restart();
event.handle();
}
}
}
void ScrollableWidget::setScrollOffset(const Vec2& offset)
{
auto ext = getContentExtents();
auto cont = getContentBounds();
f32 maxScrollY = -(ext.h - cont.h);
f32 maxScrollX = -(ext.w - cont.w);
m_scrollOffset = offset;
if (m_scrollOffset.y < maxScrollY) m_scrollOffset.y = maxScrollY;
if (m_scrollOffset.y > 0) m_scrollOffset.y = 0;
if (m_scrollOffset.x < maxScrollX) m_scrollOffset.x = maxScrollX;
if (m_scrollOffset.x > 0) m_scrollOffset.x = 0;
}
void ScrollableWidget::addScrollOffset(const Vec2& offset)
{
auto ext = getContentExtents();
auto cont = getContentBounds();
f32 maxScrollY = -(ext.h - cont.h);
f32 maxScrollX = -(ext.w - cont.w);
m_scrollOffset += offset;
if (m_scrollOffset.y < maxScrollY) m_scrollOffset.y = maxScrollY;
if (m_scrollOffset.y > 0) m_scrollOffset.y = 0;
if (m_scrollOffset.x < maxScrollX) m_scrollOffset.x = maxScrollX;
if (m_scrollOffset.x > 0) m_scrollOffset.x = 0;
}
bool ScrollableWidget::needsVScroll(void) const
{
return isVScrollEnabled() && getContentExtents().h > getContentBounds().h;
}
bool ScrollableWidget::needsHScroll(void) const
{
return isHScrollEnabled() && getContentExtents().w > getContentBounds().w;
}
void ScrollableWidget::onWidgetAdded(Widget& child)
{
removeWidget(m_vScrollbar);
removeWidget(m_hScrollbar);
addWidget(m_vScrollbar, { 0, 0 }, true);
addWidget(m_hScrollbar, { 0, 0 }, true);
}
f32 ScrollableWidget::getVScrollbarSize(void) const
{
if (!isVScrollEnabled())
return 0;
if (!needsVScroll())
return 0;
return m_vScrollbar.getw();
}
f32 ScrollableWidget::getHScrollbarSize(void) const
{
if (!isHScrollEnabled())
return 0;
if (!needsHScroll())
return 0;
return m_hScrollbar.geth();
}
}
}

View file

@ -27,123 +27,120 @@ namespace ogfx
{
namespace gui
{
namespace widgets
class VerticalScrollBar : public Widget
{
class VerticalScrollBar : public Widget
{
public:
inline VerticalScrollBar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
VerticalScrollBar& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseDragged(const Event& event) override;
void onMousePressed(const Event& event) override;
void onMouseReleased(const Event& event) override;
void onUpdate(void) override;
bool isMouseInsideThumb(const Vec2& mouse_pos);
public:
inline VerticalScrollBar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
VerticalScrollBar& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseDragged(const Event& event) override;
void onMousePressed(const Event& event) override;
void onMouseReleased(const Event& event) override;
void onUpdate(void) override;
bool isMouseInsideThumb(const Vec2& mouse_pos);
inline void setx(f32 xx) override { }
inline void sety(f32 yy) override { }
inline void setw(f32 ww) override { }
inline void seth(f32 hh) override { }
inline void setx(f32 xx) override { }
inline void sety(f32 yy) override { }
inline void setw(f32 ww) override { }
inline void seth(f32 hh) override { }
private:
void update_thumb(void);
void set_thumb_y(f32 thumby);
private:
void update_thumb(void);
void set_thumb_y(f32 thumby);
private:
f32 m_thumbHeight { 0 };
f32 m_thumbY { 0 };
Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 };
bool m_mousePressed { false };
bool m_mouseDragged { false };
f32 m_dragGrabOffset { 0 };
Rectangle m_correctionOffset { 1, 1, 0, 0 };
private:
f32 m_thumbHeight { 0 };
f32 m_thumbY { 0 };
Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 };
bool m_mousePressed { false };
bool m_mouseDragged { false };
f32 m_dragGrabOffset { 0 };
Rectangle m_correctionOffset { 1, 1, 0, 0 };
Rectangle m_trackBorderRadii { 0, 0, 0, 0 };
f32 m_thumbBorderRadius { 16 };
Color m_trackColor { 70, 70, 70 };
Color m_thumbColor { 120, 120, 120 };
Color m_thumbBorderColor { 150, 150, 150 };
bool m_thumbShowBorder { true };
};
class HorizontalScrollbar : public Widget
{
public:
inline HorizontalScrollbar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
HorizontalScrollbar& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseDragged(const Event& event) override;
void onMousePressed(const Event& event) override;
void onMouseReleased(const Event& event) override;
void onUpdate(void) override;
bool isMouseInsideThumb(const Vec2& mouse_pos);
Rectangle m_trackBorderRadii { 0, 0, 0, 0 };
f32 m_thumbBorderRadius { 16 };
Color m_trackColor { 70, 70, 70 };
Color m_thumbColor { 120, 120, 120 };
Color m_thumbBorderColor { 150, 150, 150 };
bool m_thumbShowBorder { true };
};
class HorizontalScrollbar : public Widget
{
public:
inline HorizontalScrollbar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
HorizontalScrollbar& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseDragged(const Event& event) override;
void onMousePressed(const Event& event) override;
void onMouseReleased(const Event& event) override;
void onUpdate(void) override;
bool isMouseInsideThumb(const Vec2& mouse_pos);
inline void setx(f32 xx) override { }
inline void sety(f32 yy) override { }
inline void setw(f32 ww) override { }
inline void seth(f32 hh) override { }
inline void setx(f32 xx) override { }
inline void sety(f32 yy) override { }
inline void setw(f32 ww) override { }
inline void seth(f32 hh) override { }
private:
void update_thumb(void);
void set_thumb_x(f32 thumbx);
private:
void update_thumb(void);
void set_thumb_x(f32 thumbx);
private:
f32 m_thumbWidth { 0 };
f32 m_thumbX { 0 };
Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 };
bool m_mousePressed { false };
bool m_mouseDragged { false };
f32 m_dragGrabOffset { 0 };
Rectangle m_correctionOffset { 0, 0, 0, 0 };
private:
f32 m_thumbWidth { 0 };
f32 m_thumbX { 0 };
Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 };
bool m_mousePressed { false };
bool m_mouseDragged { false };
f32 m_dragGrabOffset { 0 };
Rectangle m_correctionOffset { 0, 0, 0, 0 };
Rectangle m_trackBorderRadii { 0, 0, 0, 0 };
f32 m_thumbBorderRadius { 16 };
Color m_trackColor { 70, 70, 70 };
Color m_thumbColor { 120, 120, 120 };
Color m_thumbBorderColor { 150, 150, 150 };
bool m_thumbShowBorder { true };
};
class ScrollableWidget : public Widget
{
public:
inline ScrollableWidget(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
ScrollableWidget& create(void);
virtual void onUpdate(void) override;
void drawScrollbars(ogfx::BasicRenderer2D& gfx);
void updateScrollbarsSize(void);
void resetScroll(bool horizontal = true, bool vertical = true, bool propagate = true) override;
bool isMouseInsideVScrollbar(void) const;
bool isMouseInsideHScrollbar(void) const;
inline bool isMouseInsideAnyScrollbar(void) const { return isMouseInsideVScrollbar() || isMouseInsideHScrollbar(); }
virtual void onMouseScrolled(const Event& event) override;
virtual void setScrollOffset(const Vec2& offset) override;
virtual void addScrollOffset(const Vec2& offset) override;
virtual bool needsVScroll(void) const override;
virtual bool needsHScroll(void) const override;
virtual void onWidgetAdded(Widget& child) override;
virtual f32 getVScrollbarSize(void) const override;
virtual f32 getHScrollbarSize(void) const override;
inline Vec2 getScrollOffset(void) const override { return m_scrollOffset; }
inline void setScrollSpeed(f32 speed) { m_scrollSpeed = speed; }
inline f32 getScrollSpeed(void) const { return m_scrollSpeed; }
inline void setScrollSmoothFactor(f32 speed) { m_scrollSmoothFactor = std::clamp(speed, 0.0f, 1.0f); }
inline f32 getScrollSmoothFactor(void) const { return m_scrollSmoothFactor; }
Rectangle m_trackBorderRadii { 0, 0, 0, 0 };
f32 m_thumbBorderRadius { 16 };
Color m_trackColor { 70, 70, 70 };
Color m_thumbColor { 120, 120, 120 };
Color m_thumbBorderColor { 150, 150, 150 };
bool m_thumbShowBorder { true };
};
class ScrollableWidget : public Widget
{
public:
inline ScrollableWidget(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
ScrollableWidget& create(void);
virtual void onUpdate(void) override;
void drawScrollbars(ogfx::BasicRenderer2D& gfx);
void updateScrollbarsSize(void);
void resetScroll(bool horizontal = true, bool vertical = true, bool propagate = true) override;
bool isMouseInsideVScrollbar(void) const;
bool isMouseInsideHScrollbar(void) const;
inline bool isMouseInsideAnyScrollbar(void) const { return isMouseInsideVScrollbar() || isMouseInsideHScrollbar(); }
virtual void onMouseScrolled(const Event& event) override;
virtual void setScrollOffset(const Vec2& offset) override;
virtual void addScrollOffset(const Vec2& offset) override;
virtual bool needsVScroll(void) const override;
virtual bool needsHScroll(void) const override;
virtual void onWidgetAdded(Widget& child) override;
virtual f32 getVScrollbarSize(void) const override;
virtual f32 getHScrollbarSize(void) const override;
inline Vec2 getScrollOffset(void) const override { return m_scrollOffset; }
inline void setScrollSpeed(f32 speed) { m_scrollSpeed = speed; }
inline f32 getScrollSpeed(void) const { return m_scrollSpeed; }
inline void setScrollSmoothFactor(f32 speed) { m_scrollSmoothFactor = std::clamp(speed, 0.0f, 1.0f); }
inline f32 getScrollSmoothFactor(void) const { return m_scrollSmoothFactor; }
private:
String m_title { "Panel" };
Vec2 m_scrollOffset { 0, 0 };
VerticalScrollBar m_vScrollbar { getWindow() };
HorizontalScrollbar m_hScrollbar { getWindow() };
ostd::StepTimer m_smoothScrollTimer;
f32 m_scrollSpeed { 0.8f };
Vec2 m_scrollVelocity { 0.0f, 0.0f };
f32 m_scrollSmoothFactor { 0.7f };
f32 m_scrollSpeedMultiplier { 15.0f };
bool m_hScrollbarAdded { false };
bool m_vScrollbarAdded { false };
};
}
private:
String m_title { "Panel" };
Vec2 m_scrollOffset { 0, 0 };
VerticalScrollBar m_vScrollbar { getWindow() };
HorizontalScrollbar m_hScrollbar { getWindow() };
ostd::StepTimer m_smoothScrollTimer;
f32 m_scrollSpeed { 0.8f };
Vec2 m_scrollVelocity { 0.0f, 0.0f };
f32 m_scrollSmoothFactor { 0.7f };
f32 m_scrollSpeedMultiplier { 15.0f };
bool m_hScrollbarAdded { false };
bool m_vScrollbarAdded { false };
};
}
}

View file

@ -25,169 +25,166 @@ namespace ogfx
{
namespace gui
{
namespace widgets
Slider& Slider::create(bool vertical, f32 min, f32 max, f32 step)
{
Slider& Slider::create(bool vertical, f32 min, f32 max, f32 step)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::Slider");
disableChildren();
disableBackground();
disableBorder();
setContentOffset({ 0, 0 });
setStylesheetCategoryName("slider");
setMinValue(min);
setMaxValue(max);
setStep(step);
validate();
return *this;
}
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::Slider");
disableChildren();
disableBackground();
disableBorder();
setContentOffset({ 0, 0 });
setStylesheetCategoryName("slider");
setMinValue(min);
setMaxValue(max);
setStep(step);
validate();
return *this;
}
void Slider::applyTheme(const ostd::Stylesheet& theme)
{
setHandleColor(getThemeValue<Color>(theme, "handle.color", getHandleColor()));
setHandleSize(getThemeValue<Vec2>(theme, "handle.size", getHandleSize()));
setTrackColor(getThemeValue<Color>(theme, "track.color", getTrackColor()));
setTrackProgressColor(getThemeValue<Color>(theme, "track.progressColor", getTrackProgressColor()));
setTrackWidth(getThemeValue<f32>(theme, "track.width", getTrackWidth()));
enableDrawTicks(getThemeValue<bool>(theme, "showStepTicks", isDrawTicksEnabled()));
setStepTickHeight(getThemeValue<f32>(theme, "stepTickHeight", getStepTickHeight()));
setStepTickColor(getThemeValue<Color>(theme, "stepTickColor", getStepTickColor()));
}
void Slider::applyTheme(const ostd::Stylesheet& theme)
{
setHandleColor(getThemeValue<Color>(theme, "handle.color", getHandleColor()));
setHandleSize(getThemeValue<Vec2>(theme, "handle.size", getHandleSize()));
setTrackColor(getThemeValue<Color>(theme, "track.color", getTrackColor()));
setTrackProgressColor(getThemeValue<Color>(theme, "track.progressColor", getTrackProgressColor()));
setTrackWidth(getThemeValue<f32>(theme, "track.width", getTrackWidth()));
enableDrawTicks(getThemeValue<bool>(theme, "showStepTicks", isDrawTicksEnabled()));
setStepTickHeight(getThemeValue<f32>(theme, "stepTickHeight", getStepTickHeight()));
setStepTickColor(getThemeValue<Color>(theme, "stepTickColor", getStepTickColor()));
}
void Slider::onMouseReleased(const Event& event)
void Slider::onMouseReleased(const Event& event)
{
if (!isMouseInside())
return;
if (isMiddleClickShortcutEnabled() && event.mouse->button == MouseEventData::eButton::Middle)
{
if (!isMouseInside())
return;
if (isMiddleClickShortcutEnabled() && event.mouse->button == MouseEventData::eButton::Middle)
{
set_value(snap_to_step((m_min + m_max) * 0.5f));
return;
}
set_value(snap_to_step((m_min + m_max) * 0.5f));
return;
}
set_value(mouse_position_to_value({ event.mouse->position_x, event.mouse->position_y }));
}
void Slider::onMouseDragged(const Event& event)
{
if (!isMouseInside())
return;
if (event.mouse->button == MouseEventData::eButton::Left)
set_value(mouse_position_to_value({ event.mouse->position_x, event.mouse->position_y }));
}
}
void Slider::onMouseDragged(const Event& event)
void Slider::onMouseScrolled(const Event& event)
{
if (!isMouseInside())
return;
if (!isScrollWheelShortcutEnabled())
return;
if (getStep() > 0)
{
if (!isMouseInside())
return;
if (event.mouse->button == MouseEventData::eButton::Left)
set_value(mouse_position_to_value({ event.mouse->position_x, event.mouse->position_y }));
}
void Slider::onMouseScrolled(const Event& event)
{
if (!isMouseInside())
return;
if (!isScrollWheelShortcutEnabled())
return;
if (getStep() > 0)
{
if (event.mouse->scroll == MouseEventData::eScrollDirection::Down ||
event.mouse->scroll == MouseEventData::eScrollDirection::Right)
set_value(getValue() + (getStep() * -1));
else
set_value(getValue() + getStep());
}
else if (event.mouse->scroll == MouseEventData::eScrollDirection::Down ||
event.mouse->scroll == MouseEventData::eScrollDirection::Up)
set_value(getValue() + (0.1f * event.mouse->scrollAmount.y));
if (event.mouse->scroll == MouseEventData::eScrollDirection::Down ||
event.mouse->scroll == MouseEventData::eScrollDirection::Right)
set_value(getValue() + (getStep() * -1));
else
set_value(getValue() + (0.1f * event.mouse->scrollAmount.x));
event.handle();
set_value(getValue() + getStep());
}
else if (event.mouse->scroll == MouseEventData::eScrollDirection::Down ||
event.mouse->scroll == MouseEventData::eScrollDirection::Up)
set_value(getValue() + (0.1f * event.mouse->scrollAmount.y));
else
set_value(getValue() + (0.1f * event.mouse->scrollAmount.x));
event.handle();
}
void Slider::onDraw(ogfx::BasicRenderer2D& gfx)
void Slider::onDraw(ogfx::BasicRenderer2D& gfx)
{
const auto& bounds = getGlobalContentBounds();
f32 value = m_value;
f32 t = (value - m_min) / (m_max - m_min); // normalized [0, 1]
auto l_centerh = [&](f32 value) -> f32 {
return (geth() / 2.0f) - (value / 2.0f);
};
auto l_centerw = [&](f32 value) -> f32 {
return (getw() / 2.0f) - (value / 2.0f);
};
if (isHorizontal())
{
const auto& bounds = getGlobalContentBounds();
f32 value = m_value;
f32 t = (value - m_min) / (m_max - m_min); // normalized [0, 1]
auto l_centerh = [&](f32 value) -> f32 {
return (geth() / 2.0f) - (value / 2.0f);
};
auto l_centerw = [&](f32 value) -> f32 {
return (getw() / 2.0f) - (value / 2.0f);
};
if (isHorizontal())
if (m_step > 0.0f && m_drawTicks)
{
if (m_step > 0.0f && m_drawTicks)
int tickCount = (int)std::round((m_max - m_min) / m_step) + 1;
for (int i = 0; i < tickCount; i++)
{
int tickCount = (int)std::round((m_max - m_min) / m_step) + 1;
for (int i = 0; i < tickCount; i++)
{
f32 v = m_min + i * m_step;
v = std::min(v, m_max);
f32 tickT = (v - m_min) / (m_max - m_min);
f32 tickX = bounds.x + bounds.w * tickT;
gfx.fillRect({ tickX - 1, bounds.y + l_centerh(getStepTickHeight()) - getStepTickHeight(), 2, getStepTickHeight() }, getStepTickColor());
}
f32 v = m_min + i * m_step;
v = std::min(v, m_max);
f32 tickT = (v - m_min) / (m_max - m_min);
f32 tickX = bounds.x + bounds.w * tickT;
gfx.fillRect({ tickX - 1, bounds.y + l_centerh(getStepTickHeight()) - getStepTickHeight(), 2, getStepTickHeight() }, getStepTickColor());
}
Vec2 correction { -2, 0 };
Vec2 trackPos = bounds.getPosition() + Vec2 { 0, l_centerh(getTrackWidth()) };
gfx.fillRect({ trackPos + correction, bounds.w + (correction.x * -2), getTrackWidth() }, getTrackColor());
gfx.fillRect({ trackPos + correction, (bounds.w + (correction.x * -2)) * t, getTrackWidth() }, getTrackProgressColor());
f32 handleX = bounds.x + bounds.w * t - (getHandleSize().x / 2.0f);
gfx.fillRect({ handleX, bounds.y + l_centerh(getHandleSize().y), getHandleSize() }, getHandleColor());
}
else
Vec2 correction { -2, 0 };
Vec2 trackPos = bounds.getPosition() + Vec2 { 0, l_centerh(getTrackWidth()) };
gfx.fillRect({ trackPos + correction, bounds.w + (correction.x * -2), getTrackWidth() }, getTrackColor());
gfx.fillRect({ trackPos + correction, (bounds.w + (correction.x * -2)) * t, getTrackWidth() }, getTrackProgressColor());
f32 handleX = bounds.x + bounds.w * t - (getHandleSize().x / 2.0f);
gfx.fillRect({ handleX, bounds.y + l_centerh(getHandleSize().y), getHandleSize() }, getHandleColor());
}
else
{
if (m_step > 0.0f && m_drawTicks)
{
if (m_step > 0.0f && m_drawTicks)
int tickCount = (int)std::round((m_max - m_min) / m_step) + 1;
for (int i = 0; i < tickCount; i++)
{
int tickCount = (int)std::round((m_max - m_min) / m_step) + 1;
for (int i = 0; i < tickCount; i++)
{
f32 v = m_min + i * m_step;
v = std::min(v, m_max);
f32 tickT = (v - m_min) / (m_max - m_min);
f32 tickY = bounds.y + bounds.h * (1.0f - tickT);
gfx.fillRect({ bounds.x + l_centerw(getStepTickHeight()) - getStepTickHeight(), tickY - 1, getStepTickHeight(), 2 }, getStepTickColor());
}
f32 v = m_min + i * m_step;
v = std::min(v, m_max);
f32 tickT = (v - m_min) / (m_max - m_min);
f32 tickY = bounds.y + bounds.h * (1.0f - tickT);
gfx.fillRect({ bounds.x + l_centerw(getStepTickHeight()) - getStepTickHeight(), tickY - 1, getStepTickHeight(), 2 }, getStepTickColor());
}
Vec2 correction { 0, -2 };
Vec2 trackPos = bounds.getPosition() + Vec2 { l_centerw(getTrackWidth()), 0 };
gfx.fillRect({ trackPos + correction, getTrackWidth(), bounds.h + (correction.y * -2) }, getTrackColor());
f32 progressH = (bounds.h + (correction.y * -2)) * t;
gfx.fillRect({ trackPos.x + correction.x, trackPos.y + correction.y + (bounds.h + (correction.y * -2)) - progressH, getTrackWidth(), progressH }, getTrackProgressColor());
Vec2 swappedSize = getHandleSize().swap();
f32 handleY = bounds.y + bounds.h * (1.0f - t) - (swappedSize.y / 2.0f);
gfx.fillRect({ bounds.x + l_centerw(swappedSize.x), handleY, swappedSize }, getHandleColor());
}
Vec2 correction { 0, -2 };
Vec2 trackPos = bounds.getPosition() + Vec2 { l_centerw(getTrackWidth()), 0 };
gfx.fillRect({ trackPos + correction, getTrackWidth(), bounds.h + (correction.y * -2) }, getTrackColor());
f32 progressH = (bounds.h + (correction.y * -2)) * t;
gfx.fillRect({ trackPos.x + correction.x, trackPos.y + correction.y + (bounds.h + (correction.y * -2)) - progressH, getTrackWidth(), progressH }, getTrackProgressColor());
Vec2 swappedSize = getHandleSize().swap();
f32 handleY = bounds.y + bounds.h * (1.0f - t) - (swappedSize.y / 2.0f);
gfx.fillRect({ bounds.x + l_centerw(swappedSize.x), handleY, swappedSize }, getHandleColor());
}
}
void Slider::enableVertical(bool enable)
{
if (isVertical() && enable) return;
if (isHorizontal() && !enable) return;
m_vertical = enable;
setSize(getSize().swap());
}
void Slider::enableVertical(bool enable)
{
if (isVertical() && enable) return;
if (isHorizontal() && !enable) return;
m_vertical = enable;
setSize(getSize().swap());
}
f32 Slider::snap_to_step(f32 val)
{
if (m_step <= 0.0f) return std::clamp(val, m_min, m_max); // full precision
f32 snapped = std::round((val - m_min) / m_step) * m_step + m_min;
return std::clamp(snapped, m_min, m_max);
}
f32 Slider::snap_to_step(f32 val)
{
if (m_step <= 0.0f) return std::clamp(val, m_min, m_max); // full precision
f32 snapped = std::round((val - m_min) / m_step) * m_step + m_min;
return std::clamp(snapped, m_min, m_max);
}
f32 Slider::mouse_position_to_value(Vec2 mousePosition)
{
const auto& bounds = getGlobalBounds();
f32 raw = isHorizontal() ? (mousePosition.x - bounds.x) / bounds.w : 1.0f - (mousePosition.y - bounds.y) / bounds.h;
raw = std::clamp(raw, 0.0f, 1.0f);
return m_min + raw * (m_max - m_min);
}
f32 Slider::mouse_position_to_value(Vec2 mousePosition)
{
const auto& bounds = getGlobalBounds();
f32 raw = isHorizontal() ? (mousePosition.x - bounds.x) / bounds.w : 1.0f - (mousePosition.y - bounds.y) / bounds.h;
raw = std::clamp(raw, 0.0f, 1.0f);
return m_min + raw * (m_max - m_min);
}
void Slider::set_value(f32 val)
{
if (m_value == val)
return;
f32 old = m_value;
m_value = snap_to_step(val);
if (callback_onValueChanged)
callback_onValueChanged(old, m_value);
}
void Slider::set_value(f32 val)
{
if (m_value == val)
return;
f32 old = m_value;
m_value = snap_to_step(val);
if (callback_onValueChanged)
callback_onValueChanged(old, m_value);
}
}
}

View file

@ -26,70 +26,67 @@ namespace ogfx
{
namespace gui
{
namespace widgets
class Slider : public Widget
{
class Slider : public Widget
{
public:
inline Slider(WindowCore& window, bool vertical = false, f32 min = 0.0f, f32 max = 1.0f, f32 step = 0.1f) : Widget({ 0, 0, 0, 0 }, window) { create(vertical, min, max, step); }
Slider& create(bool vertical, f32 min, f32 max, f32 step);
void applyTheme(const ostd::Stylesheet& theme) override;
void onMouseReleased(const Event& event) override;
void onMouseDragged(const Event& event) override;
void onMouseScrolled(const Event& event) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
inline f32 getValue(void) const { return m_value; }
inline void setValueChangedCallback(std::function<void(f32 oldValue, f32 newValue)> callback) { callback_onValueChanged = std::move(callback); }
public:
inline Slider(WindowCore& window, bool vertical = false, f32 min = 0.0f, f32 max = 1.0f, f32 step = 0.1f) : Widget({ 0, 0, 0, 0 }, window) { create(vertical, min, max, step); }
Slider& create(bool vertical, f32 min, f32 max, f32 step);
void applyTheme(const ostd::Stylesheet& theme) override;
void onMouseReleased(const Event& event) override;
void onMouseDragged(const Event& event) override;
void onMouseScrolled(const Event& event) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
inline f32 getValue(void) const { return m_value; }
inline void setValueChangedCallback(std::function<void(f32 oldValue, f32 newValue)> callback) { callback_onValueChanged = std::move(callback); }
void enableVertical(bool enable = true);
inline bool isVertical(void) const { return m_vertical; }
inline void disableVertical(void) { enableVertical(false); }
inline bool isHorizontal(void) const { return !isVertical(); }
inline void enableHorizontal(bool enable = true) { enableVertical(!enable); }
inline void disableHorizontal(void) { enableHorizontal(false); }
void enableVertical(bool enable = true);
inline bool isVertical(void) const { return m_vertical; }
inline void disableVertical(void) { enableVertical(false); }
inline bool isHorizontal(void) const { return !isVertical(); }
inline void enableHorizontal(bool enable = true) { enableVertical(!enable); }
inline void disableHorizontal(void) { enableHorizontal(false); }
OSTD_BOOL_PARAM_GETSET_E(MiddleClickShortcut, m_middleClickShortcut);
OSTD_BOOL_PARAM_GETSET_E(ScrollWheelShortcut, m_enableScrollWheel);
OSTD_PARAM_GETSET(f32, MinValue, m_min);
OSTD_PARAM_GETSET(f32, MaxValue, m_max);
OSTD_PARAM_GETSET(f32, Step, m_step);
OSTD_BOOL_PARAM_GETSET_E(MiddleClickShortcut, m_middleClickShortcut);
OSTD_BOOL_PARAM_GETSET_E(ScrollWheelShortcut, m_enableScrollWheel);
OSTD_PARAM_GETSET(f32, MinValue, m_min);
OSTD_PARAM_GETSET(f32, MaxValue, m_max);
OSTD_PARAM_GETSET(f32, Step, m_step);
OSTD_PARAM_GETSET(f32, TrackWidth, m_trackWidth);
OSTD_PARAM_GETSET(Color, TrackColor, m_trackColor);
OSTD_PARAM_GETSET(Color, TrackProgressColor, m_trackProgressColor);
OSTD_BOOL_PARAM_GETSET_E(DrawTicks, m_drawTicks);
OSTD_PARAM_GETSET(f32, StepTickHeight, m_tickHeight);
OSTD_PARAM_GETSET(Color, StepTickColor, m_tickColor);
OSTD_PARAM_GETSET(Color, HandleColor, m_handleColor);
OSTD_PARAM_GETSET(Vec2, HandleSize, m_handleSize);
OSTD_PARAM_GETSET(f32, TrackWidth, m_trackWidth);
OSTD_PARAM_GETSET(Color, TrackColor, m_trackColor);
OSTD_PARAM_GETSET(Color, TrackProgressColor, m_trackProgressColor);
OSTD_BOOL_PARAM_GETSET_E(DrawTicks, m_drawTicks);
OSTD_PARAM_GETSET(f32, StepTickHeight, m_tickHeight);
OSTD_PARAM_GETSET(Color, StepTickColor, m_tickColor);
OSTD_PARAM_GETSET(Color, HandleColor, m_handleColor);
OSTD_PARAM_GETSET(Vec2, HandleSize, m_handleSize);
private:
f32 snap_to_step(f32 val);
f32 mouse_position_to_value(Vec2 mousePosition);
void set_value(f32 val);
private:
f32 snap_to_step(f32 val);
f32 mouse_position_to_value(Vec2 mousePosition);
void set_value(f32 val);
private:
f32 m_value { 0.0f };
std::function<void(f32 oldValue, f32 newValue)> callback_onValueChanged { nullptr };
private:
f32 m_value { 0.0f };
std::function<void(f32 oldValue, f32 newValue)> callback_onValueChanged { nullptr };
f32 m_min { 0.0f };
f32 m_max { 1.0f };
f32 m_step { 0.1f };
bool m_vertical { false };
bool m_middleClickShortcut { true };
bool m_enableScrollWheel { true };
f32 m_min { 0.0f };
f32 m_max { 1.0f };
f32 m_step { 0.1f };
bool m_vertical { false };
bool m_middleClickShortcut { true };
bool m_enableScrollWheel { true };
f32 m_trackWidth { 6 };
Color m_trackColor { "#500000FF" };
Color m_trackProgressColor { "#AA0000FF" };
f32 m_trackWidth { 6 };
Color m_trackColor { "#500000FF" };
Color m_trackProgressColor { "#AA0000FF" };
bool m_drawTicks { true };
f32 m_tickHeight { 4 };
Color m_tickColor { Colors::Crimson };
bool m_drawTicks { true };
f32 m_tickHeight { 4 };
Color m_tickColor { Colors::Crimson };
Color m_handleColor { Colors::Crimson };
Vec2 m_handleSize { 10, 18 };
};
}
Color m_handleColor { Colors::Crimson };
Vec2 m_handleSize { 10, 18 };
};
}
}

View file

@ -47,12 +47,17 @@ namespace ogfx
if (!result) return false;
if (!__skip_callback)
onWidgetAdded(child);
if (m_layout && !__skip_callback)
relayout();
return result;
}
bool Widget::removeWidget(Widget& child)
{
return m_widgets.removeWidget(child);
bool result = m_widgets.removeWidget(child);
if (result && m_layout)
relayout();
return result;
}
Vec2 Widget::getGlobalPosition(void) const
@ -105,6 +110,14 @@ namespace ogfx
Rectangle Widget::getContentExtents(void) const
{
// If a layout owns this widget, prefer its measure() — it knows
// the real desired extent (e.g. a vertical box with 50 items
// reports 50*itemHeight even if only 5 are visible).
if (m_layout)
{
Vec2 desired = m_layout->measure(*this);
return { 0, 0, desired.x, desired.y };
}
f32 maxX = 0, maxY = 0;
for (auto* child : m_widgets.getWidgets())
{
@ -216,6 +229,28 @@ namespace ogfx
m_visible = v;
}
void Widget::setLayout(std::unique_ptr<Layout> layout)
{
m_layout = std::move(layout);
if (m_layout)
relayout();
}
void Widget::relayout(void)
{
if (!m_layout) return;
m_layout->arrange(*this);
// After our children's bounds changed, give *their* layouts a chance
// to re-run (a horizontal layout containing a vertical layout, etc.).
for (auto* c : m_widgets.getWidgets())
{
if (!c || c->isInvalid()) continue;
if (c->hasLayout())
c->relayout();
}
}
void Widget::setCallback(eCallback type, EventCallback callback)
{
switch (type)
@ -440,6 +475,8 @@ namespace ogfx
callback_onWindowResized(event);
onWindowResized(event);
}
if (m_layout)
relayout();
}
void Widget::__onWindowFocused(const Event& event)

View file

@ -27,6 +27,9 @@
#include <ostd/io/Stylesheet.hpp>
#include <ostd/utils/Defines.hpp>
#include <ostd/utils/Time.hpp>
#include <ogfx/gui/LayoutHint.hpp>
#include <ogfx/gui/Layout.hpp>
#include <memory>
#include <functional>
namespace ogfx
@ -107,6 +110,33 @@ namespace ogfx
// ============================== LAYOUT ==============================
void setLayout(std::unique_ptr<Layout> layout);
template<typename L, typename... Args>
inline L& setLayout(Args&&... args)
{
static_assert(std::is_base_of_v<Layout, L>, "L must derive from Layout");
auto owned = std::make_unique<L>(std::forward<Args>(args)...);
L* raw = owned.get();
setLayout(std::move(owned));
return *raw;
}
inline Layout* getLayout(void) { return m_layout.get(); }
inline bool hasLayout(void) const { return m_layout != nullptr; }
inline void setw(f32 w) override { Rectangle::setw(w); if (m_layout) relayout(); }
inline void seth(f32 h) override { Rectangle::seth(h); if (m_layout) relayout(); }
// Re-runs the owned Layout (if any) and recursively re-runs layouts
// on children that have their own. Safe to call when no layout is set.
void relayout(void);
inline LayoutHint& layoutHint(void) { return m_layoutHint; }
inline const LayoutHint& layoutHint(void) const { return m_layoutHint; }
// ====================================================================
// =============================== DIMENSIONS ===============================
virtual Vec2 getGlobalPosition(void) const;
virtual Vec2 getGlobalContentPosition(void) const;
@ -126,6 +156,7 @@ namespace ogfx
inline static ostd::BaseObject* getDragAndDropData(void) { return s_dragAndDropData; }
inline WindowCore& getWindow(void) { return *m_window; }
inline Widget* getParent(void) { return m_parent; }
inline const Widget* getParent(void) const { return m_parent; }
inline i32 getZIndex(void) const { return m_zIndex; }
inline ogfx::MouseEventData::eButton getPressedMouseButton(void) const { return m_pressedButton; }
inline const stdvec<String>& getThemeIDList(void) const { return m_themeIDList; }
@ -167,7 +198,7 @@ namespace ogfx
OSTD_BOOL_PARAM_GETSET_E(TopMost, m_topMost);
OSTD_BOOL_PARAM_GETSET_E(IgnoreScroll, m_ignoreScroll);
OSTD_BOOL_PARAM_GETSET_E(VScroll, m_vScrollEnabled);
OSTD_BOOL_PARAM_GETSET_E(HScroll, m_vScrollEnabled);
OSTD_BOOL_PARAM_GETSET_E(HScroll, m_hScrollEnabled);
OSTD_BOOL_PARAM_GETSET_E(BackgroundGradient, m_useBackgroundGradient);
OSTD_BOOL_PARAM_GETSET_E(Theming, m_enableTheming);
OSTD_BOOL_PARAM_GETSET_E(Tooltip, m_enableTooltip);
@ -199,6 +230,8 @@ namespace ogfx
WindowCore* m_window { nullptr };
Widget* m_parent { nullptr };
WidgetManager m_widgets;
std::unique_ptr<Layout> m_layout;
LayoutHint m_layoutHint;
bool m_rootChild { false };
bool m_focused { false };
@ -340,6 +373,7 @@ namespace ogfx
inline virtual void onWindowResized(const Event& event) { }
inline virtual void onWindowFocused(const Event& event) { }
inline virtual void onWindowFocusLost(const Event& event) { }
inline virtual void onBoundsChanged(const Vec2& newSize) { }
};
}
}

View file

@ -374,9 +374,6 @@ namespace ogfx
if (m_vertexCount + 4 >= MaxVertices || m_indexCount + 6 >= MaxIndices)
flushBatch();
if (m_texture != nullptr)
flushBatch();
m_texture = nullptr;
SDL_FColor c0 = COLOR_CAST(colors[i]);
SDL_FColor c1 = COLOR_CAST(colors[i + 1]);

View file

@ -316,70 +316,71 @@ namespace ostd
inline virtual f32 gety(void) const { return y; }
inline virtual f32 getw(void) const { return w; }
inline virtual f32 geth(void) const { return h; }
inline virtual f32 getCenterX(void) const { return getx() + getw() / 2; }
inline virtual f32 getCenterY(void) const { return gety() + geth() / 2; }
inline f32 getCenterX(void) const { return getx() + getw() / 2; }
inline f32 getCenterY(void) const { return gety() + geth() / 2; }
inline virtual void setx(f32 xx) { x = xx; }
inline virtual void sety(f32 yy) { y = yy; }
inline virtual void setw(f32 ww) { w = ww; }
inline virtual void seth(f32 hh) { h = hh; }
inline virtual Vec2 getPosition(void) const { return Vec2(getx(), gety()); }
inline virtual Vec2 getSize(void) const { return Vec2(getw(), geth()); }
inline virtual Vec2 getCenter(void) const { return Vec2(getx() + getw() / 2, gety() + geth() / 2); }
inline Vec2 getPosition(void) const { return Vec2(getx(), gety()); }
inline Vec2 getSize(void) const { return Vec2(getw(), geth()); }
inline Vec2 getCenter(void) const { return Vec2(getx() + getw() / 2, gety() + geth() / 2); }
inline Rectangle getBounds(void) const { return *this; }
inline virtual void setPosition(const Vec2& pos) { setx(pos.x); sety(pos.y); }
inline virtual void setPosition(f32 xx, f32 yy) { setx(xx); sety(yy); }
inline virtual void setSize(Vec2 size) { setw(size.x); seth(size.y); }
inline virtual void setSize(f32 ww, f32 hh) { setw(ww); seth(hh); }
inline virtual void setBounds(f32 xx, f32 yy, f32 ww, f32 hh) { setx(xx); sety(yy); setw(ww); seth(hh); }
inline void setPosition(const Vec2& pos) { setx(pos.x); sety(pos.y); }
inline void setPosition(f32 xx, f32 yy) { setx(xx); sety(yy); }
inline void setSize(Vec2 size) { setw(size.x); seth(size.y); }
inline void setSize(f32 ww, f32 hh) { setw(ww); seth(hh); }
inline void setBounds(f32 xx, f32 yy, f32 ww, f32 hh) { setx(xx); sety(yy); setw(ww); seth(hh); }
inline virtual f32 addx(f32 xx) { setx(getx() + xx); return getx(); }
inline virtual f32 addy(f32 yy) { sety(gety() + yy); return gety(); }
inline virtual Vec2 addPos(f32 xx, f32 yy) { return Vec2(addx(xx), addy(yy)); }
inline virtual Vec2 addPos(Vec2 pos) { return addPos(pos.x, pos.y); }
inline virtual f32 addw(f32 ww) { setw(getw() + ww); return getw(); }
inline virtual f32 addh(f32 hh) { seth(geth() + hh); return geth(); }
inline virtual Vec2 addSize(f32 ww, f32 hh) { addw(ww), addh(hh); return getSize(); }
inline virtual Vec2 addSize(Vec2 size) { addSize(size.x, size.y); return getSize(); }
inline f32 addx(f32 xx) { setx(getx() + xx); return getx(); }
inline f32 addy(f32 yy) { sety(gety() + yy); return gety(); }
inline Vec2 addPos(f32 xx, f32 yy) { return Vec2(addx(xx), addy(yy)); }
inline Vec2 addPos(Vec2 pos) { return addPos(pos.x, pos.y); }
inline f32 addw(f32 ww) { setw(getw() + ww); return getw(); }
inline f32 addh(f32 hh) { seth(geth() + hh); return geth(); }
inline Vec2 addSize(f32 ww, f32 hh) { addw(ww), addh(hh); return getSize(); }
inline Vec2 addSize(Vec2 size) { addSize(size.x, size.y); return getSize(); }
inline virtual f32 subx(f32 xx) { setx(getx() - xx); return getx(); }
inline virtual f32 suby(f32 yy) { sety(gety() - yy); return gety(); }
inline virtual Vec2 subPos(f32 xx, f32 yy) { return Vec2(subx(xx), suby(yy)); }
inline virtual Vec2 subPos(Vec2 pos) { return subPos(pos.x, pos.y); }
inline virtual f32 subw(f32 ww) { setw(getw() - ww); return getw(); }
inline virtual f32 subh(f32 hh) { seth(geth() - hh); return geth(); }
inline virtual Vec2 subSize(f32 ww, f32 hh) { return Vec2(subw(ww), subh(hh)); }
inline virtual Vec2 subSize(Vec2 size) { return subPos(size.x, size.y); }
inline f32 subx(f32 xx) { setx(getx() - xx); return getx(); }
inline f32 suby(f32 yy) { sety(gety() - yy); return gety(); }
inline Vec2 subPos(f32 xx, f32 yy) { return Vec2(subx(xx), suby(yy)); }
inline Vec2 subPos(Vec2 pos) { return subPos(pos.x, pos.y); }
inline f32 subw(f32 ww) { setw(getw() - ww); return getw(); }
inline f32 subh(f32 hh) { seth(geth() - hh); return geth(); }
inline Vec2 subSize(f32 ww, f32 hh) { return Vec2(subw(ww), subh(hh)); }
inline Vec2 subSize(Vec2 size) { return subPos(size.x, size.y); }
inline virtual f32 mulx(f32 xx) { setx(getx() * xx); return getx(); }
inline virtual f32 muly(f32 yy) { sety(gety() * yy); return gety(); }
inline virtual Vec2 mulPos(f32 xx, f32 yy) { return Vec2(mulx(xx), muly(yy)); }
inline virtual Vec2 mulPos(Vec2 pos) { return mulPos(pos.x, pos.y); }
inline virtual f32 mulw(f32 ww) { setw(getw() * ww); return getw(); }
inline virtual f32 mulh(f32 hh) { seth(geth() * hh); return geth(); }
inline virtual Vec2 mulSize(f32 ww, f32 hh) { return Vec2(mulw(ww), mulh(hh)); }
inline virtual Vec2 mulSize(Vec2 size) { return mulPos(size.x, size.y); }
inline f32 mulx(f32 xx) { setx(getx() * xx); return getx(); }
inline f32 muly(f32 yy) { sety(gety() * yy); return gety(); }
inline Vec2 mulPos(f32 xx, f32 yy) { return Vec2(mulx(xx), muly(yy)); }
inline Vec2 mulPos(Vec2 pos) { return mulPos(pos.x, pos.y); }
inline f32 mulw(f32 ww) { setw(getw() * ww); return getw(); }
inline f32 mulh(f32 hh) { seth(geth() * hh); return geth(); }
inline Vec2 mulSize(f32 ww, f32 hh) { return Vec2(mulw(ww), mulh(hh)); }
inline Vec2 mulSize(Vec2 size) { return mulPos(size.x, size.y); }
inline virtual f32 divx(f32 xx) { setx(getx() / xx); return getx(); }
inline virtual f32 divy(f32 yy) { sety(gety() / yy); return gety(); }
inline virtual Vec2 divPos(f32 xx, f32 yy) { return Vec2(divx(xx), divy(yy)); }
inline virtual Vec2 divPos(Vec2 pos) { return divPos(pos.x, pos.y); }
inline virtual f32 divw(f32 ww) { setw(getw() / ww); return getw(); }
inline virtual f32 divh(f32 hh) { seth(geth() / hh); return geth(); }
inline virtual Vec2 divSize(f32 ww, f32 hh) { return Vec2(divw(ww), divh(hh)); }
inline virtual Vec2 divSize(Vec2 size) { return divPos(size.x, size.y); }
inline f32 divx(f32 xx) { setx(getx() / xx); return getx(); }
inline f32 divy(f32 yy) { sety(gety() / yy); return gety(); }
inline Vec2 divPos(f32 xx, f32 yy) { return Vec2(divx(xx), divy(yy)); }
inline Vec2 divPos(Vec2 pos) { return divPos(pos.x, pos.y); }
inline f32 divw(f32 ww) { setw(getw() / ww); return getw(); }
inline f32 divh(f32 hh) { seth(geth() / hh); return geth(); }
inline Vec2 divSize(f32 ww, f32 hh) { return Vec2(divw(ww), divh(hh)); }
inline Vec2 divSize(Vec2 size) { return divPos(size.x, size.y); }
inline virtual Vec2 topLeft(void) const { return getPosition(); }
inline virtual Vec2 topRight(void) const { return Vec2(getx() + getw(), gety()); }
inline virtual Vec2 bottomLeft(void) const { return Vec2(getx(), gety() + geth()); }
inline virtual Vec2 bottomRight(void) const { return Vec2(getx() + getw(), gety() + geth()); }
inline virtual Rectangle edgeRect(void) const { return { getx(), gety(), getx() + getw(), gety() + getw() }; }
inline Vec2 topLeft(void) const { return getPosition(); }
inline Vec2 topRight(void) const { return Vec2(getx() + getw(), gety()); }
inline Vec2 bottomLeft(void) const { return Vec2(getx(), gety() + geth()); }
inline Vec2 bottomRight(void) const { return Vec2(getx() + getw(), gety() + geth()); }
inline Rectangle edgeRect(void) const { return { getx(), gety(), getx() + getw(), gety() + getw() }; }
inline virtual f32 left(void) const { return getx(); }
inline virtual f32 right(void) const { return getw(); }
inline virtual f32 top(void) const { return gety(); }
inline virtual f32 bottom(void) const { return geth(); }
inline f32 left(void) const { return getx(); }
inline f32 right(void) const { return getw(); }
inline f32 top(void) const { return gety(); }
inline f32 bottom(void) const { return geth(); }
inline virtual String toString(void) const override { return String("{ ").add(x).add(", ").add(y).add(", ").add(w).add(", ").add(h).add(" }"); }
@ -411,14 +412,14 @@ namespace ostd
f32 bottomY = std::min(y + h, rect.y + rect.h);
return { leftX, topY, rightX - leftX, bottomY - topY };
}
inline virtual bool contains(Vec2 p, bool includeBounds = false) const
inline virtual bool contains(Vec2 p, bool includeBounds = true) const
{
if (includeBounds)
return p.x >= x && p.y >= y && p.x <= x + w && p.y <= y + h;
else
return p.x > x && p.y > y && p.x < x + w && p.y < y + h;
}
inline bool contains(f32 xx, f32 yy, bool includeBounds = false) const { return contains({ xx, yy }, includeBounds); }
inline bool contains(f32 xx, f32 yy, bool includeBounds = true) const { return contains({ xx, yy }, includeBounds); }
inline virtual f32 getDistance(Vec2 p) const { return sqrt(fabs((p.x - getx()) * (p.x - getx()) + (p.y - gety()) * (p.y - gety()))); }

View file

@ -30,12 +30,12 @@ ogfx::WindowCore::FileDialogFilterList filters = {
};
ostd::ConsoleOutputHandler out;
using namespace ogfx::gui::widgets;
using namespace ogfx::gui;
class Window : public ogfx::gui::Window
class TestWindow : public Window
{
public:
inline Window(void) { }
inline TestWindow(void) { }
inline void onInitialize(void) override
{
ogfx::AnimationData ad;
@ -55,6 +55,8 @@ class Window : public ogfx::gui::Window
setTheme(m_theme);
getToolBar().setHeight(30);
getToolBar().setLayout<BoxLayout>(BoxLayout::Orientation::Horizontal);
getToolBar().getLayout()->setSpacing(10);
getStatusBar().setHeight(24);
showMenuBar();
showToolBar();
@ -72,7 +74,7 @@ class Window : public ogfx::gui::Window
iconsAD.still = true;
m_label1.setText("Show Panel2");
m_label1.setCallback(ogfx::gui::Widget::eCallback::MousePressed, [&](const ogfx::gui::Event& event) -> void {
m_label1.setCallback(Widget::eCallback::MousePressed, [&](const Event& event) -> void {
m_check1.setChecked(!m_check1.isChecked());
});
m_label1.disableTheming();
@ -95,7 +97,7 @@ class Window : public ogfx::gui::Window
m_btn1.enableTooltip();
m_btn1.setTooltipText("Test tooltip");
m_btn1.setText("TEST BUTTON");
m_btn1.setCallback(ogfx::gui::Widget::eCallback::ActionPerformed, [&](const ogfx::gui::Event& event) -> void {
m_btn1.setCallback(Widget::eCallback::ActionPerformed, [&](const Event& event) -> void {
std::cout << showOpenFileDialog(filters) << "\n";
});
@ -148,7 +150,7 @@ class Window : public ogfx::gui::Window
m_list.addLine("Item 3333333");
m_list.getLine(10).setFontSize(40);
m_list.getLine(160).setTextColor(Colors::Crimson);
m_list.setSelectionChangedCallback([&](stdvec<ogfx::gui::widgets::ListView::Item*>& selection) -> void {\
m_list.setSelectionChangedCallback([&](stdvec<ListView::Item*>& selection) -> void {\
std::cout << *(selection[0]) << "\n";
});
@ -171,9 +173,6 @@ class Window : public ogfx::gui::Window
auto& t2 = m_tabs.addTab("Tab2 Test");
auto& t3 = m_tabs.addTab("Long Tab Test");
t2.addThemeOverride("@panel_tab.panel.backgroundColor", Colors::SkyBlue);
t3.addThemeOverride("@panel_tab.panel.backgroundColor", Colors::Orange);
t1.addWidget(m_check1, { 30, 30 });
m_radioGroup.addButton(t1, "Radio this out!", { 30, 80 });
m_radioGroup.addButton(t1, "Radio Opt. 2", { 30, 110 });
@ -188,6 +187,26 @@ class Window : public ogfx::gui::Window
t1.addWidget(m_list, { 30, 300 });
t1.addWidget(m_panel2, { 500, 100 });
t2.setLayout<BoxLayout>(BoxLayout::Orientation::Vertical);
t2.getLayout()->setSpacing(16);
auto* header = new Button(*this);
header->layoutHint().preferred = { -1, 32 }; // fixed 32px height, width follows cross-stretch
auto* body = new ListView(*this);
body->layoutHint().stretch = 1.0f; // takes all leftover vertical space
auto* footer = new Button(*this);
footer->layoutHint().preferred = { -1, 24 };
t2.addWidget(*header); // each addWidget() call triggers a relayout
t2.addWidget(*body);
for (i32 i = 0; i < 100; i++)
{
body->addLine(ostd::Random::getString(ostd::Random::getui8(1, 40)));
}
t2.addWidget(*footer);
m_panel3.addWidget(m_label4);
m_panel1.addWidget(m_label2);
@ -222,11 +241,11 @@ class Window : public ogfx::gui::Window
});
m_menu.onActivate = [this](const ogfx::gui::ContextMenu::Entry& e) {
m_menu.onActivate = [this](const ContextMenu::Entry& e) {
std::cout << e.text << " - " << (i32)e.id << "\n";
};
auto onActivate = [this](const ogfx::gui::ContextMenu::Entry& e) {
auto onActivate = [this](const 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();
@ -264,16 +283,12 @@ class Window : public ogfx::gui::Window
{
auto& mmd = cast<ogfx::MouseEventData&>(signal.userData);
if (mmd.button == ogfx::MouseEventData::eButton::Right)
{
setContextMenu(m_menu);
showContextMenu({ mmd.position_x, mmd.position_y });
}
showContextMenu(m_menu, { mmd.position_x, mmd.position_y });
}
else if (signal.ID == ostd::BuiltinSignals::WindowResized)
{
auto& wrd = cast<ogfx::WindowResizedData&>(signal.userData);
m_tabs.setSize(cast<f32>(getWindowWidth()), cast<f32>(getWindowHeight() - getMenuBar().geth() - getToolBar().geth() - getStatusBar().geth()));
std::cout << m_tabs.getSize() << "\n";
m_tabs.setPosition(0, -1);
m_tabs.refreshCurrentTab();
@ -312,7 +327,7 @@ class Window : public ogfx::gui::Window
enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };
ogfx::gui::ContextMenu::Instance m_menu { {
ContextMenu::Instance m_menu { {
{ "File", -1, {
{ "New", MenuId::New },
{ "Open...", MenuId::Open },
@ -369,7 +384,7 @@ class Window : public ogfx::gui::Window
Help_About,
};
ogfx::gui::ContextMenu::Instance fileMenu { {
ContextMenu::Instance fileMenu { {
{ "New", TestMenuId::File_New },
{ "Open...", TestMenuId::File_Open },
{ "Open Recent", {
@ -390,7 +405,7 @@ class Window : public ogfx::gui::Window
nullptr
};
ogfx::gui::ContextMenu::Instance editMenu { {
ContextMenu::Instance editMenu { {
{ "Undo", TestMenuId::Edit_Undo },
{ "Redo", TestMenuId::Edit_Redo },
{ "Cut", TestMenuId::Edit_Cut },
@ -400,7 +415,7 @@ class Window : public ogfx::gui::Window
nullptr
};
ogfx::gui::ContextMenu::Instance viewMenu { {
ContextMenu::Instance viewMenu { {
{ "Zoom In", TestMenuId::View_ZoomIn },
{ "Zoom Out", TestMenuId::View_ZoomOut },
{ "Reset Zoom", TestMenuId::View_ZoomReset },
@ -412,7 +427,7 @@ class Window : public ogfx::gui::Window
nullptr
};
ogfx::gui::ContextMenu::Instance helpMenu { {
ContextMenu::Instance helpMenu { {
{ "Documentation", TestMenuId::Help_Documentation },
{ "About", TestMenuId::Help_About } },
nullptr
@ -428,7 +443,7 @@ i32 main(i32 argc, char** argv)
{
ostd::Random::autoSeed();
ostd::initialize();
Window window;
TestWindow window;
window.initialize(1200, 800, "OmniaFramework - Test Window");
window.setClearColor({ 0, 0, 0 });
window.setPosition({ 50, 50 });