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,18 +22,17 @@
#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)
{
m_text = text;
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::Button");
setTypeName("ogfx::gui::Button");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("button");
@ -42,6 +41,50 @@ namespace ogfx
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)
@ -66,26 +109,14 @@ namespace ogfx
m_text,
contentTL + m_drawTextOffset,
getTextColor(),
getFontSize(),
m_contentScale
getFontSize()
);
}
}
void Button::onUpdate(void)
{
if (isAnimatedEnabled())
m_anim.update();
}
void Button::onMouseReleased(const Event& event)
{
if (!isMouseInside())
return;
if (event.mouse->button == MouseEventData::eButton::Left && callback_onActionPerformed)
callback_onActionPerformed(event);
}
// ===================================================================
// Setters that trigger a recompute
// ===================================================================
void Button::setText(const String& text)
{
m_text = text;
@ -107,7 +138,6 @@ namespace ogfx
{
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()));
@ -124,56 +154,83 @@ namespace ogfx
invalidate_layout();
}
Vec2 Button::native_icon_size(void) const
// ===================================================================
// 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 };
// Treat font size as the natural icon edge if the user didn't pick one.
const f32 fontEdge = cast<f32>(getFontSize());
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);
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)
// 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)
{
if (nat.x >= nat.y)
return nat.propx(fontEdge);
return nat.propy(fontEdge);
const auto frame = m_anim.getFrameRect();
nat = { frame.w, frame.h };
}
}
return { fontEdge, fontEdge };
else
{
nat = m_icon.getSize();
}
// 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);
const f32 aspect = nat.x / nat.y;
return { targetH * aspect, targetH };
}
return { hint.x, hint.x };
}
return hint;
// 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;
@ -183,75 +240,96 @@ namespace ogfx
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);
// 1) Text dimensions (native font size in all modes).
const bool hasText = !isIconOnlyEnabled() && m_text.len() > 0;
const Vec2 textNative = hasText
const Vec2 textSize = 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;
// 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 (iconSize.x > 0 && iconSize.y > 0
&& (iconSize.x > availW || iconSize.y > availH))
{
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 };
}
}
}
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)
{
nativeW += iconNative.x;
nativeH = std::max(nativeH, iconNative.y);
contentNativeW += iconSize.x;
contentNativeH = std::max(contentNativeH, iconSize.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 });
}
else
{
// Honor the size the user already set on the widget.
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
{
const f32 sx = availW / nativeW;
const f32 sy = availH / nativeH;
m_contentScale = std::min(sx, sy);
}
if (hasIcon) contentNativeW += m_iconSpacing;
contentNativeW += textSize.x;
contentNativeH = std::max(contentNativeH, textSize.y);
}
// 4) Compute scaled draw sizes and centered offsets within the content area.
// 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 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 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;
const f32 totalW = iconScaled.x + spacing + textScaled.x;
const f32 startX = std::max(0.0f, (contentSize.x - totalW) * 0.5f);
m_drawIconSize = iconScaled;
m_drawIconSize = iconSize;
if (hasIcon)
m_drawIconOffset = { startX, std::max(0.0f, (contentSize.y - iconScaled.y) * 0.5f) };
m_drawIconOffset = { startX, (contentSize.y - iconSize.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)
startX + (hasIcon ? iconSize.x + spacing : 0.0f),
(contentSize.y - textSize.y) * 0.5f
};
else
m_drawTextOffset = { 0, 0 };
}
}
}
}

View file

