Added layouts
This commit is contained in:
parent
a7265febdf
commit
3663ac69fd
33 changed files with 3168 additions and 2269 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
332
src/ogfx/gui/Layout.cpp
Normal 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
145
src/ogfx/gui/Layout.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
80
src/ogfx/gui/LayoutHint.hpp
Normal file
80
src/ogfx/gui/LayoutHint.hpp
Normal 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 };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
// =================
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,6 @@ namespace ogfx
|
|||
{
|
||||
namespace gui
|
||||
{
|
||||
namespace widgets
|
||||
{
|
||||
|
||||
class CheckBox : public Widget
|
||||
{
|
||||
public:
|
||||
|
|
@ -67,4 +64,3 @@ namespace ogfx
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@
|
|||
namespace ogfx
|
||||
{
|
||||
namespace gui
|
||||
{
|
||||
namespace widgets
|
||||
{
|
||||
class Panel : public ScrollableWidget
|
||||
{
|
||||
|
|
@ -123,4 +121,3 @@ namespace ogfx
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@
|
|||
namespace ogfx
|
||||
{
|
||||
namespace gui
|
||||
{
|
||||
namespace widgets
|
||||
{
|
||||
class ListView : public ScrollableWidget
|
||||
{
|
||||
|
|
@ -116,4 +114,3 @@ namespace ogfx
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@
|
|||
namespace ogfx
|
||||
{
|
||||
namespace gui
|
||||
{
|
||||
namespace widgets
|
||||
{
|
||||
class ProgressBar : public Widget
|
||||
{
|
||||
|
|
@ -61,4 +59,3 @@ namespace ogfx
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@
|
|||
namespace ogfx
|
||||
{
|
||||
namespace gui
|
||||
{
|
||||
namespace widgets
|
||||
{
|
||||
class RadioButtonGroup;
|
||||
class RadioButton : public Widget
|
||||
|
|
@ -82,4 +80,3 @@ namespace ogfx
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@
|
|||
namespace ogfx
|
||||
{
|
||||
namespace gui
|
||||
{
|
||||
namespace widgets
|
||||
{
|
||||
class RootWidget : public Widget
|
||||
{
|
||||
|
|
@ -50,4 +48,3 @@ namespace ogfx
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@
|
|||
namespace ogfx
|
||||
{
|
||||
namespace gui
|
||||
{
|
||||
namespace widgets
|
||||
{
|
||||
class VerticalScrollBar : public Widget
|
||||
{
|
||||
|
|
@ -146,4 +144,3 @@ namespace ogfx
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@
|
|||
namespace ogfx
|
||||
{
|
||||
namespace gui
|
||||
{
|
||||
namespace widgets
|
||||
{
|
||||
class Slider : public Widget
|
||||
{
|
||||
|
|
@ -92,4 +90,3 @@ namespace ogfx
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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()))); }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Reference in a new issue