@ -28,35 +28,63 @@ 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
{
AutoSize,
Manual,
LayoutManaged
};
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 onBoundsChanged(const Vec2& newSize) override;
void applyTheme(const ostd::Stylesheet& theme) 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; }
// 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);
// =========================== 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; }
// 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;
// ==================================================================
// 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);
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon);
OSTD_PARAM_GETSET(ostd::AnimationData, AnimationData, m_animData);
@ -65,19 +93,23 @@ namespace ogfx
OSTD_PARAM_GETSET(Color, IconTintColor, m_iconTint);
private:
// Force a layout recompute on the next draw.
inline void invalidate_layout(void) { m_layoutDirty = true; }
// 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.
// Re-runs the size + content computation. What this does depends
// entirely on effectiveSizeMode() — see the cpp for details.
void recompute_layout(void);
// 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;
// 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 { "" };
@ -88,19 +120,23 @@ namespace ogfx
f32 m_iconSpacing { 10 };
Color m_iconTint { Colors::White };
// 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
// ===== Mode =====
eSizeMode m_sizeMode { eSizeMode::AutoSize };
bool m_modeExplicitlySet { false };
// ================
// Flags
bool m_autoSize { true };
// ===== 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

@ -24,14 +24,12 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
CheckBox& CheckBox::create(const String& text)
{
setText(text);
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::CheckBox");
setTypeName("ogfx::gui::CheckBox");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("checkbox");
@ -94,4 +92,3 @@ namespace ogfx
}
}
}
}

View file

@ -27,9 +27,6 @@ namespace ogfx
{
namespace gui
{
namespace widgets
{
class CheckBox : public Widget
{
public:
@ -67,4 +64,3 @@ namespace ogfx
};
}
}
}

View file

@ -25,13 +25,11 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
Panel& Panel::create(void)
{
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::Panel");
setTypeName("ogfx::gui::Panel");
disableFocus();
enableStopEvents();
setStylesheetCategoryName("panel");
@ -164,7 +162,7 @@ namespace ogfx
TabPanel& TabPanel::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::TabPanel");
setTypeName("ogfx::gui::TabPanel");
disableFocus();
enableStopEvents();
setStylesheetCategoryName("tabPanel");
@ -366,4 +364,3 @@ namespace ogfx
}
}
}
}

View file

@ -27,8 +27,6 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
class Panel : public ScrollableWidget
{
@ -123,4 +121,3 @@ namespace ogfx
};
}
}
}

View file

@ -22,57 +22,110 @@
#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)
{
setText(text);
m_text = text;
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::Label");
setTypeName("ogfx::gui::Label");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("label");
validate();
invalidate_layout();
return *this;
}
void Label::onDraw(ogfx::BasicRenderer2D& gfx)
void Label::setSizeMode(eSizeMode mode)
{
if (m_textChanged)
__update_size(gfx);
gfx.drawString(getText(), getGlobalContentPosition(), getTextColor(), getFontSize());
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;
m_textChanged = true;
invalidate_layout();
}
void Label::__update_size(ogfx::BasicRenderer2D& gfx)
void Label::onDraw(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;
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)
{
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 };
}
else
{
// 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::widgets::ImageLabel");
setTypeName("ogfx::gui::ImageLabel");
disableChildren();
disableBackground();
disableBorder();
@ -90,7 +143,11 @@ namespace ogfx
String filePath = getThemeValue<String>(theme, "path", m_image.getFilePath());
if (filePath != m_image.getFilePath())
setImage(filePath);
setSize(getThemeValue<Vec2>(theme, "size", getSize()));
// 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())
{
m_anim.create(m_animData);
@ -98,20 +155,6 @@ namespace ogfx
}
}
void ImageLabel::onDraw(ogfx::BasicRenderer2D& gfx)
{
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;
@ -121,6 +164,85 @@ namespace ogfx
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)
{
const auto frame = m_anim.getFrameRect();
nat = { frame.w, frame.h };
}
else
{
nat = m_image.getSize();
}
}
if (m_fit == eImageFit::Fit)
{
if (!m_keepAspect)
{
// Stretch to fill, no aspect preservation.
outPos = contentTL;
outSize = contentSize;
return;
}
// 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
{
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 onBoundsChanged(const Vec2& newSize) override;
void setText(const String& text);
inline String getText(void) const { return m_text; }
// Same semantics as Button::setSizeMode / effectiveSizeMode.
void setSizeMode(eSizeMode mode);
inline eSizeMode getSizeMode(void) const { return m_sizeMode; }
eSizeMode effectiveSizeMode(void) const;
private:
void __update_size(ogfx::BasicRenderer2D& gfx);
inline void invalidate_layout(void) { m_layoutDirty = true; }
void recompute_layout(void);
private:
String m_text { "" };
bool m_textChanged { false };
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& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
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

@ -26,8 +26,6 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
void ListView::Item::setText(const String& text)
{
@ -86,7 +84,7 @@ namespace ogfx
ListView& ListView::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::ListView");
setTypeName("ogfx::gui::ListView");
enableStopEvents();
enableBackground();
enableBorder();
@ -272,4 +270,3 @@ namespace ogfx
}
}
}
}

View file

@ -27,8 +27,6 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
class ListView : public ScrollableWidget
{
@ -116,4 +114,3 @@ namespace ogfx
};
}
}
}

View file

@ -24,13 +24,11 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
ProgressBar& ProgressBar::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::ProgressBar");
setTypeName("ogfx::gui::ProgressBar");
disableChildren();
enableBackground();
enableBorder();
@ -94,4 +92,3 @@ namespace ogfx
}
}
}
}

View file

@ -26,8 +26,6 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
class ProgressBar : public Widget
{
@ -61,4 +59,3 @@ namespace ogfx
};
}
}
}

View file

@ -24,15 +24,13 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
RadioButton& RadioButton::create(RadioButtonGroup& group, const String& text)
{
m_radioGroup = &group;
setText(text);
setPadding({ 5, 5, 5, 5 });
setTypeName("ogfx::gui::widgets::RadioButton");
setTypeName("ogfx::gui::RadioButton");
disableChildren();
enableBackground(false);
setStylesheetCategoryName("radioButton");
@ -144,4 +142,3 @@ namespace ogfx
}
}
}
}

View file

@ -25,8 +25,6 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
class RadioButtonGroup;
class RadioButton : public Widget
@ -82,4 +80,3 @@ namespace ogfx
};
}
}
}

View file

@ -25,15 +25,13 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
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");
setTypeName("ogfx::gui::RootWidget");
}
void RootWidget::onWindowResized(const Event& event)
@ -65,4 +63,3 @@ namespace ogfx
}
}
}
}

View file

@ -25,8 +25,6 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
class RootWidget : public Widget
{
@ -50,4 +48,3 @@ namespace ogfx
};
}
}
}

View file

@ -24,14 +24,12 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
VerticalScrollBar& VerticalScrollBar::create(void)
{
setPadding({ 0, 0, 0, 0 });
setMargin({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::Label");
setTypeName("ogfx::gui::Label");
disableChildren();
enableBackground(true);
enableBorder(false);
@ -154,7 +152,7 @@ namespace ogfx
{
setPadding({ 0, 0, 0, 0 });
setMargin({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::Label");
setTypeName("ogfx::gui::Label");
disableChildren();
enableBackground(true);
enableBorder(false);
@ -276,7 +274,7 @@ namespace ogfx
ScrollableWidget& ScrollableWidget::create(void)
{
setTypeName("ogfx::gui::widgets::ScrollableWidget");
setTypeName("ogfx::gui::ScrollableWidget");
enableVScroll(true);
enableHScroll(true);
m_vScrollbar.enableManualDraw(true);
@ -464,4 +462,3 @@ namespace ogfx
}
}
}
}

View file

@ -26,8 +26,6 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
class VerticalScrollBar : public Widget
{
@ -146,4 +144,3 @@ namespace ogfx
};
}
}
}

View file

@ -24,13 +24,11 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
Slider& Slider::create(bool vertical, f32 min, f32 max, f32 step)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::Slider");
setTypeName("ogfx::gui::Slider");
disableChildren();
disableBackground();
disableBorder();
@ -190,4 +188,3 @@ namespace ogfx
}
}
}
}

View file

@ -25,8 +25,6 @@
namespace ogfx
{
namespace gui
{
namespace widgets
{
class Slider : public Widget
{
@ -92,4 +90,3 @@ namespace ogfx
};
}
}
}

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 });