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/ContextMenu.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/MenuBar.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/ToolBar.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/Layout.cpp
|
||||||
|
|
||||||
# gui/widgets
|
# gui/widgets
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Widget.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Widget.cpp
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
***Add Dark Mode Default theme
|
***Add Dark Mode Default theme
|
||||||
***FIX: Optimize ListView
|
***FIX: Optimize ListView
|
||||||
***FIX: Limit FPS even when continuous events happen
|
***FIX: Limit FPS even when continuous events happen
|
||||||
Add theme caching
|
|
||||||
Implement cursors in Stylesheet
|
Implement cursors in Stylesheet
|
||||||
FIX: Window getting exponentially bigger when Desktop scale changes
|
FIX: Window getting exponentially bigger when Desktop scale changes
|
||||||
FIX: Refreshing scroll when content extent changes
|
FIX: Refreshing scroll when content extent changes
|
||||||
|
|
@ -47,6 +46,8 @@ Add Hex Editor widget
|
||||||
Add Message Boxes
|
Add Message Boxes
|
||||||
FIX: Widget Content extents increasing negatively not showing scrollbars
|
FIX: Widget Content extents increasing negatively not showing scrollbars
|
||||||
FIX: Button state getting hung after showing an openFileDialog
|
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
|
***MenuBar
|
||||||
***Toolbar
|
***Toolbar
|
||||||
***StatusBar
|
***StatusBar
|
||||||
|
***Layouts
|
||||||
|
***Vertical
|
||||||
|
***Horizontal
|
||||||
|
***Grid
|
||||||
|
***Fill
|
||||||
ComboBox
|
ComboBox
|
||||||
TreeView
|
TreeView
|
||||||
Layouts
|
|
||||||
Vertical
|
|
||||||
Horizontal
|
|
||||||
Grid
|
|
||||||
Fill
|
|
||||||
Text Input
|
Text Input
|
||||||
Text Area
|
Text Area
|
||||||
Numeric Field
|
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();
|
setRootChild();
|
||||||
setStylesheetCategoryName("toolbar");
|
setStylesheetCategoryName("toolbar");
|
||||||
setTypeName("ogfx::gui::widgets::ToolBar");
|
setTypeName("ogfx::gui::ToolBar");
|
||||||
auto& win = cast<Window&>(getWindow());
|
auto& win = cast<Window&>(getWindow());
|
||||||
w = win.getWindowWidth();
|
w = win.getWindowWidth();
|
||||||
h = m_height;
|
h = m_height;
|
||||||
|
|
@ -51,19 +51,18 @@ namespace ogfx
|
||||||
return *this;
|
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() });
|
m_buttons.push_back({ getWindow() });
|
||||||
auto& btn = m_buttons.back();
|
auto& btn = m_buttons.back();
|
||||||
btn.addThemeID("tool_button");
|
btn.addThemeID("tool_button");
|
||||||
btn.setIcon(iconPath);
|
btn.setIcon(iconPath);
|
||||||
btn.setIconSize({ m_height, m_height });
|
|
||||||
btn.setText(text);
|
btn.setText(text);
|
||||||
|
btn.setSizeMode(eSizeMode::LayoutManaged);
|
||||||
btn.setCallback(ogfx::gui::Widget::eCallback::ActionPerformed, callback);
|
btn.setCallback(ogfx::gui::Widget::eCallback::ActionPerformed, callback);
|
||||||
btn.enableIconOnly();
|
btn.enableIconOnly();
|
||||||
addWidget(btn, { m_height * (m_buttons.size() - 1) + pad, 0 });
|
btn.layoutHint().preferred = { m_height, m_height };
|
||||||
pad += 15;
|
addWidget(btn);
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ namespace ogfx
|
||||||
public:
|
public:
|
||||||
ToolBar(Window& window, bool statusbar = false);
|
ToolBar(Window& window, bool statusbar = false);
|
||||||
ToolBar& create(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 onWindowResized(const Event& event) override;
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
void onDraw(BasicRenderer2D& gfx) override;
|
void onDraw(BasicRenderer2D& gfx) override;
|
||||||
|
|
@ -59,7 +59,7 @@ namespace ogfx
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool m_disableButtonText { true };
|
bool m_disableButtonText { true };
|
||||||
std::deque<widgets::Button> m_buttons;
|
std::deque<Button> m_buttons;
|
||||||
bool m_isStatusBar { false };
|
bool m_isStatusBar { false };
|
||||||
|
|
||||||
f32 m_height { 26 };
|
f32 m_height { 26 };
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,7 @@ namespace ogfx
|
||||||
void addWidget(Widget& widget, const Vec2& position = { 0, 0 });
|
void addWidget(Widget& widget, const Vec2& position = { 0, 0 });
|
||||||
void setTheme(const ostd::Stylesheet& theme) override;
|
void setTheme(const ostd::Stylesheet& theme) override;
|
||||||
inline void showContextMenu(const Vec2& pos) { m_cmenu.show(pos); }
|
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 void setContextMenu(const ContextMenu::Instance& instance) { m_cmenu.setInstance(instance); }
|
||||||
inline bool isContextMenuVisible(void) const { return m_cmenu.isVisible(); }
|
inline bool isContextMenuVisible(void) const { return m_cmenu.isVisible(); }
|
||||||
inline bool isMenuBarVisible(void) const { return m_menubar.isVisible(); }
|
inline bool isMenuBarVisible(void) const { return m_menubar.isVisible(); }
|
||||||
|
|
@ -306,7 +307,7 @@ namespace ogfx
|
||||||
void __main_loop_cycle(void);
|
void __main_loop_cycle(void);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
widgets::RootWidget m_rootWidget { *this };
|
RootWidget m_rootWidget { *this };
|
||||||
ostd::StepTimer m_fixedUpdateTimer;
|
ostd::StepTimer m_fixedUpdateTimer;
|
||||||
ostd::StepTimer::TimePoint m_lastFrameTime;
|
ostd::StepTimer::TimePoint m_lastFrameTime;
|
||||||
ostd::StepTimer m_mainLoopTimer;
|
ostd::StepTimer m_mainLoopTimer;
|
||||||
|
|
@ -315,7 +316,7 @@ namespace ogfx
|
||||||
ToolBar m_toolbar { *this };
|
ToolBar m_toolbar { *this };
|
||||||
ToolBar m_statusbar { *this, true };
|
ToolBar m_statusbar { *this, true };
|
||||||
|
|
||||||
friend class widgets::RootWidget;
|
friend class RootWidget;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,236 +22,314 @@
|
||||||
#include "../../render/BasicRenderer.hpp"
|
#include "../../render/BasicRenderer.hpp"
|
||||||
#include "../../../ostd/io/FileSystem.hpp"
|
#include "../../../ostd/io/FileSystem.hpp"
|
||||||
#include "../Window.hpp"
|
#include "../Window.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace ogfx
|
namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
Button& Button::create(const String& text)
|
||||||
{
|
{
|
||||||
Button& Button::create(const String& text)
|
m_text = text;
|
||||||
{
|
setPadding({ 5, 5, 5, 5 });
|
||||||
m_text = text;
|
setTypeName("ogfx::gui::Button");
|
||||||
setPadding({ 5, 5, 5, 5 });
|
disableChildren();
|
||||||
setTypeName("ogfx::gui::widgets::Button");
|
enableBackground(false);
|
||||||
disableChildren();
|
setStylesheetCategoryName("button");
|
||||||
enableBackground(false);
|
validate();
|
||||||
setStylesheetCategoryName("button");
|
invalidate_layout();
|
||||||
validate();
|
return *this;
|
||||||
invalidate_layout();
|
}
|
||||||
return *this;
|
|
||||||
}
|
// ===================================================================
|
||||||
|
// Mode handling
|
||||||
void Button::onDraw(ogfx::BasicRenderer2D& gfx)
|
// ===================================================================
|
||||||
{
|
void Button::setSizeMode(eSizeMode mode)
|
||||||
if (m_layoutDirty)
|
{
|
||||||
recompute_layout();
|
m_sizeMode = mode;
|
||||||
|
m_modeExplicitlySet = true;
|
||||||
const Vec2 contentTL = getGlobalContentPosition();
|
invalidate_layout();
|
||||||
|
}
|
||||||
// --- Icon ---
|
|
||||||
if (isIconEnabled() && m_drawIconSize.x > 0 && m_drawIconSize.y > 0)
|
eSizeMode Button::effectiveSizeMode(void) const
|
||||||
{
|
{
|
||||||
const Vec2 iconPos = contentTL + m_drawIconOffset;
|
// Auto-detect: if user never set a mode explicitly and our parent
|
||||||
if (isAnimatedEnabled())
|
// owns a layout, we run as LayoutManaged. Otherwise honor the
|
||||||
gfx.drawAnimation(m_anim, iconPos, m_drawIconSize, getIconTintColor());
|
// stored mode (AutoSize by default).
|
||||||
else
|
if (!m_modeExplicitlySet && getParent() && getParent()->hasLayout())
|
||||||
gfx.drawImage(m_icon, iconPos, m_drawIconSize, getIconTintColor());
|
return eSizeMode::LayoutManaged;
|
||||||
}
|
return m_sizeMode;
|
||||||
|
}
|
||||||
// --- Text ---
|
|
||||||
if (!isIconOnlyEnabled() && m_text.len() > 0)
|
// ===================================================================
|
||||||
{
|
// Lifecycle hooks
|
||||||
gfx.drawString(
|
// ===================================================================
|
||||||
m_text,
|
void Button::onBoundsChanged(const Vec2& /*newSize*/)
|
||||||
contentTL + m_drawTextOffset,
|
{
|
||||||
getTextColor(),
|
// Layouts call this after they assign new bounds. Recompute the
|
||||||
getFontSize(),
|
// content placement so it stays centered inside the new size.
|
||||||
m_contentScale
|
invalidate_layout();
|
||||||
);
|
}
|
||||||
}
|
|
||||||
}
|
void Button::onMouseReleased(const Event& event)
|
||||||
|
{
|
||||||
void Button::onUpdate(void)
|
if (!isMouseInside())
|
||||||
|
return;
|
||||||
|
if (event.mouse->button == MouseEventData::eButton::Left && callback_onActionPerformed)
|
||||||
|
callback_onActionPerformed(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::onUpdate(void)
|
||||||
|
{
|
||||||
|
if (isAnimatedEnabled())
|
||||||
|
m_anim.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
if (m_layoutDirty)
|
||||||
|
recompute_layout();
|
||||||
|
|
||||||
|
const Vec2 contentTL = getGlobalContentPosition();
|
||||||
|
|
||||||
|
// --- Icon ---
|
||||||
|
if (isIconEnabled() && m_drawIconSize.x > 0 && m_drawIconSize.y > 0)
|
||||||
{
|
{
|
||||||
|
const Vec2 iconPos = contentTL + m_drawIconOffset;
|
||||||
if (isAnimatedEnabled())
|
if (isAnimatedEnabled())
|
||||||
m_anim.update();
|
gfx.drawAnimation(m_anim, iconPos, m_drawIconSize, getIconTintColor());
|
||||||
|
else
|
||||||
|
gfx.drawImage(m_icon, iconPos, m_drawIconSize, getIconTintColor());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Button::onMouseReleased(const Event& event)
|
// --- Text ---
|
||||||
|
if (!isIconOnlyEnabled() && m_text.len() > 0)
|
||||||
{
|
{
|
||||||
if (!isMouseInside())
|
gfx.drawString(
|
||||||
return;
|
m_text,
|
||||||
if (event.mouse->button == MouseEventData::eButton::Left && callback_onActionPerformed)
|
contentTL + m_drawTextOffset,
|
||||||
callback_onActionPerformed(event);
|
getTextColor(),
|
||||||
|
getFontSize()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Button::setText(const String& text)
|
// ===================================================================
|
||||||
|
// Setters that trigger a recompute
|
||||||
|
// ===================================================================
|
||||||
|
void Button::setText(const String& text)
|
||||||
|
{
|
||||||
|
m_text = text;
|
||||||
|
invalidate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::setIcon(const String& filePath)
|
||||||
|
{
|
||||||
|
disableIcon();
|
||||||
|
if (filePath == "") { invalidate_layout(); return; }
|
||||||
|
if (!ostd::FileSystem::fileExists(filePath)) { invalidate_layout(); return; }
|
||||||
|
m_icon.destroy();
|
||||||
|
m_icon.loadFromFile(filePath, getWindow().getGFX());
|
||||||
|
enableIcon();
|
||||||
|
invalidate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::applyTheme(const ostd::Stylesheet& theme)
|
||||||
|
{
|
||||||
|
enableIcon(getThemeValue<bool>(theme, "showIcon", m_showIcon));
|
||||||
|
enableIconOnly(getThemeValue<bool>(theme, "iconOnly", m_iconOnly));
|
||||||
|
enableAnimated(getThemeValue<bool>(theme, "icon.animated", m_animated));
|
||||||
|
setAnimationData(getThemeValue<AnimationData>(theme, "icon.animation", m_animData));
|
||||||
|
setIconTintColor(getThemeValue<Color>(theme, "icon.tint", getIconTintColor()));
|
||||||
|
String filePath = getThemeValue<String>(theme, "icon.path", m_icon.getFilePath());
|
||||||
|
if (filePath != m_icon.getFilePath())
|
||||||
|
setIcon(filePath);
|
||||||
|
setIconSize(getThemeValue<Vec2>(theme, "icon.size", getIconSize()));
|
||||||
|
setIconSpacing(getThemeValue<f32>(theme, "icon.spacing", m_iconSpacing));
|
||||||
|
if (isAnimatedEnabled())
|
||||||
{
|
{
|
||||||
m_text = text;
|
m_anim.create(m_animData);
|
||||||
invalidate_layout();
|
m_anim.setSpriteSheet(m_icon);
|
||||||
}
|
}
|
||||||
|
invalidate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
void Button::setIcon(const String& filePath)
|
// ===================================================================
|
||||||
|
// Icon size helpers
|
||||||
|
// ===================================================================
|
||||||
|
Vec2 Button::icon_floor(void) const
|
||||||
|
{
|
||||||
|
// Configurable hint floor: if the user set m_iconSize, use it as the
|
||||||
|
// minimum icon dimensions. Otherwise fall back to font size square.
|
||||||
|
const f32 fontEdge = (f32)getFontSize();
|
||||||
|
|
||||||
|
if (m_iconSize.x > 0 && m_iconSize.y > 0)
|
||||||
|
return m_iconSize;
|
||||||
|
if (m_iconSize.x > 0)
|
||||||
|
return { m_iconSize.x, m_iconSize.x };
|
||||||
|
if (m_iconSize.y > 0)
|
||||||
|
return { m_iconSize.y, m_iconSize.y };
|
||||||
|
return { fontEdge, fontEdge };
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec2 Button::compute_icon_size(f32 textHeightHint) const
|
||||||
|
{
|
||||||
|
if (!isIconEnabled())
|
||||||
|
return { 0, 0 };
|
||||||
|
|
||||||
|
const Vec2 floor = icon_floor();
|
||||||
|
|
||||||
|
// Target height: max(text height, floor.y). Width derives from
|
||||||
|
// the icon's natural aspect ratio if the icon is loaded; otherwise
|
||||||
|
// match the floor.
|
||||||
|
f32 targetH = std::max(textHeightHint, floor.y);
|
||||||
|
|
||||||
|
if (m_icon.isLoaded())
|
||||||
{
|
{
|
||||||
disableIcon();
|
// When the icon is animated, the "natural size" is one frame,
|
||||||
if (filePath == "") { invalidate_layout(); return; }
|
// NOT the whole sprite sheet — the sheet's aspect ratio is
|
||||||
if (!ostd::FileSystem::fileExists(filePath)) { invalidate_layout(); return; }
|
// meaningless (it depends on column/row count). Same logic
|
||||||
m_icon.destroy();
|
// as ImageLabel::compute_draw_geometry.
|
||||||
m_icon.loadFromFile(filePath, getWindow().getGFX());
|
Vec2 nat;
|
||||||
enableIcon();
|
if (m_animated)
|
||||||
invalidate_layout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Button::applyTheme(const ostd::Stylesheet& theme)
|
|
||||||
{
|
|
||||||
enableIcon(getThemeValue<bool>(theme, "showIcon", m_showIcon));
|
|
||||||
enableIconOnly(getThemeValue<bool>(theme, "iconOnly", m_iconOnly));
|
|
||||||
enableAutoSize(getThemeValue<bool>(theme, "autoSize", m_autoSize));
|
|
||||||
enableAnimated(getThemeValue<bool>(theme, "icon.animated", m_animated));
|
|
||||||
setAnimationData(getThemeValue<AnimationData>(theme, "icon.animation", m_animData));
|
|
||||||
setIconTintColor(getThemeValue<Color>(theme, "icon.tint", getIconTintColor()));
|
|
||||||
String filePath = getThemeValue<String>(theme, "icon.path", m_icon.getFilePath());
|
|
||||||
if (filePath != m_icon.getFilePath())
|
|
||||||
setIcon(filePath);
|
|
||||||
setIconSize(getThemeValue<Vec2>(theme, "icon.size", getIconSize()));
|
|
||||||
setIconSpacing(getThemeValue<f32>(theme, "icon.spacing", m_iconSpacing));
|
|
||||||
if (isAnimatedEnabled())
|
|
||||||
{
|
{
|
||||||
m_anim.create(m_animData);
|
const auto frame = m_anim.getFrameRect();
|
||||||
m_anim.setSpriteSheet(m_icon);
|
nat = { frame.w, frame.h };
|
||||||
}
|
|
||||||
invalidate_layout();
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec2 Button::native_icon_size(void) const
|
|
||||||
{
|
|
||||||
if (!isIconEnabled())
|
|
||||||
return { 0, 0 };
|
|
||||||
|
|
||||||
// Treat font size as the natural icon edge if the user didn't pick one.
|
|
||||||
const f32 fontEdge = cast<f32>(getFontSize());
|
|
||||||
|
|
||||||
Vec2 hint = m_iconSize;
|
|
||||||
if (hint.x <= 0 && hint.y <= 0)
|
|
||||||
{
|
|
||||||
if (m_icon.isLoaded())
|
|
||||||
{
|
|
||||||
// Fit the icon's natural aspect ratio into a font-sized square.
|
|
||||||
Vec2 nat = m_icon.getSize();
|
|
||||||
if (nat.x > 0 && nat.y > 0)
|
|
||||||
{
|
|
||||||
if (nat.x >= nat.y)
|
|
||||||
return nat.propx(fontEdge);
|
|
||||||
return nat.propy(fontEdge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { fontEdge, fontEdge };
|
|
||||||
}
|
|
||||||
|
|
||||||
// One axis specified: derive the other from the icon's aspect (if any),
|
|
||||||
// otherwise make it square.
|
|
||||||
if (hint.x <= 0)
|
|
||||||
{
|
|
||||||
if (m_icon.isLoaded())
|
|
||||||
{
|
|
||||||
Vec2 nat = m_icon.getSize();
|
|
||||||
if (nat.x > 0 && nat.y > 0)
|
|
||||||
return nat.propy(hint.y);
|
|
||||||
}
|
|
||||||
return { hint.y, hint.y };
|
|
||||||
}
|
|
||||||
if (hint.y <= 0)
|
|
||||||
{
|
|
||||||
if (m_icon.isLoaded())
|
|
||||||
{
|
|
||||||
Vec2 nat = m_icon.getSize();
|
|
||||||
if (nat.x > 0 && nat.y > 0)
|
|
||||||
return nat.propx(hint.x);
|
|
||||||
}
|
|
||||||
return { hint.x, hint.x };
|
|
||||||
}
|
|
||||||
return hint;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Button::recompute_layout(void)
|
|
||||||
{
|
|
||||||
m_layoutDirty = false;
|
|
||||||
|
|
||||||
auto& gfx = getWindow().getGFX();
|
|
||||||
const Rectangle pad = getPadding();
|
|
||||||
const f32 padW = pad.left() + pad.right();
|
|
||||||
const f32 padH = pad.top() + pad.bottom();
|
|
||||||
|
|
||||||
// 1) Native (un-scaled) sizes of the pieces.
|
|
||||||
const Vec2 iconNative = native_icon_size();
|
|
||||||
const bool hasIcon = isIconEnabled() && (iconNative.x > 0 && iconNative.y > 0);
|
|
||||||
const bool hasText = !isIconOnlyEnabled() && m_text.len() > 0;
|
|
||||||
const Vec2 textNative = hasText
|
|
||||||
? gfx.getStringDimensions(m_text, getFontSize())
|
|
||||||
: Vec2 { 0, 0 };
|
|
||||||
|
|
||||||
// 2) Native content extents (icon + spacing + text), no padding yet.
|
|
||||||
f32 nativeW = 0;
|
|
||||||
f32 nativeH = 0;
|
|
||||||
if (hasIcon)
|
|
||||||
{
|
|
||||||
nativeW += iconNative.x;
|
|
||||||
nativeH = std::max(nativeH, iconNative.y);
|
|
||||||
}
|
|
||||||
if (hasText)
|
|
||||||
{
|
|
||||||
if (hasIcon) nativeW += m_iconSpacing;
|
|
||||||
nativeW += textNative.x;
|
|
||||||
nativeH = std::max(nativeH, textNative.y);
|
|
||||||
}
|
|
||||||
if (nativeW <= 0) nativeW = cast<f32>(getFontSize()); // degenerate fallback
|
|
||||||
if (nativeH <= 0) nativeH = cast<f32>(getFontSize());
|
|
||||||
|
|
||||||
// 3) Decide the widget size and the content scale.
|
|
||||||
if (isAutoSizeEnabled())
|
|
||||||
{
|
|
||||||
m_contentScale = 1.0f;
|
|
||||||
setSize({ nativeW + padW, nativeH + padH });
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Honor the size the user already set on the widget.
|
nat = m_icon.getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nat.x > 0 && nat.y > 0)
|
||||||
|
{
|
||||||
|
const f32 aspect = nat.x / nat.y;
|
||||||
|
return { targetH * aspect, targetH };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No natural size known — fall back to whichever is wider.
|
||||||
|
return { std::max(floor.x, targetH), targetH };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Layout recompute
|
||||||
|
//
|
||||||
|
// Branches by effective mode:
|
||||||
|
//
|
||||||
|
// AutoSize:
|
||||||
|
// 1. Measure text at native font size.
|
||||||
|
// 2. Compute icon size: height = max(textH, floorH), aspect preserved.
|
||||||
|
// 3. Set widget size to fit padding + icon + spacing + text.
|
||||||
|
// 4. Place icon and text inside the content area.
|
||||||
|
//
|
||||||
|
// Manual / LayoutManaged:
|
||||||
|
// 1. Widget size is whatever it currently is (user / layout decided).
|
||||||
|
// 2. Measure text at native font size.
|
||||||
|
// 3. Icon size = m_iconSize hint (or font-size square if unset),
|
||||||
|
// aspect preserved against icon natural size.
|
||||||
|
// 4. Center the text+icon group inside the content area. Overflow
|
||||||
|
// is allowed and clipped at widget level (positions can go
|
||||||
|
// slightly outside if the user picked sizes that don't fit).
|
||||||
|
// ===================================================================
|
||||||
|
void Button::recompute_layout(void)
|
||||||
|
{
|
||||||
|
m_layoutDirty = false;
|
||||||
|
|
||||||
|
auto& gfx = getWindow().getGFX();
|
||||||
|
const Rectangle pad = getPadding();
|
||||||
|
const f32 padW = pad.left() + pad.right();
|
||||||
|
const f32 padH = pad.top() + pad.bottom();
|
||||||
|
|
||||||
|
// 1) Text dimensions (native font size in all modes).
|
||||||
|
const bool hasText = !isIconOnlyEnabled() && m_text.len() > 0;
|
||||||
|
const Vec2 textSize = hasText
|
||||||
|
? gfx.getStringDimensions(m_text, getFontSize())
|
||||||
|
: Vec2 { 0, 0 };
|
||||||
|
|
||||||
|
// 2) Icon dimensions.
|
||||||
|
const eSizeMode mode = effectiveSizeMode();
|
||||||
|
Vec2 iconSize { 0, 0 };
|
||||||
|
if (isIconEnabled())
|
||||||
|
{
|
||||||
|
if (mode == eSizeMode::AutoSize)
|
||||||
|
{
|
||||||
|
// Floor from m_iconSize hint (or font size); rise to text height.
|
||||||
|
iconSize = compute_icon_size(textSize.y);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Manual / LayoutManaged: just use the configured hint, no
|
||||||
|
// floor-against-text logic. If unset, use font size square.
|
||||||
|
iconSize = compute_icon_size(0.0f);
|
||||||
|
|
||||||
|
// Cap the icon to the content area so padding is always
|
||||||
|
// respected. Aspect ratio is preserved during the cap.
|
||||||
|
// AutoSize doesn't need this because it grows the widget
|
||||||
|
// to accommodate the icon + padding.
|
||||||
const f32 availW = std::max(0.0f, getw() - padW);
|
const f32 availW = std::max(0.0f, getw() - padW);
|
||||||
const f32 availH = std::max(0.0f, geth() - padH);
|
const f32 availH = std::max(0.0f, geth() - padH);
|
||||||
if (availW <= 0 || availH <= 0)
|
if (iconSize.x > 0 && iconSize.y > 0
|
||||||
m_contentScale = 0.0f;
|
&& (iconSize.x > availW || iconSize.y > availH))
|
||||||
else
|
|
||||||
{
|
{
|
||||||
const f32 sx = availW / nativeW;
|
const f32 sx = (iconSize.x > 0) ? availW / iconSize.x : 1.0f;
|
||||||
const f32 sy = availH / nativeH;
|
const f32 sy = (iconSize.y > 0) ? availH / iconSize.y : 1.0f;
|
||||||
m_contentScale = std::min(sx, sy);
|
const f32 s = std::min(sx, sy);
|
||||||
|
iconSize = { iconSize.x * s, iconSize.y * s };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Compute scaled draw sizes and centered offsets within the content area.
|
|
||||||
const Vec2 contentSize = getContentBounds().getSize();
|
|
||||||
const Vec2 iconScaled = hasIcon ? iconNative * m_contentScale : Vec2 { 0, 0 };
|
|
||||||
const Vec2 textScaled = hasText ? textNative * m_contentScale : Vec2 { 0, 0 };
|
|
||||||
const f32 spacing = (hasIcon && hasText) ? (m_iconSpacing * m_contentScale) : 0.0f;
|
|
||||||
|
|
||||||
const f32 totalW = iconScaled.x + spacing + textScaled.x;
|
|
||||||
const f32 startX = std::max(0.0f, (contentSize.x - totalW) * 0.5f);
|
|
||||||
|
|
||||||
m_drawIconSize = iconScaled;
|
|
||||||
if (hasIcon)
|
|
||||||
m_drawIconOffset = { startX, std::max(0.0f, (contentSize.y - iconScaled.y) * 0.5f) };
|
|
||||||
else
|
|
||||||
m_drawIconOffset = { 0, 0 };
|
|
||||||
|
|
||||||
if (hasText)
|
|
||||||
m_drawTextOffset = {
|
|
||||||
startX + iconScaled.x + spacing,
|
|
||||||
std::max(0.0f, (contentSize.y - textScaled.y) * 0.5f)
|
|
||||||
};
|
|
||||||
else
|
|
||||||
m_drawTextOffset = { 0, 0 };
|
|
||||||
}
|
}
|
||||||
|
const bool hasIcon = iconSize.x > 0 && iconSize.y > 0;
|
||||||
|
|
||||||
|
// 3) Native content extents (icon + spacing + text), no padding yet.
|
||||||
|
f32 contentNativeW = 0;
|
||||||
|
f32 contentNativeH = 0;
|
||||||
|
if (hasIcon)
|
||||||
|
{
|
||||||
|
contentNativeW += iconSize.x;
|
||||||
|
contentNativeH = std::max(contentNativeH, iconSize.y);
|
||||||
|
}
|
||||||
|
if (hasText)
|
||||||
|
{
|
||||||
|
if (hasIcon) contentNativeW += m_iconSpacing;
|
||||||
|
contentNativeW += textSize.x;
|
||||||
|
contentNativeH = std::max(contentNativeH, textSize.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Decide widget size (AutoSize only) or honor the existing one.
|
||||||
|
if (mode == eSizeMode::AutoSize)
|
||||||
|
{
|
||||||
|
const f32 fallback = (f32)getFontSize();
|
||||||
|
const f32 wantW = (contentNativeW > 0 ? contentNativeW : fallback) + padW;
|
||||||
|
const f32 wantH = (contentNativeH > 0 ? contentNativeH : fallback) + padH;
|
||||||
|
|
||||||
|
// Only call setSize if the size actually changed — avoids the
|
||||||
|
// hover-flicker cycle (theme reload -> applyTheme ->
|
||||||
|
// invalidate_layout -> recompute -> setSize -> hover boundary
|
||||||
|
// crosses -> ... ).
|
||||||
|
if (std::abs(getw() - wantW) > 0.5f || std::abs(geth() - wantH) > 0.5f)
|
||||||
|
setSize({ wantW, wantH });
|
||||||
|
}
|
||||||
|
// else: Manual & LayoutManaged keep current widget size.
|
||||||
|
|
||||||
|
// 5) Place icon and text inside the content area, centered.
|
||||||
|
const Vec2 contentSize = getContentBounds().getSize();
|
||||||
|
const f32 spacing = (hasIcon && hasText) ? m_iconSpacing : 0.0f;
|
||||||
|
const f32 totalW = (hasIcon ? iconSize.x : 0) + spacing + (hasText ? textSize.x : 0);
|
||||||
|
const f32 startX = (contentSize.x - totalW) * 0.5f;
|
||||||
|
|
||||||
|
m_drawIconSize = iconSize;
|
||||||
|
if (hasIcon)
|
||||||
|
m_drawIconOffset = { startX, (contentSize.y - iconSize.y) * 0.5f };
|
||||||
|
else
|
||||||
|
m_drawIconOffset = { 0, 0 };
|
||||||
|
|
||||||
|
if (hasText)
|
||||||
|
m_drawTextOffset = {
|
||||||
|
startX + (hasIcon ? iconSize.x + spacing : 0.0f),
|
||||||
|
(contentSize.y - textSize.y) * 0.5f
|
||||||
|
};
|
||||||
|
else
|
||||||
|
m_drawTextOffset = { 0, 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,79 +28,115 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
// ===========================================================================
|
||||||
|
// eSizeMode — shared across Button / Label / (and analogous on ImageLabel).
|
||||||
|
//
|
||||||
|
// AutoSize: widget computes its own size to fit content. Used when
|
||||||
|
// there's no layout managing it, and the user wants the
|
||||||
|
// widget to "just be the right size".
|
||||||
|
//
|
||||||
|
// Manual: user controls the widget's size; user also controls the
|
||||||
|
// content sizes (text/icon). Nothing is computed automatically.
|
||||||
|
// Content is drawn at its user-specified size, centered, and
|
||||||
|
// clipped at the widget edge if it doesn't fit.
|
||||||
|
//
|
||||||
|
// LayoutManaged: parent's Layout controls the widget's size. Content
|
||||||
|
// (text/icon) is at its user-specified size, and the widget
|
||||||
|
// honors whatever bounds the layout assigns.
|
||||||
|
// This mode is auto-selected when added to a parent that
|
||||||
|
// has a layout — unless the user explicitly set Manual.
|
||||||
|
// ===========================================================================
|
||||||
|
enum class eSizeMode : u8
|
||||||
{
|
{
|
||||||
class Button : public Widget
|
AutoSize,
|
||||||
{
|
Manual,
|
||||||
public:
|
LayoutManaged
|
||||||
inline Button(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
|
};
|
||||||
inline Button(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
|
|
||||||
Button& create(const String& text);
|
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
|
||||||
void onUpdate(void) override;
|
|
||||||
void onMouseReleased(const Event& event) override;
|
|
||||||
void setText(const String& text);
|
|
||||||
void setIcon(const String& filePath);
|
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
|
||||||
inline String getText(void) const { return m_text; }
|
|
||||||
inline Image& getIcon(void) { return m_icon; }
|
|
||||||
|
|
||||||
// When auto-size is enabled, the widget resizes itself to fit text+icon+padding
|
class Button : public Widget
|
||||||
// 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
|
public:
|
||||||
// content is scaled uniformly down/up to fit that size (text uses the renderer's
|
inline Button(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
|
||||||
// scale parameter; icon is scaled proportionally).
|
inline Button(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
|
||||||
OSTD_BOOL_PARAM_GETSET_E(AutoSize, m_autoSize);
|
Button& create(const String& text);
|
||||||
|
|
||||||
// Icon-only mode: text is ignored when laying out and drawing.
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
// Combined with auto-size, the button becomes a square based on font size.
|
void onUpdate(void) override;
|
||||||
// Combined with manual size, the icon scales to fit the content area.
|
void onMouseReleased(const Event& event) override;
|
||||||
OSTD_BOOL_PARAM_GETSET_E(IconOnly, m_iconOnly);
|
void onBoundsChanged(const Vec2& newSize) override;
|
||||||
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
|
|
||||||
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
|
void setText(const String& text);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon);
|
void setIcon(const String& filePath);
|
||||||
OSTD_PARAM_GETSET(ostd::AnimationData, AnimationData, m_animData);
|
inline String getText(void) const { return m_text; }
|
||||||
OSTD_PARAM_GETSET(Vec2, IconSize, m_iconSize);
|
inline Image& getIcon(void) { return m_icon; }
|
||||||
OSTD_PARAM_GETSET(f32, IconSpacing, m_iconSpacing);
|
|
||||||
OSTD_PARAM_GETSET(Color, IconTintColor, m_iconTint);
|
|
||||||
|
|
||||||
private:
|
// =========================== SIZE MODE ============================
|
||||||
// Force a layout recompute on the next draw.
|
// Set the widget's size mode explicitly. After calling this, the
|
||||||
inline void invalidate_layout(void) { m_layoutDirty = true; }
|
// auto-detection (LayoutManaged when parent has a layout) is
|
||||||
|
// disabled — the user's choice is respected.
|
||||||
|
void setSizeMode(eSizeMode mode);
|
||||||
|
inline eSizeMode getSizeMode(void) const { return m_sizeMode; }
|
||||||
|
|
||||||
// Recomputes layout. In auto-size mode this also resizes the widget.
|
// The mode actually used by the layout/draw code. Resolves auto-
|
||||||
// In manual-size mode this computes the scale factor that fits content
|
// detection: if the user hasn't explicitly set a mode and the
|
||||||
// into the current widget size.
|
// parent has a layout, this returns LayoutManaged.
|
||||||
void recompute_layout(void);
|
eSizeMode effectiveSizeMode(void) const;
|
||||||
|
// ==================================================================
|
||||||
|
|
||||||
// Returns the icon size at native (un-scaled) resolution. If m_iconSize is
|
OSTD_BOOL_PARAM_GETSET_E(IconOnly, m_iconOnly);
|
||||||
// (0,0) and an icon is loaded, falls back to the icon's natural size; if no
|
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
|
||||||
// icon is loaded, falls back to font-size-based square. Aspect ratio of
|
OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon);
|
||||||
// m_iconSize is preserved.
|
OSTD_PARAM_GETSET(ostd::AnimationData, AnimationData, m_animData);
|
||||||
Vec2 native_icon_size(void) const;
|
OSTD_PARAM_GETSET(Vec2, IconSize, m_iconSize);
|
||||||
|
OSTD_PARAM_GETSET(f32, IconSpacing, m_iconSpacing);
|
||||||
|
OSTD_PARAM_GETSET(Color, IconTintColor, m_iconTint);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
String m_text { "" };
|
inline void invalidate_layout(void) { m_layoutDirty = true; }
|
||||||
Image m_icon;
|
|
||||||
ostd::AnimationData m_animData;
|
|
||||||
Animation m_anim;
|
|
||||||
Vec2 m_iconSize { 0, 0 };
|
|
||||||
f32 m_iconSpacing { 10 };
|
|
||||||
Color m_iconTint { Colors::White };
|
|
||||||
|
|
||||||
// Computed layout state (refreshed by recompute_layout).
|
// Re-runs the size + content computation. What this does depends
|
||||||
bool m_layoutDirty { true };
|
// entirely on effectiveSizeMode() — see the cpp for details.
|
||||||
f32 m_contentScale { 1.0f }; // applied to text & icon when !m_autoSize
|
void recompute_layout(void);
|
||||||
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
|
|
||||||
|
|
||||||
// Flags
|
// Returns the icon size to draw, given the current effective text
|
||||||
bool m_autoSize { true };
|
// height. In AutoSize: max(text-height, hintFloor) with aspect
|
||||||
bool m_iconOnly { false };
|
// preserved. In Manual/LayoutManaged: the user's m_iconSize hint
|
||||||
bool m_showIcon { false };
|
// (or fontSize fallback). Aspect ratio is always preserved when
|
||||||
bool m_animated { false };
|
// the icon is loaded with a known natural size.
|
||||||
};
|
Vec2 compute_icon_size(f32 textHeightHint) const;
|
||||||
}
|
|
||||||
|
// Native floor for the icon: m_iconSize if set, else font size.
|
||||||
|
// (User-configurable hint as discussed; the icon will not be
|
||||||
|
// drawn smaller than this in AutoSize mode.)
|
||||||
|
Vec2 icon_floor(void) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
String m_text { "" };
|
||||||
|
Image m_icon;
|
||||||
|
ostd::AnimationData m_animData;
|
||||||
|
Animation m_anim;
|
||||||
|
Vec2 m_iconSize { 0, 0 };
|
||||||
|
f32 m_iconSpacing { 10 };
|
||||||
|
Color m_iconTint { Colors::White };
|
||||||
|
|
||||||
|
// ===== Mode =====
|
||||||
|
eSizeMode m_sizeMode { eSizeMode::AutoSize };
|
||||||
|
bool m_modeExplicitlySet { false };
|
||||||
|
// ================
|
||||||
|
|
||||||
|
// ===== Computed draw state (refreshed by recompute_layout) =====
|
||||||
|
bool m_layoutDirty { true };
|
||||||
|
Vec2 m_drawIconSize { 0, 0 };
|
||||||
|
Vec2 m_drawIconOffset { 0, 0 };
|
||||||
|
Vec2 m_drawTextOffset { 0, 0 };
|
||||||
|
// ===============================================================
|
||||||
|
|
||||||
|
// ===== Flags =====
|
||||||
|
bool m_iconOnly { false };
|
||||||
|
bool m_showIcon { false };
|
||||||
|
bool m_animated { false };
|
||||||
|
// =================
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,73 +25,70 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
CheckBox& CheckBox::create(const String& text)
|
||||||
{
|
{
|
||||||
CheckBox& CheckBox::create(const String& text)
|
setText(text);
|
||||||
{
|
setPadding({ 5, 5, 5, 5 });
|
||||||
setText(text);
|
setTypeName("ogfx::gui::CheckBox");
|
||||||
setPadding({ 5, 5, 5, 5 });
|
disableChildren();
|
||||||
setTypeName("ogfx::gui::widgets::CheckBox");
|
enableBackground(false);
|
||||||
disableChildren();
|
setStylesheetCategoryName("checkbox");
|
||||||
enableBackground(false);
|
validate();
|
||||||
setStylesheetCategoryName("checkbox");
|
return *this;
|
||||||
validate();
|
}
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CheckBox::applyTheme(const ostd::Stylesheet& theme)
|
void CheckBox::applyTheme(const ostd::Stylesheet& theme)
|
||||||
{
|
{
|
||||||
setCheckBorderColor(getThemeValue<Color>(theme, "checkBorderColor", Colors::White));
|
setCheckBorderColor(getThemeValue<Color>(theme, "checkBorderColor", Colors::White));
|
||||||
setCheckBoxColor(getThemeValue<Color>(theme, "checkBoxColor", Colors::White));
|
setCheckBoxColor(getThemeValue<Color>(theme, "checkBoxColor", Colors::White));
|
||||||
m_checkBorderRadius = getThemeValue<i32>(theme, "checkBorderRadius", 5);
|
m_checkBorderRadius = getThemeValue<i32>(theme, "checkBorderRadius", 5);
|
||||||
m_checkBorderWidth = getThemeValue<i32>(theme, "checkBorderWidth", 1);
|
m_checkBorderWidth = getThemeValue<i32>(theme, "checkBorderWidth", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheckBox::onDraw(ogfx::BasicRenderer2D& gfx)
|
void CheckBox::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
{
|
{
|
||||||
if (m_textChanged)
|
if (m_textChanged)
|
||||||
__update_size(gfx);
|
__update_size(gfx);
|
||||||
gfx.drawRoundRect({ getGlobalContentPosition(), m_checkSize }, getCheckBorderColor(), m_checkBorderRadius, m_checkBorderWidth);
|
gfx.drawRoundRect({ getGlobalContentPosition(), m_checkSize }, getCheckBorderColor(), m_checkBorderRadius, m_checkBorderWidth);
|
||||||
if (isChecked())
|
if (isChecked())
|
||||||
gfx.fillRoundRect({ getGlobalContentPosition() + 5, m_checkSize - 10 }, getCheckBoxColor(), m_checkBorderRadius / 2.0f);
|
gfx.fillRoundRect({ getGlobalContentPosition() + 5, m_checkSize - 10 }, getCheckBoxColor(), m_checkBorderRadius / 2.0f);
|
||||||
gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_checkSize.x + m_spacing, 0 }, getTextColor(), getFontSize());
|
gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_checkSize.x + m_spacing, 0 }, getTextColor(), getFontSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheckBox::onMouseReleased(const Event& event)
|
void CheckBox::onMouseReleased(const Event& event)
|
||||||
{
|
{
|
||||||
if (!isMouseInside())
|
if (!isMouseInside())
|
||||||
return;
|
return;
|
||||||
setChecked(!isChecked());
|
setChecked(!isChecked());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheckBox::setText(const String& text)
|
void CheckBox::setText(const String& text)
|
||||||
{
|
{
|
||||||
m_text = text;
|
m_text = text;
|
||||||
m_textChanged = true;
|
m_textChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheckBox::setChecked(bool checked)
|
void CheckBox::setChecked(bool checked)
|
||||||
{
|
{
|
||||||
if (m_checked == checked)
|
if (m_checked == checked)
|
||||||
return;
|
return;
|
||||||
m_checked = checked;
|
m_checked = checked;
|
||||||
setThemeQualifier("active", m_checked);
|
setThemeQualifier("active", m_checked);
|
||||||
if (callback_onStateChanged)
|
if (callback_onStateChanged)
|
||||||
callback_onStateChanged(*this, m_checked);
|
callback_onStateChanged(*this, m_checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheckBox::__update_size(ogfx::BasicRenderer2D& gfx)
|
void CheckBox::__update_size(ogfx::BasicRenderer2D& gfx)
|
||||||
{
|
{
|
||||||
auto size = gfx.getStringDimensions(getText(), getFontSize());
|
auto size = gfx.getStringDimensions(getText(), getFontSize());
|
||||||
m_checkSize = { cast<f32>(size.y), cast<f32>(size.y) };
|
m_checkSize = { cast<f32>(size.y), cast<f32>(size.y) };
|
||||||
size.x += m_spacing + m_checkSize.x;
|
size.x += m_spacing + m_checkSize.x;
|
||||||
size.x += getPadding().left();
|
size.x += getPadding().left();
|
||||||
size.x += getPadding().right();
|
size.x += getPadding().right();
|
||||||
size.y += getPadding().top();
|
size.y += getPadding().top();
|
||||||
size.y += getPadding().bottom();
|
size.y += getPadding().bottom();
|
||||||
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
|
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
|
||||||
m_textChanged = false;
|
m_textChanged = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,44 +27,40 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
class CheckBox : public Widget
|
||||||
{
|
{
|
||||||
|
public:
|
||||||
|
inline CheckBox(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
|
||||||
|
inline CheckBox(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
|
||||||
|
CheckBox& create(const String& text);
|
||||||
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
|
void onMouseReleased(const Event& event) override;
|
||||||
|
void setText(const String& text);
|
||||||
|
void setChecked(bool checked);
|
||||||
|
inline String getText(void) const { return m_text; }
|
||||||
|
inline Color getCheckBorderColor(void) const { return m_checkBorderColor; }
|
||||||
|
inline void setCheckBorderColor(const Color& color) { m_checkBorderColor = color; }
|
||||||
|
inline Color getCheckBoxColor(void) const { return m_checkBoxColor; }
|
||||||
|
inline void setCheckBoxColor(const Color& color) { m_checkBoxColor = color; }
|
||||||
|
inline bool isChecked(void) const { return m_checked; }
|
||||||
|
inline void setStateChangedCallback(std::function<void(CheckBox&, bool)> callback) { callback_onStateChanged = std::move(callback); }
|
||||||
|
|
||||||
class CheckBox : public Widget
|
private:
|
||||||
{
|
void __update_size(ogfx::BasicRenderer2D& gfx);
|
||||||
public:
|
|
||||||
inline CheckBox(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
|
|
||||||
inline CheckBox(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
|
|
||||||
CheckBox& create(const String& text);
|
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
|
||||||
void onMouseReleased(const Event& event) override;
|
|
||||||
void setText(const String& text);
|
|
||||||
void setChecked(bool checked);
|
|
||||||
inline String getText(void) const { return m_text; }
|
|
||||||
inline Color getCheckBorderColor(void) const { return m_checkBorderColor; }
|
|
||||||
inline void setCheckBorderColor(const Color& color) { m_checkBorderColor = color; }
|
|
||||||
inline Color getCheckBoxColor(void) const { return m_checkBoxColor; }
|
|
||||||
inline void setCheckBoxColor(const Color& color) { m_checkBoxColor = color; }
|
|
||||||
inline bool isChecked(void) const { return m_checked; }
|
|
||||||
inline void setStateChangedCallback(std::function<void(CheckBox&, bool)> callback) { callback_onStateChanged = std::move(callback); }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void __update_size(ogfx::BasicRenderer2D& gfx);
|
bool m_checked { false };
|
||||||
|
String m_text { "" };
|
||||||
|
bool m_textChanged { false };
|
||||||
|
f32 m_spacing { 10 };
|
||||||
|
Vec2 m_checkSize { 0, 0 };
|
||||||
|
i32 m_checkBorderRadius { 5 };
|
||||||
|
i32 m_checkBorderWidth { 1 };
|
||||||
|
Color m_checkBorderColor { 255, 255, 255 };
|
||||||
|
Color m_checkBoxColor { 255, 255, 255 };
|
||||||
|
|
||||||
private:
|
std::function<void(CheckBox&, bool)> callback_onStateChanged { nullptr };
|
||||||
bool m_checked { false };
|
};
|
||||||
String m_text { "" };
|
|
||||||
bool m_textChanged { false };
|
|
||||||
f32 m_spacing { 10 };
|
|
||||||
Vec2 m_checkSize { 0, 0 };
|
|
||||||
i32 m_checkBorderRadius { 5 };
|
|
||||||
i32 m_checkBorderWidth { 1 };
|
|
||||||
Color m_checkBorderColor { 255, 255, 255 };
|
|
||||||
Color m_checkBoxColor { 255, 255, 255 };
|
|
||||||
|
|
||||||
std::function<void(CheckBox&, bool)> callback_onStateChanged { nullptr };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,344 +26,341 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
Panel& Panel::create(void)
|
||||||
{
|
{
|
||||||
Panel& Panel::create(void)
|
setPadding({ 5, 5, 5, 5 });
|
||||||
{
|
setTypeName("ogfx::gui::Panel");
|
||||||
setPadding({ 5, 5, 5, 5 });
|
disableFocus();
|
||||||
setTypeName("ogfx::gui::widgets::Panel");
|
enableStopEvents();
|
||||||
disableFocus();
|
setStylesheetCategoryName("panel");
|
||||||
enableStopEvents();
|
validate();
|
||||||
setStylesheetCategoryName("panel");
|
return *this;
|
||||||
validate();
|
}
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Panel::applyTheme(const ostd::Stylesheet& theme)
|
void Panel::applyTheme(const ostd::Stylesheet& theme)
|
||||||
{
|
{
|
||||||
setScrollSpeed(getThemeValue<f32>(theme, "scrollSpeed", 0.8f));
|
setScrollSpeed(getThemeValue<f32>(theme, "scrollSpeed", 0.8f));
|
||||||
setScrollSmoothFactor(getThemeValue<f32>(theme, "scrollSmoothFactor", 0.7f));
|
setScrollSmoothFactor(getThemeValue<f32>(theme, "scrollSmoothFactor", 0.7f));
|
||||||
m_titleColor = getThemeValue<Color>(theme, "titleColor", Colors::Black);
|
m_titleColor = getThemeValue<Color>(theme, "titleColor", Colors::Black);
|
||||||
m_titlebarColor = getThemeValue<Color>(theme, "titlebarColor", Colors::Transparent);
|
m_titlebarColor = getThemeValue<Color>(theme, "titlebarColor", Colors::Transparent);
|
||||||
m_titlebarBorderColor = getThemeValue<Color>(theme, "titlebarBorderColor", Colors::Black);
|
m_titlebarBorderColor = getThemeValue<Color>(theme, "titlebarBorderColor", Colors::Black);
|
||||||
m_titlebarHeight = getThemeValue<f32>(theme, "titlebarHeight", 30);
|
m_titlebarHeight = getThemeValue<f32>(theme, "titlebarHeight", 30);
|
||||||
m_titlebarBorderWidth = getThemeValue<i32>(theme, "titlebarBorderWidth", 1);
|
m_titlebarBorderWidth = getThemeValue<i32>(theme, "titlebarBorderWidth", 1);
|
||||||
m_titlebarFontSize = getThemeValue<i32>(theme, "titlebarFontSize", 26);
|
m_titlebarFontSize = getThemeValue<i32>(theme, "titlebarFontSize", 26);
|
||||||
m_titleTextAlign = getThemeValue<i32>(theme, "titlebarTextAlign", cast<i32>(WindowCore::eTextAlign::Left));
|
m_titleTextAlign = getThemeValue<i32>(theme, "titlebarTextAlign", cast<i32>(WindowCore::eTextAlign::Left));
|
||||||
setTitlebarType(getThemeValue<String>(theme, "titlebarType", TitleBarTypes::None));
|
setTitlebarType(getThemeValue<String>(theme, "titlebarType", TitleBarTypes::None));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Panel::onMousePressed(const Event& event)
|
void Panel::onMousePressed(const Event& event)
|
||||||
|
{
|
||||||
|
if (!isDraggable())
|
||||||
|
return;
|
||||||
|
if (m_titleBarBounds.contains({ event.mouse->position_x, event.mouse->position_y }, true))
|
||||||
{
|
{
|
||||||
if (!isDraggable())
|
m_mousePressed = true;
|
||||||
return;
|
m_mousePos = { event.mouse->position_x, event.mouse->position_y };
|
||||||
if (m_titleBarBounds.contains({ event.mouse->position_x, event.mouse->position_y }, true))
|
|
||||||
{
|
|
||||||
m_mousePressed = true;
|
|
||||||
m_mousePos = { event.mouse->position_x, event.mouse->position_y };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Panel::onMouseReleased(const Event& event)
|
void Panel::onMouseReleased(const Event& event)
|
||||||
|
{
|
||||||
|
m_mousePressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel::onMouseDragged(const Event& event)
|
||||||
|
{
|
||||||
|
if (m_mousePressed)
|
||||||
{
|
{
|
||||||
m_mousePressed = false;
|
Vec2 mpos { event.mouse->position_x, event.mouse->position_y };
|
||||||
|
addPos(mpos - m_mousePos);
|
||||||
|
m_mousePos = mpos;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Panel::onMouseDragged(const Event& event)
|
void Panel::afterDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
{
|
{
|
||||||
if (m_mousePressed)
|
draw_titlebar(gfx);
|
||||||
{
|
drawScrollbars(gfx);
|
||||||
Vec2 mpos { event.mouse->position_x, event.mouse->position_y };
|
}
|
||||||
addPos(mpos - m_mousePos);
|
|
||||||
m_mousePos = mpos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Panel::afterDraw(ogfx::BasicRenderer2D& gfx)
|
void Panel::onWindowResized(const Event& event)
|
||||||
{
|
{
|
||||||
draw_titlebar(gfx);
|
updateScrollbarsSize();
|
||||||
drawScrollbars(gfx);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void Panel::onWindowResized(const Event& event)
|
void Panel::setTitlebarType(const String& type)
|
||||||
|
{
|
||||||
|
String t = type.new_toLower().trim();
|
||||||
|
if (t == TitleBarTypes::None)
|
||||||
{
|
{
|
||||||
|
setContentOffset({ 0, 0 });
|
||||||
updateScrollbarsSize();
|
updateScrollbarsSize();
|
||||||
|
m_titlebarType = TitleBarTypes::NoneValue;
|
||||||
}
|
}
|
||||||
|
else if (t == TitleBarTypes::Minimal)
|
||||||
void Panel::setTitlebarType(const String& type)
|
|
||||||
{
|
{
|
||||||
String t = type.new_toLower().trim();
|
setContentOffset({ 0, m_titlebarHeight });
|
||||||
if (t == TitleBarTypes::None)
|
updateScrollbarsSize();
|
||||||
|
m_titlebarType = TitleBarTypes::MinimalValue;
|
||||||
|
}
|
||||||
|
else if (t == TitleBarTypes::Full)
|
||||||
|
{
|
||||||
|
setContentOffset({ 0, m_titlebarHeight });
|
||||||
|
updateScrollbarsSize();
|
||||||
|
m_titlebarType = TitleBarTypes::FullValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String Panel::getTitlebarType(void) const
|
||||||
|
{
|
||||||
|
switch (m_titlebarType)
|
||||||
|
{
|
||||||
|
case TitleBarTypes::FullValue: return TitleBarTypes::Full;
|
||||||
|
case TitleBarTypes::MinimalValue: return TitleBarTypes::Minimal;
|
||||||
|
case TitleBarTypes::NoneValue: return TitleBarTypes::None;
|
||||||
|
default: return TitleBarTypes::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel::draw_titlebar(BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
f32 br = cast<f32>(getBorderWidth());
|
||||||
|
br /= 2;
|
||||||
|
Rectangle titleBarBounds {
|
||||||
|
getGlobalPosition() + Vec2 { br, br },
|
||||||
|
Vec2 { getw(), m_titlebarHeight } - Vec2 { br * 2, br * 2 }
|
||||||
|
};
|
||||||
|
m_titleBarBounds = titleBarBounds;
|
||||||
|
switch (m_titlebarType)
|
||||||
|
{
|
||||||
|
case TitleBarTypes::FullValue:
|
||||||
{
|
{
|
||||||
setContentOffset({ 0, 0 });
|
gfx.outlinedRoundRect(titleBarBounds, m_titlebarColor, m_titlebarColor, { cast<f32>(getBorderRadius()), cast<f32>(getBorderRadius()), 0, 0 }, getBorderWidth());
|
||||||
updateScrollbarsSize();
|
gfx.drawLine({ getGlobalPosition() + Vec2 { 0, m_titlebarHeight - 2 }, getGlobalPosition() + Vec2 { getGlobalBounds().w, m_titlebarHeight - 2 } }, m_titlebarBorderColor, m_titlebarBorderWidth);
|
||||||
m_titlebarType = TitleBarTypes::NoneValue;
|
if (m_titleTextAlign == cast<i32>(WindowCore::eTextAlign::Center))
|
||||||
}
|
gfx.drawCenteredString(m_title, titleBarBounds, m_titleColor, m_titlebarFontSize);
|
||||||
else if (t == TitleBarTypes::Minimal)
|
|
||||||
{
|
|
||||||
setContentOffset({ 0, m_titlebarHeight });
|
|
||||||
updateScrollbarsSize();
|
|
||||||
m_titlebarType = TitleBarTypes::MinimalValue;
|
|
||||||
}
|
|
||||||
else if (t == TitleBarTypes::Full)
|
|
||||||
{
|
|
||||||
setContentOffset({ 0, m_titlebarHeight });
|
|
||||||
updateScrollbarsSize();
|
|
||||||
m_titlebarType = TitleBarTypes::FullValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String Panel::getTitlebarType(void) const
|
|
||||||
{
|
|
||||||
switch (m_titlebarType)
|
|
||||||
{
|
|
||||||
case TitleBarTypes::FullValue: return TitleBarTypes::Full;
|
|
||||||
case TitleBarTypes::MinimalValue: return TitleBarTypes::Minimal;
|
|
||||||
case TitleBarTypes::NoneValue: return TitleBarTypes::None;
|
|
||||||
default: return TitleBarTypes::None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Panel::draw_titlebar(BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
f32 br = cast<f32>(getBorderWidth());
|
|
||||||
br /= 2;
|
|
||||||
Rectangle titleBarBounds {
|
|
||||||
getGlobalPosition() + Vec2 { br, br },
|
|
||||||
Vec2 { getw(), m_titlebarHeight } - Vec2 { br * 2, br * 2 }
|
|
||||||
};
|
|
||||||
m_titleBarBounds = titleBarBounds;
|
|
||||||
switch (m_titlebarType)
|
|
||||||
{
|
|
||||||
case TitleBarTypes::FullValue:
|
|
||||||
{
|
|
||||||
gfx.outlinedRoundRect(titleBarBounds, m_titlebarColor, m_titlebarColor, { cast<f32>(getBorderRadius()), cast<f32>(getBorderRadius()), 0, 0 }, getBorderWidth());
|
|
||||||
gfx.drawLine({ getGlobalPosition() + Vec2 { 0, m_titlebarHeight - 2 }, getGlobalPosition() + Vec2 { getGlobalBounds().w, m_titlebarHeight - 2 } }, m_titlebarBorderColor, m_titlebarBorderWidth);
|
|
||||||
if (m_titleTextAlign == cast<i32>(WindowCore::eTextAlign::Center))
|
|
||||||
gfx.drawCenteredString(m_title, titleBarBounds, m_titleColor, m_titlebarFontSize);
|
|
||||||
else
|
|
||||||
gfx.drawString(m_title, titleBarBounds.getPosition() + Vec2 { 10, 5 }, m_titleColor, m_titlebarFontSize);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case TitleBarTypes::MinimalValue:
|
|
||||||
{
|
|
||||||
if (m_titleTextAlign == cast<i32>(WindowCore::eTextAlign::Center))
|
|
||||||
gfx.drawCenteredString(m_title, titleBarBounds, m_titleColor, m_titlebarFontSize);
|
|
||||||
else
|
|
||||||
gfx.drawString(m_title, titleBarBounds.getPosition() + Vec2 { 10, 5 }, m_titleColor, m_titlebarFontSize);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case TitleBarTypes::NoneValue:
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
TabPanel& TabPanel::create(void)
|
|
||||||
{
|
|
||||||
setPadding({ 0, 0, 0, 0 });
|
|
||||||
setTypeName("ogfx::gui::widgets::TabPanel");
|
|
||||||
disableFocus();
|
|
||||||
enableStopEvents();
|
|
||||||
setStylesheetCategoryName("tabPanel");
|
|
||||||
enableBackground();
|
|
||||||
enableBorder();
|
|
||||||
setTabBarHeight(m_tabBarHeight);
|
|
||||||
validate();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabPanel::applyTheme(const ostd::Stylesheet& theme)
|
|
||||||
{
|
|
||||||
setTabBarHeight(getThemeValue<f32>(theme, "tabBar.height", m_tabBarHeight));
|
|
||||||
setTabBarBackgroundColor(getThemeValue<Color>(theme, "tabBar.backgroundColor", m_tabBarBackgroundColor));
|
|
||||||
setTabBarBorderColor(getThemeValue<Color>(theme, "tabBar.borderColor", m_tabBarBorderColor));
|
|
||||||
setTabBarBorderColor(getThemeValue<i32>(theme, "tabBar.borderWidth", m_tabBarBorderWidth));
|
|
||||||
setTabBarSidePadding(getThemeValue<f32>(theme, "tabBar.sidePadding", m_tabSidePadding));
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabPanel::onMousePressed(const Event& event)
|
|
||||||
{
|
|
||||||
if (!isMouseInsideTabBar({ event.mouse->position_x, event.mouse->position_y }))
|
|
||||||
return;
|
|
||||||
const f32 extra_offset = 5;
|
|
||||||
i32 index = 0;
|
|
||||||
for (auto& b : m_tabBoundsList)
|
|
||||||
{
|
|
||||||
if (b.contains(event.mouse->position_x, event.mouse->position_y))
|
|
||||||
{
|
|
||||||
setCurrentTab(*m_tabs[index]);
|
|
||||||
Rectangle tabBounds = m_tabBoundsList[index];
|
|
||||||
f32 tabLocalLeft = tabBounds.x - getGlobalPosition().x;
|
|
||||||
f32 tabLocalRight = tabLocalLeft + tabBounds.w;
|
|
||||||
|
|
||||||
if (tabLocalLeft < 0)
|
|
||||||
m_tabScrollOffset += tabLocalLeft - extra_offset;
|
|
||||||
else if (tabLocalRight > getw())
|
|
||||||
m_tabScrollOffset += (tabLocalRight - getw()) + extra_offset;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabPanel::onMouseScrolled(const Event& event)
|
|
||||||
{
|
|
||||||
if (!isMouseInsideTabBar({ event.mouse->position_x, event.mouse->position_y }))
|
|
||||||
return;
|
|
||||||
if (m_tabs.size() == 0)
|
|
||||||
return;
|
|
||||||
const f32 extra_offset = 5;
|
|
||||||
m_tabScrollOffset += (event.mouse->scrollAmount.y * -40.0f);
|
|
||||||
m_tabScrollOffset = std::clamp(m_tabScrollOffset, -extra_offset, m_totalTabWidth - getw() + extra_offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabPanel::onDraw(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
gfx.outlinedRect({ getGlobalPosition(), { getw(), getTabBarHeight() } }, getTabBarBackgroundColor(), getTabBarBorderColor(), getTabBarBorderWidth(), false, false, true, false);
|
|
||||||
draw_tabs(gfx);
|
|
||||||
}
|
|
||||||
|
|
||||||
Panel& TabPanel::addTab(const String& title)
|
|
||||||
{
|
|
||||||
m_tabs.push_back(std::make_unique<Panel>(getWindow()));
|
|
||||||
auto& tab = *m_tabs.back();
|
|
||||||
tab.setTitle(title);
|
|
||||||
tab.setTitlebarType(Panel::TitleBarTypes::None);
|
|
||||||
tab.addThemeOverride("@panel_tab.panel.titlebarType", "none");
|
|
||||||
tab.addThemeOverride("@panel_tab.panel.showBorder", false);
|
|
||||||
tab.addThemeOverride("@panel_tab.panel.borderRadius", 0);
|
|
||||||
tab.setBackgroundColor(getBackgroundColor());
|
|
||||||
if (m_currentTab == nullptr && m_tabs.size() == 1)
|
|
||||||
setCurrentTab(tab);
|
|
||||||
tab.addThemeID("panel_tab");
|
|
||||||
tab.reloadTheme(true);
|
|
||||||
return tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TabPanel::removeTab(Panel& tab)
|
|
||||||
{
|
|
||||||
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
|
|
||||||
if (it == m_tabs.end())
|
|
||||||
return false;
|
|
||||||
if (m_currentTab == it->get())
|
|
||||||
prepare_for_current_tab_removal();
|
|
||||||
m_tabs.erase(it);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TabPanel::removeTab(i32 index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= (i32)m_tabs.size())
|
|
||||||
return false;
|
|
||||||
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
|
|
||||||
if (std::distance(m_tabs.begin(), it) == index)
|
|
||||||
prepare_for_current_tab_removal();
|
|
||||||
m_tabs.erase(m_tabs.begin() + index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TabPanel::removeTab(const String& title)
|
|
||||||
{
|
|
||||||
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p->getTitle() == title; });
|
|
||||||
if (it == m_tabs.end())
|
|
||||||
return false;
|
|
||||||
m_tabs.erase(it);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TabPanel::setCurrentTab(Panel& tab)
|
|
||||||
{
|
|
||||||
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
|
|
||||||
if (it == m_tabs.end())
|
|
||||||
return false;
|
|
||||||
i32 index = it - m_tabs.begin();
|
|
||||||
return setCurrentTab(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TabPanel::setCurrentTab(i32 index, bool ignore_same_tab)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= (i32)m_tabs.size())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
auto curr = m_tabs[index].get();
|
|
||||||
if (!ignore_same_tab && curr == m_currentTab && m_currentTab != nullptr)
|
|
||||||
return false;
|
|
||||||
removeWidget(*m_currentTab);
|
|
||||||
m_currentTabIndex = index;
|
|
||||||
m_currentTab = curr;
|
|
||||||
m_currentTab->setMargin({ 0, 0, 0, 0 });
|
|
||||||
m_currentTab->setSize(getPureContentBounds().getSize());
|
|
||||||
m_currentTab->setPosition({ 0, 0 });
|
|
||||||
m_currentTab->reloadTheme(true);
|
|
||||||
m_currentTab->resetScroll();
|
|
||||||
m_currentTab->updateScrollbarsSize();
|
|
||||||
return addWidget(*m_currentTab);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabPanel::setTabBarHeight(f32 height)
|
|
||||||
{
|
|
||||||
m_tabBarHeight = height;
|
|
||||||
m_tabBarBorderRadii = { cast<f32>(getBorderRadius()), cast<f32>(getBorderRadius()), 0, 0 };
|
|
||||||
setContentOffset({ 0, m_tabBarHeight });
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TabPanel::isMouseInsideTabBar(const Vec2& mousePos)
|
|
||||||
{
|
|
||||||
Rectangle bounds { getGlobalPosition(), { getw(), getTabBarHeight() } };
|
|
||||||
return bounds.contains(mousePos, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabPanel::prepare_for_current_tab_removal(void)
|
|
||||||
{
|
|
||||||
if (m_currentTab == nullptr)
|
|
||||||
return;
|
|
||||||
if (m_tabs.size() < 2)
|
|
||||||
{
|
|
||||||
m_currentTab = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
|
|
||||||
|
|
||||||
if (it == m_tabs.end())
|
|
||||||
return; // shouldn't happen but defensive
|
|
||||||
|
|
||||||
if (it != m_tabs.begin())
|
|
||||||
m_currentTab = (it - 1)->get(); // tab to the left
|
|
||||||
else
|
|
||||||
m_currentTab = (it + 1)->get(); // first tab being removed, go right
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabPanel::draw_tabs(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
f32 nextTabX = 1 - m_tabScrollOffset;
|
|
||||||
m_tabBoundsList.clear();
|
|
||||||
i32 index = -1;
|
|
||||||
for (const auto& _tab : m_tabs)
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
if (_tab == nullptr) continue;
|
|
||||||
const auto& tab = *_tab;
|
|
||||||
auto titleBounds = gfx.getStringDimensions(tab.getTitle(), getFontSize());
|
|
||||||
auto glob = getGlobalPosition();
|
|
||||||
Rectangle tabBounds { glob + Vec2 { nextTabX, 2}, { titleBounds.x + (m_tabSidePadding * 2), m_tabBarHeight } };
|
|
||||||
if (m_currentTab == _tab.get())
|
|
||||||
{
|
|
||||||
gfx.outlinedRect(tabBounds, tab.getBackgroundColor(), getTabBarBorderColor(), getTabBarBorderWidth(), true, true, true, true);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
gfx.drawString(m_title, titleBarBounds.getPosition() + Vec2 { 10, 5 }, m_titleColor, m_titlebarFontSize);
|
||||||
bool draw_right_edge = !(m_currentTabIndex == (index + 1));
|
break;
|
||||||
gfx.drawRect(tabBounds - Rectangle { 0, 0, 0, 2 }, getTabBarBorderColor(), getTabBarBorderWidth(), false, draw_right_edge, true, false);
|
|
||||||
}
|
|
||||||
gfx.drawCenteredString(tab.getTitle(), tabBounds, getTextColor(), getFontSize());
|
|
||||||
nextTabX += titleBounds.x + (m_tabSidePadding * 2);
|
|
||||||
m_tabBoundsList.push_back(tabBounds);
|
|
||||||
}
|
}
|
||||||
m_totalTabWidth = nextTabX + m_tabScrollOffset - 1;
|
case TitleBarTypes::MinimalValue:
|
||||||
|
{
|
||||||
|
if (m_titleTextAlign == cast<i32>(WindowCore::eTextAlign::Center))
|
||||||
|
gfx.drawCenteredString(m_title, titleBarBounds, m_titleColor, m_titlebarFontSize);
|
||||||
|
else
|
||||||
|
gfx.drawString(m_title, titleBarBounds.getPosition() + Vec2 { 10, 5 }, m_titleColor, m_titlebarFontSize);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TitleBarTypes::NoneValue:
|
||||||
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
TabPanel& TabPanel::create(void)
|
||||||
|
{
|
||||||
|
setPadding({ 0, 0, 0, 0 });
|
||||||
|
setTypeName("ogfx::gui::TabPanel");
|
||||||
|
disableFocus();
|
||||||
|
enableStopEvents();
|
||||||
|
setStylesheetCategoryName("tabPanel");
|
||||||
|
enableBackground();
|
||||||
|
enableBorder();
|
||||||
|
setTabBarHeight(m_tabBarHeight);
|
||||||
|
validate();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabPanel::applyTheme(const ostd::Stylesheet& theme)
|
||||||
|
{
|
||||||
|
setTabBarHeight(getThemeValue<f32>(theme, "tabBar.height", m_tabBarHeight));
|
||||||
|
setTabBarBackgroundColor(getThemeValue<Color>(theme, "tabBar.backgroundColor", m_tabBarBackgroundColor));
|
||||||
|
setTabBarBorderColor(getThemeValue<Color>(theme, "tabBar.borderColor", m_tabBarBorderColor));
|
||||||
|
setTabBarBorderColor(getThemeValue<i32>(theme, "tabBar.borderWidth", m_tabBarBorderWidth));
|
||||||
|
setTabBarSidePadding(getThemeValue<f32>(theme, "tabBar.sidePadding", m_tabSidePadding));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabPanel::onMousePressed(const Event& event)
|
||||||
|
{
|
||||||
|
if (!isMouseInsideTabBar({ event.mouse->position_x, event.mouse->position_y }))
|
||||||
|
return;
|
||||||
|
const f32 extra_offset = 5;
|
||||||
|
i32 index = 0;
|
||||||
|
for (auto& b : m_tabBoundsList)
|
||||||
|
{
|
||||||
|
if (b.contains(event.mouse->position_x, event.mouse->position_y))
|
||||||
|
{
|
||||||
|
setCurrentTab(*m_tabs[index]);
|
||||||
|
Rectangle tabBounds = m_tabBoundsList[index];
|
||||||
|
f32 tabLocalLeft = tabBounds.x - getGlobalPosition().x;
|
||||||
|
f32 tabLocalRight = tabLocalLeft + tabBounds.w;
|
||||||
|
|
||||||
|
if (tabLocalLeft < 0)
|
||||||
|
m_tabScrollOffset += tabLocalLeft - extra_offset;
|
||||||
|
else if (tabLocalRight > getw())
|
||||||
|
m_tabScrollOffset += (tabLocalRight - getw()) + extra_offset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabPanel::onMouseScrolled(const Event& event)
|
||||||
|
{
|
||||||
|
if (!isMouseInsideTabBar({ event.mouse->position_x, event.mouse->position_y }))
|
||||||
|
return;
|
||||||
|
if (m_tabs.size() == 0)
|
||||||
|
return;
|
||||||
|
const f32 extra_offset = 5;
|
||||||
|
m_tabScrollOffset += (event.mouse->scrollAmount.y * -40.0f);
|
||||||
|
m_tabScrollOffset = std::clamp(m_tabScrollOffset, -extra_offset, m_totalTabWidth - getw() + extra_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabPanel::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
gfx.outlinedRect({ getGlobalPosition(), { getw(), getTabBarHeight() } }, getTabBarBackgroundColor(), getTabBarBorderColor(), getTabBarBorderWidth(), false, false, true, false);
|
||||||
|
draw_tabs(gfx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel& TabPanel::addTab(const String& title)
|
||||||
|
{
|
||||||
|
m_tabs.push_back(std::make_unique<Panel>(getWindow()));
|
||||||
|
auto& tab = *m_tabs.back();
|
||||||
|
tab.setTitle(title);
|
||||||
|
tab.setTitlebarType(Panel::TitleBarTypes::None);
|
||||||
|
tab.addThemeOverride("@panel_tab.panel.titlebarType", "none");
|
||||||
|
tab.addThemeOverride("@panel_tab.panel.showBorder", false);
|
||||||
|
tab.addThemeOverride("@panel_tab.panel.borderRadius", 0);
|
||||||
|
tab.setBackgroundColor(getBackgroundColor());
|
||||||
|
if (m_currentTab == nullptr && m_tabs.size() == 1)
|
||||||
|
setCurrentTab(tab);
|
||||||
|
tab.addThemeID("panel_tab");
|
||||||
|
tab.reloadTheme(true);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabPanel::removeTab(Panel& tab)
|
||||||
|
{
|
||||||
|
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
|
||||||
|
if (it == m_tabs.end())
|
||||||
|
return false;
|
||||||
|
if (m_currentTab == it->get())
|
||||||
|
prepare_for_current_tab_removal();
|
||||||
|
m_tabs.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabPanel::removeTab(i32 index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= (i32)m_tabs.size())
|
||||||
|
return false;
|
||||||
|
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
|
||||||
|
if (std::distance(m_tabs.begin(), it) == index)
|
||||||
|
prepare_for_current_tab_removal();
|
||||||
|
m_tabs.erase(m_tabs.begin() + index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabPanel::removeTab(const String& title)
|
||||||
|
{
|
||||||
|
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p->getTitle() == title; });
|
||||||
|
if (it == m_tabs.end())
|
||||||
|
return false;
|
||||||
|
m_tabs.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabPanel::setCurrentTab(Panel& tab)
|
||||||
|
{
|
||||||
|
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
|
||||||
|
if (it == m_tabs.end())
|
||||||
|
return false;
|
||||||
|
i32 index = it - m_tabs.begin();
|
||||||
|
return setCurrentTab(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabPanel::setCurrentTab(i32 index, bool ignore_same_tab)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= (i32)m_tabs.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto curr = m_tabs[index].get();
|
||||||
|
if (!ignore_same_tab && curr == m_currentTab && m_currentTab != nullptr)
|
||||||
|
return false;
|
||||||
|
removeWidget(*m_currentTab);
|
||||||
|
m_currentTabIndex = index;
|
||||||
|
m_currentTab = curr;
|
||||||
|
m_currentTab->setMargin({ 0, 0, 0, 0 });
|
||||||
|
m_currentTab->setSize(getPureContentBounds().getSize());
|
||||||
|
m_currentTab->setPosition({ 0, 0 });
|
||||||
|
m_currentTab->reloadTheme(true);
|
||||||
|
m_currentTab->resetScroll();
|
||||||
|
m_currentTab->updateScrollbarsSize();
|
||||||
|
return addWidget(*m_currentTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabPanel::setTabBarHeight(f32 height)
|
||||||
|
{
|
||||||
|
m_tabBarHeight = height;
|
||||||
|
m_tabBarBorderRadii = { cast<f32>(getBorderRadius()), cast<f32>(getBorderRadius()), 0, 0 };
|
||||||
|
setContentOffset({ 0, m_tabBarHeight });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabPanel::isMouseInsideTabBar(const Vec2& mousePos)
|
||||||
|
{
|
||||||
|
Rectangle bounds { getGlobalPosition(), { getw(), getTabBarHeight() } };
|
||||||
|
return bounds.contains(mousePos, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabPanel::prepare_for_current_tab_removal(void)
|
||||||
|
{
|
||||||
|
if (m_currentTab == nullptr)
|
||||||
|
return;
|
||||||
|
if (m_tabs.size() < 2)
|
||||||
|
{
|
||||||
|
m_currentTab = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
|
||||||
|
|
||||||
|
if (it == m_tabs.end())
|
||||||
|
return; // shouldn't happen but defensive
|
||||||
|
|
||||||
|
if (it != m_tabs.begin())
|
||||||
|
m_currentTab = (it - 1)->get(); // tab to the left
|
||||||
|
else
|
||||||
|
m_currentTab = (it + 1)->get(); // first tab being removed, go right
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabPanel::draw_tabs(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
f32 nextTabX = 1 - m_tabScrollOffset;
|
||||||
|
m_tabBoundsList.clear();
|
||||||
|
i32 index = -1;
|
||||||
|
for (const auto& _tab : m_tabs)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
if (_tab == nullptr) continue;
|
||||||
|
const auto& tab = *_tab;
|
||||||
|
auto titleBounds = gfx.getStringDimensions(tab.getTitle(), getFontSize());
|
||||||
|
auto glob = getGlobalPosition();
|
||||||
|
Rectangle tabBounds { glob + Vec2 { nextTabX, 2}, { titleBounds.x + (m_tabSidePadding * 2), m_tabBarHeight } };
|
||||||
|
if (m_currentTab == _tab.get())
|
||||||
|
{
|
||||||
|
gfx.outlinedRect(tabBounds, tab.getBackgroundColor(), getTabBarBorderColor(), getTabBarBorderWidth(), true, true, true, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool draw_right_edge = !(m_currentTabIndex == (index + 1));
|
||||||
|
gfx.drawRect(tabBounds - Rectangle { 0, 0, 0, 2 }, getTabBarBorderColor(), getTabBarBorderWidth(), false, draw_right_edge, true, false);
|
||||||
|
}
|
||||||
|
gfx.drawCenteredString(tab.getTitle(), tabBounds, getTextColor(), getFontSize());
|
||||||
|
nextTabX += titleBounds.x + (m_tabSidePadding * 2);
|
||||||
|
m_tabBoundsList.push_back(tabBounds);
|
||||||
|
}
|
||||||
|
m_totalTabWidth = nextTabX + m_tabScrollOffset - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,99 +28,96 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
class Panel : public ScrollableWidget
|
||||||
{
|
{
|
||||||
class Panel : public ScrollableWidget
|
public: struct TitleBarTypes
|
||||||
{
|
{
|
||||||
public: struct TitleBarTypes
|
inline static const String None { "none" };
|
||||||
{
|
inline static const String Full { "full" };
|
||||||
inline static const String None { "none" };
|
inline static const String Minimal { "minimal" };
|
||||||
inline static const String Full { "full" };
|
|
||||||
inline static const String Minimal { "minimal" };
|
|
||||||
|
|
||||||
inline static constexpr i32 NoneValue = 0;
|
inline static constexpr i32 NoneValue = 0;
|
||||||
inline static constexpr i32 FullValue = 1;
|
inline static constexpr i32 FullValue = 1;
|
||||||
inline static constexpr i32 MinimalValue = 2;
|
inline static constexpr i32 MinimalValue = 2;
|
||||||
};
|
|
||||||
public:
|
|
||||||
inline Panel(WindowCore& window) : ScrollableWidget(window) { create(); }
|
|
||||||
Panel& create(void);
|
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
|
||||||
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
|
|
||||||
void onMousePressed(const Event& event) override;
|
|
||||||
void onMouseReleased(const Event& event) override;
|
|
||||||
void onMouseDragged(const Event& event) override;
|
|
||||||
void onWindowResized(const Event& event) override;
|
|
||||||
void setTitlebarType(const String& type);
|
|
||||||
String getTitlebarType(void) const;
|
|
||||||
inline String getTitle(void) const { return m_title; }
|
|
||||||
inline void setTitle(const String& title) { m_title = title; }
|
|
||||||
inline f32 getTitlebarHeight(void) const { return m_titlebarHeight; }
|
|
||||||
OSTD_BOOL_PARAM_GETSET_I(Draggable, m_draggable);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void draw_titlebar(BasicRenderer2D& gfx);
|
|
||||||
|
|
||||||
private:
|
|
||||||
String m_title { "Panel" };
|
|
||||||
Vec2 m_mousePos { 0, 0 };
|
|
||||||
bool m_mousePressed { false };
|
|
||||||
bool m_draggable { false };
|
|
||||||
Rectangle m_titleBarBounds;
|
|
||||||
|
|
||||||
Color m_titleColor { Colors::Black };
|
|
||||||
i32 m_titlebarType = TitleBarTypes::NoneValue;
|
|
||||||
f32 m_titlebarHeight { 30 };
|
|
||||||
i32 m_titlebarBorderWidth { 1 };
|
|
||||||
i32 m_titlebarFontSize { 26 };
|
|
||||||
Color m_titlebarColor { Colors::Transparent };
|
|
||||||
Color m_titlebarBorderColor { Colors::Black };
|
|
||||||
i32 m_titleTextAlign { 0 };
|
|
||||||
};
|
};
|
||||||
class TabPanel : public Widget
|
public:
|
||||||
{
|
inline Panel(WindowCore& window) : ScrollableWidget(window) { create(); }
|
||||||
public:
|
Panel& create(void);
|
||||||
inline TabPanel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
TabPanel& create(void);
|
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
void onMousePressed(const Event& event) override;
|
||||||
void onMousePressed(const Event& event) override;
|
void onMouseReleased(const Event& event) override;
|
||||||
void onMouseScrolled(const Event& event) override;
|
void onMouseDragged(const Event& event) override;
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
void onWindowResized(const Event& event) override;
|
||||||
Panel& addTab(const String& title);
|
void setTitlebarType(const String& type);
|
||||||
bool removeTab(Panel& tab);
|
String getTitlebarType(void) const;
|
||||||
bool removeTab(i32 index);
|
inline String getTitle(void) const { return m_title; }
|
||||||
bool removeTab(const String& title);
|
inline void setTitle(const String& title) { m_title = title; }
|
||||||
bool setCurrentTab(Panel& tab);
|
inline f32 getTitlebarHeight(void) const { return m_titlebarHeight; }
|
||||||
bool setCurrentTab(i32 index, bool ignore_same_tab = false);
|
OSTD_BOOL_PARAM_GETSET_I(Draggable, m_draggable);
|
||||||
void setTabBarHeight(f32 height);
|
|
||||||
bool isMouseInsideTabBar(const Vec2& mousePos);
|
|
||||||
inline void refreshCurrentTab(void) { setCurrentTab(m_currentTabIndex, true); }
|
|
||||||
|
|
||||||
inline f32 getTabBarHeight(void) const { return m_tabBarHeight; }
|
private:
|
||||||
OSTD_PARAM_GETSET(Color, TabBarBackgroundColor, m_tabBarBackgroundColor);
|
void draw_titlebar(BasicRenderer2D& gfx);
|
||||||
OSTD_PARAM_GETSET(Color, TabBarBorderColor, m_tabBarBorderColor);
|
|
||||||
OSTD_PARAM_GETSET(f32, TabBarSidePadding, m_tabSidePadding);
|
|
||||||
OSTD_PARAM_GETSET(i32, TabBarBorderWidth, m_tabBarBorderWidth);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void prepare_for_current_tab_removal(void);
|
String m_title { "Panel" };
|
||||||
void draw_tabs(ogfx::BasicRenderer2D& gfx);
|
Vec2 m_mousePos { 0, 0 };
|
||||||
|
bool m_mousePressed { false };
|
||||||
|
bool m_draggable { false };
|
||||||
|
Rectangle m_titleBarBounds;
|
||||||
|
|
||||||
private:
|
Color m_titleColor { Colors::Black };
|
||||||
stdvec<std::unique_ptr<Panel>> m_tabs;
|
i32 m_titlebarType = TitleBarTypes::NoneValue;
|
||||||
stdvec<Rectangle> m_tabBoundsList;
|
f32 m_titlebarHeight { 30 };
|
||||||
Panel* m_currentTab { nullptr };
|
i32 m_titlebarBorderWidth { 1 };
|
||||||
i32 m_currentTabIndex { -1 };
|
i32 m_titlebarFontSize { 26 };
|
||||||
f32 m_tabScrollOffset { 0.0f };
|
Color m_titlebarColor { Colors::Transparent };
|
||||||
f32 m_totalTabWidth { 0.0f };
|
Color m_titlebarBorderColor { Colors::Black };
|
||||||
Rectangle m_tabBarBorderRadii { 0, 0, 0, 0 };
|
i32 m_titleTextAlign { 0 };
|
||||||
|
};
|
||||||
|
class TabPanel : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
inline TabPanel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
|
||||||
|
TabPanel& create(void);
|
||||||
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
|
void onMousePressed(const Event& event) override;
|
||||||
|
void onMouseScrolled(const Event& event) override;
|
||||||
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
|
Panel& addTab(const String& title);
|
||||||
|
bool removeTab(Panel& tab);
|
||||||
|
bool removeTab(i32 index);
|
||||||
|
bool removeTab(const String& title);
|
||||||
|
bool setCurrentTab(Panel& tab);
|
||||||
|
bool setCurrentTab(i32 index, bool ignore_same_tab = false);
|
||||||
|
void setTabBarHeight(f32 height);
|
||||||
|
bool isMouseInsideTabBar(const Vec2& mousePos);
|
||||||
|
inline void refreshCurrentTab(void) { setCurrentTab(m_currentTabIndex, true); }
|
||||||
|
|
||||||
f32 m_tabBarHeight { 35 };
|
inline f32 getTabBarHeight(void) const { return m_tabBarHeight; }
|
||||||
Color m_tabBarBackgroundColor { 120, 120, 120 };
|
OSTD_PARAM_GETSET(Color, TabBarBackgroundColor, m_tabBarBackgroundColor);
|
||||||
Color m_tabBarBorderColor { 170, 170, 170 };
|
OSTD_PARAM_GETSET(Color, TabBarBorderColor, m_tabBarBorderColor);
|
||||||
f32 m_tabSidePadding { 20 };
|
OSTD_PARAM_GETSET(f32, TabBarSidePadding, m_tabSidePadding);
|
||||||
i32 m_tabBarBorderWidth { 2 };
|
OSTD_PARAM_GETSET(i32, TabBarBorderWidth, m_tabBarBorderWidth);
|
||||||
};
|
|
||||||
}
|
private:
|
||||||
|
void prepare_for_current_tab_removal(void);
|
||||||
|
void draw_tabs(ogfx::BasicRenderer2D& gfx);
|
||||||
|
|
||||||
|
private:
|
||||||
|
stdvec<std::unique_ptr<Panel>> m_tabs;
|
||||||
|
stdvec<Rectangle> m_tabBoundsList;
|
||||||
|
Panel* m_currentTab { nullptr };
|
||||||
|
i32 m_currentTabIndex { -1 };
|
||||||
|
f32 m_tabScrollOffset { 0.0f };
|
||||||
|
f32 m_totalTabWidth { 0.0f };
|
||||||
|
Rectangle m_tabBarBorderRadii { 0, 0, 0, 0 };
|
||||||
|
|
||||||
|
f32 m_tabBarHeight { 35 };
|
||||||
|
Color m_tabBarBackgroundColor { 120, 120, 120 };
|
||||||
|
Color m_tabBarBorderColor { 170, 170, 170 };
|
||||||
|
f32 m_tabSidePadding { 20 };
|
||||||
|
i32 m_tabBarBorderWidth { 2 };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,105 +22,227 @@
|
||||||
#include "../../render/BasicRenderer.hpp"
|
#include "../../render/BasicRenderer.hpp"
|
||||||
#include "../../../ostd/io/FileSystem.hpp"
|
#include "../../../ostd/io/FileSystem.hpp"
|
||||||
#include "../Window.hpp"
|
#include "../Window.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace ogfx
|
namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
// ===================================================================
|
||||||
|
// Label
|
||||||
|
// ===================================================================
|
||||||
|
Label& Label::create(const String& text)
|
||||||
{
|
{
|
||||||
Label& Label::create(const String& text)
|
m_text = text;
|
||||||
|
setPadding({ 5, 5, 5, 5 });
|
||||||
|
setTypeName("ogfx::gui::Label");
|
||||||
|
disableChildren();
|
||||||
|
enableBackground(false);
|
||||||
|
setStylesheetCategoryName("label");
|
||||||
|
validate();
|
||||||
|
invalidate_layout();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Label::setSizeMode(eSizeMode mode)
|
||||||
|
{
|
||||||
|
m_sizeMode = mode;
|
||||||
|
m_modeExplicitlySet = true;
|
||||||
|
invalidate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
eSizeMode Label::effectiveSizeMode(void) const
|
||||||
|
{
|
||||||
|
if (!m_modeExplicitlySet && getParent() && getParent()->hasLayout())
|
||||||
|
return eSizeMode::LayoutManaged;
|
||||||
|
return m_sizeMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Label::onBoundsChanged(const Vec2& /*newSize*/)
|
||||||
|
{
|
||||||
|
invalidate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Label::setText(const String& text)
|
||||||
|
{
|
||||||
|
m_text = text;
|
||||||
|
invalidate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Label::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
if (m_layoutDirty)
|
||||||
|
recompute_layout();
|
||||||
|
gfx.drawString(
|
||||||
|
m_text,
|
||||||
|
getGlobalContentPosition() + m_drawTextOffset,
|
||||||
|
getTextColor(),
|
||||||
|
getFontSize()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Label::recompute_layout(void)
|
||||||
|
{
|
||||||
|
m_layoutDirty = false;
|
||||||
|
|
||||||
|
auto& gfx = getWindow().getGFX();
|
||||||
|
const Vec2 textSize = gfx.getStringDimensions(m_text, getFontSize());
|
||||||
|
const Rectangle pad = getPadding();
|
||||||
|
const eSizeMode mode = effectiveSizeMode();
|
||||||
|
|
||||||
|
if (mode == eSizeMode::AutoSize)
|
||||||
{
|
{
|
||||||
setText(text);
|
const f32 wantW = textSize.x + pad.left() + pad.right();
|
||||||
setPadding({ 5, 5, 5, 5 });
|
const f32 wantH = textSize.y + pad.top() + pad.bottom();
|
||||||
setTypeName("ogfx::gui::widgets::Label");
|
|
||||||
disableChildren();
|
// Avoid feedback loop on hover/focus theme reloads — only resize
|
||||||
enableBackground(false);
|
// if the desired size actually changed by a meaningful amount.
|
||||||
setStylesheetCategoryName("label");
|
if (std::abs(getw() - wantW) > 0.5f || std::abs(geth() - wantH) > 0.5f)
|
||||||
validate();
|
setSize({ wantW, wantH });
|
||||||
return *this;
|
|
||||||
|
// Auto-size means the content fills the content box exactly,
|
||||||
|
// so the text offset is (0, 0).
|
||||||
|
m_drawTextOffset = { 0, 0 };
|
||||||
}
|
}
|
||||||
|
else
|
||||||
void Label::onDraw(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
{
|
||||||
if (m_textChanged)
|
// Manual / LayoutManaged: center text inside the (user/layout-given)
|
||||||
__update_size(gfx);
|
// content area. Overflow is clipped at the widget edge.
|
||||||
gfx.drawString(getText(), getGlobalContentPosition(), getTextColor(), getFontSize());
|
const Vec2 contentSize = getContentBounds().getSize();
|
||||||
}
|
m_drawTextOffset = {
|
||||||
|
(contentSize.x - textSize.x) * 0.5f,
|
||||||
void Label::setText(const String& text)
|
(contentSize.y - textSize.y) * 0.5f
|
||||||
{
|
};
|
||||||
m_text = text;
|
|
||||||
m_textChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Label::__update_size(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
auto size = gfx.getStringDimensions(getText(), getFontSize());
|
|
||||||
size.x += getPadding().left();
|
|
||||||
size.x += getPadding().right();
|
|
||||||
size.y += getPadding().top();
|
|
||||||
size.y += getPadding().bottom();
|
|
||||||
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
|
|
||||||
m_textChanged = false;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// ImageLabel
|
||||||
|
// ===================================================================
|
||||||
|
ImageLabel& ImageLabel::create(const String& filePath)
|
||||||
|
{
|
||||||
|
setImage(filePath);
|
||||||
|
setPadding({ 0, 0, 0, 0 });
|
||||||
|
setTypeName("ogfx::gui::ImageLabel");
|
||||||
|
disableChildren();
|
||||||
|
disableBackground();
|
||||||
|
disableBorder();
|
||||||
|
setStylesheetCategoryName("image");
|
||||||
|
setSize(64, 64);
|
||||||
|
validate();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageLabel::applyTheme(const ostd::Stylesheet& theme)
|
||||||
ImageLabel& ImageLabel::create(const String& filePath)
|
{
|
||||||
{
|
enableAnimated(getThemeValue<bool>(theme, "animated", m_animated));
|
||||||
|
setTintColor(getThemeValue<Color>(theme, "tint", getTintColor()));
|
||||||
|
setAnimationData(getThemeValue<AnimationData>(theme, "animation", m_animData));
|
||||||
|
String filePath = getThemeValue<String>(theme, "path", m_image.getFilePath());
|
||||||
|
if (filePath != m_image.getFilePath())
|
||||||
setImage(filePath);
|
setImage(filePath);
|
||||||
setPadding({ 0, 0, 0, 0 });
|
// NOTE: the old code set widget size from theme here, which is wrong
|
||||||
setTypeName("ogfx::gui::widgets::ImageLabel");
|
// once layouts come into play. The theme's "size" affects m_imageSize
|
||||||
disableChildren();
|
// (which is meaningful in Native mode). Widget size stays under
|
||||||
disableBackground();
|
// user/layout control.
|
||||||
disableBorder();
|
setImageSize(getThemeValue<Vec2>(theme, "size", getImageSize()));
|
||||||
setStylesheetCategoryName("image");
|
if (isAnimatedEnabled())
|
||||||
setSize(64, 64);
|
|
||||||
validate();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageLabel::applyTheme(const ostd::Stylesheet& theme)
|
|
||||||
{
|
{
|
||||||
enableAnimated(getThemeValue<bool>(theme, "animated", m_animated));
|
m_anim.create(m_animData);
|
||||||
setTintColor(getThemeValue<Color>(theme, "tint", getTintColor()));
|
m_anim.setSpriteSheet(m_image);
|
||||||
setAnimationData(getThemeValue<AnimationData>(theme, "animation", m_animData));
|
}
|
||||||
String filePath = getThemeValue<String>(theme, "path", m_image.getFilePath());
|
}
|
||||||
if (filePath != m_image.getFilePath())
|
|
||||||
setImage(filePath);
|
void ImageLabel::setImage(const String& filePath)
|
||||||
setSize(getThemeValue<Vec2>(theme, "size", getSize()));
|
{
|
||||||
if (isAnimatedEnabled())
|
if (filePath == "") return;
|
||||||
|
if (!ostd::FileSystem::fileExists(filePath))
|
||||||
|
return;
|
||||||
|
m_image.destroy();
|
||||||
|
m_image.loadFromFile(filePath, getWindow().getGFX());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageLabel::onUpdate(void)
|
||||||
|
{
|
||||||
|
if (isAnimatedEnabled())
|
||||||
|
m_anim.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageLabel::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
Vec2 drawPos, drawSize;
|
||||||
|
compute_draw_geometry(drawPos, drawSize);
|
||||||
|
|
||||||
|
if (drawSize.x <= 0 || drawSize.y <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (isAnimatedEnabled())
|
||||||
|
gfx.drawAnimation(m_anim, drawPos, drawSize, getTintColor());
|
||||||
|
else
|
||||||
|
gfx.drawImage(m_image, drawPos, drawSize, getTintColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageLabel::compute_draw_geometry(Vec2& outPos, Vec2& outSize) const
|
||||||
|
{
|
||||||
|
const Vec2 contentTL = getGlobalContentPosition();
|
||||||
|
const Vec2 contentSize = getContentBounds().getSize();
|
||||||
|
|
||||||
|
// Natural size: when the image is a sprite sheet driven by an
|
||||||
|
// Animation, one frame is the natural unit, not the whole sheet.
|
||||||
|
// (Otherwise letterboxing would respect the sheet's aspect ratio,
|
||||||
|
// which is meaningless — it depends on column/row count.)
|
||||||
|
Vec2 nat { 0, 0 };
|
||||||
|
if (m_image.isLoaded())
|
||||||
|
{
|
||||||
|
if (m_animated)
|
||||||
{
|
{
|
||||||
m_anim.create(m_animData);
|
const auto frame = m_anim.getFrameRect();
|
||||||
m_anim.setSpriteSheet(m_image);
|
nat = { frame.w, frame.h };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nat = m_image.getSize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImageLabel::onDraw(ogfx::BasicRenderer2D& gfx)
|
if (m_fit == eImageFit::Fit)
|
||||||
{
|
{
|
||||||
if (isAnimatedEnabled())
|
if (!m_keepAspect)
|
||||||
gfx.drawAnimation(m_anim, getGlobalPosition() + getPadding().topLeft(), getSize() - getPadding().bottomRight(), getTintColor());
|
{
|
||||||
else
|
// Stretch to fill, no aspect preservation.
|
||||||
gfx.drawImage(m_image, getGlobalPosition() + getPadding().topLeft(), getSize() - getPadding().bottomRight(), getTintColor());
|
outPos = contentTL;
|
||||||
}
|
outSize = contentSize;
|
||||||
|
|
||||||
void ImageLabel::onUpdate(void)
|
|
||||||
{
|
|
||||||
if (isAnimatedEnabled())
|
|
||||||
m_anim.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageLabel::setImage(const String& filePath)
|
|
||||||
{
|
|
||||||
if (filePath == "") return;
|
|
||||||
if (!ostd::FileSystem::fileExists(filePath))
|
|
||||||
return;
|
return;
|
||||||
m_image.destroy();
|
}
|
||||||
m_image.loadFromFile(filePath, getWindow().getGFX());
|
|
||||||
|
// Letterbox: scale uniformly to fit, then center.
|
||||||
|
if (nat.x <= 0 || nat.y <= 0)
|
||||||
|
{
|
||||||
|
// No natural size known — fall back to filling.
|
||||||
|
outPos = contentTL;
|
||||||
|
outSize = contentSize;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const f32 sx = contentSize.x / nat.x;
|
||||||
|
const f32 sy = contentSize.y / nat.y;
|
||||||
|
const f32 s = std::min(sx, sy);
|
||||||
|
outSize = { nat.x * s, nat.y * s };
|
||||||
|
outPos = contentTL + Vec2 {
|
||||||
|
(contentSize.x - outSize.x) * 0.5f,
|
||||||
|
(contentSize.y - outSize.y) * 0.5f
|
||||||
|
};
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Native: draw at m_imageSize, centered. Overflow is clipped by
|
||||||
|
// the widget's automatic clipping at the renderer level.
|
||||||
|
outSize = m_imageSize;
|
||||||
|
outPos = contentTL + Vec2 {
|
||||||
|
(contentSize.x - outSize.x) * 0.5f,
|
||||||
|
(contentSize.y - outSize.y) * 0.5f
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <ogfx/gui/widgets/Widget.hpp>
|
#include <ogfx/gui/widgets/Widget.hpp>
|
||||||
|
#include <ogfx/gui/widgets/Button.hpp> // for eSizeMode
|
||||||
#include <ogfx/resources/Image.hpp>
|
#include <ogfx/resources/Image.hpp>
|
||||||
#include <ogfx/utils/Animation.hpp>
|
#include <ogfx/utils/Animation.hpp>
|
||||||
|
|
||||||
|
|
@ -28,47 +29,115 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
// ===========================================================================
|
||||||
|
// Label — text-only widget.
|
||||||
|
//
|
||||||
|
// Modes (using the shared eSizeMode):
|
||||||
|
// AutoSize: widget shrinks/grows to fit text + padding.
|
||||||
|
// Manual: widget keeps user-set size; text drawn inside, clipped
|
||||||
|
// if it doesn't fit.
|
||||||
|
// LayoutManaged: parent layout decides size; otherwise behaves like Manual.
|
||||||
|
// ===========================================================================
|
||||||
|
class Label : public Widget
|
||||||
{
|
{
|
||||||
class Label : public Widget
|
public:
|
||||||
{
|
inline Label(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
|
||||||
public:
|
inline Label(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
|
||||||
inline Label(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
|
Label& create(const String& text);
|
||||||
inline Label(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
|
|
||||||
Label& create(const String& text);
|
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
|
||||||
void setText(const String& text);
|
|
||||||
inline String getText(void) const { return m_text; }
|
|
||||||
|
|
||||||
private:
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
void __update_size(ogfx::BasicRenderer2D& gfx);
|
void onBoundsChanged(const Vec2& newSize) override;
|
||||||
|
|
||||||
private:
|
void setText(const String& text);
|
||||||
String m_text { "" };
|
inline String getText(void) const { return m_text; }
|
||||||
bool m_textChanged { false };
|
|
||||||
};
|
|
||||||
class ImageLabel : public Widget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
inline ImageLabel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
|
|
||||||
inline ImageLabel(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); }
|
|
||||||
ImageLabel& create(const String& filePath);
|
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
|
||||||
void onUpdate(void) override;
|
|
||||||
void setImage(const String& filePath);
|
|
||||||
inline Image& getImage(void) { return m_image; }
|
|
||||||
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
|
|
||||||
OSTD_PARAM_GETSET(AnimationData, AnimationData, m_animData);
|
|
||||||
OSTD_PARAM_GETSET(Color, TintColor, m_tintColor);
|
|
||||||
|
|
||||||
private:
|
// Same semantics as Button::setSizeMode / effectiveSizeMode.
|
||||||
Image m_image;
|
void setSizeMode(eSizeMode mode);
|
||||||
Animation m_anim;
|
inline eSizeMode getSizeMode(void) const { return m_sizeMode; }
|
||||||
AnimationData m_animData;
|
eSizeMode effectiveSizeMode(void) const;
|
||||||
Color m_tintColor { Colors::White };
|
|
||||||
bool m_animated { false };
|
private:
|
||||||
};
|
inline void invalidate_layout(void) { m_layoutDirty = true; }
|
||||||
}
|
void recompute_layout(void);
|
||||||
|
|
||||||
|
private:
|
||||||
|
String m_text { "" };
|
||||||
|
eSizeMode m_sizeMode { eSizeMode::AutoSize };
|
||||||
|
bool m_modeExplicitlySet { false };
|
||||||
|
bool m_layoutDirty { true };
|
||||||
|
Vec2 m_drawTextOffset { 0, 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// ImageLabel — image-only widget.
|
||||||
|
//
|
||||||
|
// Two image modes (separate from widget size — the widget's size always
|
||||||
|
// comes from the user or from the parent layout):
|
||||||
|
//
|
||||||
|
// eImageFit::Fit: image scales to fill the widget's content area.
|
||||||
|
// Optional keepAspectRatio: if true, image is
|
||||||
|
// scaled uniformly to fit and centered (letterbox);
|
||||||
|
// if false, image stretches non-uniformly to fill.
|
||||||
|
//
|
||||||
|
// eImageFit::Native: image drawn at user-specified m_imageSize,
|
||||||
|
// centered inside the widget bounds. Crops naturally
|
||||||
|
// if m_imageSize > widget content size — the widget
|
||||||
|
// clips at its own bounds anyway.
|
||||||
|
// ===========================================================================
|
||||||
|
enum class eImageFit : u8
|
||||||
|
{
|
||||||
|
Fit,
|
||||||
|
Native
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImageLabel : public Widget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
inline ImageLabel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); }
|
||||||
|
inline ImageLabel(WindowCore& window, const String& filePath) : Widget({ 0, 0, 0, 0 }, window) { create(filePath); }
|
||||||
|
ImageLabel& create(const String& filePath);
|
||||||
|
|
||||||
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
|
void onUpdate(void) override;
|
||||||
|
|
||||||
|
void setImage(const String& filePath);
|
||||||
|
inline Image& getImage(void) { return m_image; }
|
||||||
|
|
||||||
|
// Mode selection. Default is Fit + keepAspectRatio=true.
|
||||||
|
inline void setImageFit(eImageFit fit) { m_fit = fit; }
|
||||||
|
inline eImageFit getImageFit(void) const { return m_fit; }
|
||||||
|
|
||||||
|
// Only meaningful when fit == Fit. If true, image keeps its
|
||||||
|
// aspect ratio and is centered with letterbox bars; if false,
|
||||||
|
// image stretches non-uniformly to fill.
|
||||||
|
OSTD_BOOL_PARAM_GETSET_E(KeepAspectRatio, m_keepAspect);
|
||||||
|
|
||||||
|
// Only meaningful when fit == Native. The size to draw the image at,
|
||||||
|
// regardless of widget size. The image is drawn centered.
|
||||||
|
OSTD_PARAM_GETSET(Vec2, ImageSize, m_imageSize);
|
||||||
|
|
||||||
|
OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated);
|
||||||
|
OSTD_PARAM_GETSET(AnimationData, AnimationData, m_animData);
|
||||||
|
OSTD_PARAM_GETSET(Color, TintColor, m_tintColor);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Compute (drawPosition, drawSize) for the current frame.
|
||||||
|
// drawPosition is in global coords. drawSize is the on-screen size.
|
||||||
|
void compute_draw_geometry(Vec2& outPos, Vec2& outSize) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Image m_image;
|
||||||
|
Animation m_anim;
|
||||||
|
AnimationData m_animData;
|
||||||
|
Color m_tintColor { Colors::White };
|
||||||
|
|
||||||
|
eImageFit m_fit { eImageFit::Fit };
|
||||||
|
bool m_keepAspect { true };
|
||||||
|
Vec2 m_imageSize { 64, 64 }; // user-specified size for Native mode
|
||||||
|
|
||||||
|
bool m_animated { false };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,249 +27,246 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
void ListView::Item::setText(const String& text)
|
||||||
{
|
{
|
||||||
void ListView::Item::setText(const String& text)
|
if (m_text == text)
|
||||||
|
return;
|
||||||
|
m_text = text;
|
||||||
|
update_dimensions();
|
||||||
|
if (m_parent) m_parent->m_extentsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListView::Item::setFontSize(i32 fontSize)
|
||||||
|
{
|
||||||
|
if (m_fontSize == fontSize)
|
||||||
|
return;
|
||||||
|
m_fontSize = fontSize;
|
||||||
|
update_dimensions();
|
||||||
|
if (m_parent) m_parent->m_extentsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListView::Item::update_dimensions(void)
|
||||||
|
{
|
||||||
|
if (!isValid())
|
||||||
|
return;
|
||||||
|
m_dimensions = m_parent->getWindow().getGFX().getStringDimensions(m_text, m_fontSize);
|
||||||
|
m_dimensions += Vec2 { m_padding.x + m_padding.w, m_padding.y + m_padding.h };
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListView::Item::set_selected(stdvec<Item*>& selectionList)
|
||||||
|
{
|
||||||
|
for (auto& sel : selectionList)
|
||||||
|
sel->m_selected = false;
|
||||||
|
selectionList.clear();
|
||||||
|
selectionList.push_back(this);
|
||||||
|
m_selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListView::Item::add_selected(stdvec<Item*>& selectionList)
|
||||||
|
{
|
||||||
|
if (STDVEC_CONTAINS(selectionList, this))
|
||||||
|
return;
|
||||||
|
selectionList.push_back(this);
|
||||||
|
m_selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListView::Item::remove_selected(stdvec<Item*>& selectionList)
|
||||||
|
{
|
||||||
|
if (!STDVEC_CONTAINS(selectionList, this))
|
||||||
|
return;
|
||||||
|
STDVEC_REMOVE(selectionList, this);
|
||||||
|
m_selected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ListView& ListView::create(void)
|
||||||
|
{
|
||||||
|
setPadding({ 0, 0, 0, 0 });
|
||||||
|
setTypeName("ogfx::gui::ListView");
|
||||||
|
enableStopEvents();
|
||||||
|
enableBackground();
|
||||||
|
enableBorder();
|
||||||
|
setBackgroundColor({ 160, 160, 160 });
|
||||||
|
setBorderColor({ 50, 50, 50 });
|
||||||
|
setStylesheetCategoryName("list");
|
||||||
|
validate();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListView::applyTheme(const ostd::Stylesheet& theme)
|
||||||
|
{
|
||||||
|
setSeparatorLineColor(getThemeValue<Color>(theme, "separatorLineColor", getSeparatorLineColor()));
|
||||||
|
enableShowSeparatorLine(getThemeValue<bool>(theme, "showSeparatorLine", isShowSeparatorLineEnabled()));
|
||||||
|
setDefaultLineTextColor(getThemeValue<Color>(theme, "item.defaultTextColor", getDefaultLineTextColor()));
|
||||||
|
setDefaultLineFontSize(getThemeValue<i32>(theme, "item.defaultFontSize", getDefaultLineFontSize()));
|
||||||
|
setDefaultLinePadding(getThemeValue<Rectangle>(theme, "item.defaultPadding", getDefaultLinePadding()));
|
||||||
|
setDefaultLineSelectionColor(getThemeValue<Color>(theme, "item.defaultSelectionColor", getDefaultLineSelectionColor()));
|
||||||
|
setDefaultLineSelectionTextColor(getThemeValue<Color>(theme, "item.defaultSelectionTextColor", getDefaultLineSelectionTextColor()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListView::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
const auto& bounds = getGlobalBounds();
|
||||||
|
const auto& content = getContentExtents();
|
||||||
|
const f32 scrollY = -getScrollOffset().y;
|
||||||
|
const f32 visibleH = getContentBounds().h;
|
||||||
|
const f32 visibleStart = scrollY;
|
||||||
|
const f32 visibleEnd = scrollY + visibleH;
|
||||||
|
Color textColor;
|
||||||
|
|
||||||
|
f32 y = 0;
|
||||||
|
i32 startIdx = 0;
|
||||||
|
for (auto& item : m_list)
|
||||||
{
|
{
|
||||||
if (m_text == text)
|
if (!item.isValid()) { startIdx++; continue; }
|
||||||
return;
|
f32 itemH = item.getDimensions().y;
|
||||||
m_text = text;
|
if (y + itemH > visibleStart) break;
|
||||||
update_dimensions();
|
y += itemH;
|
||||||
if (m_parent) m_parent->m_extentsDirty = true;
|
startIdx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListView::Item::setFontSize(i32 fontSize)
|
for (i32 i = startIdx; i < (i32)m_list.size(); i++)
|
||||||
{
|
{
|
||||||
if (m_fontSize == fontSize)
|
auto& item = m_list[i];
|
||||||
return;
|
if (!item.isValid()) continue;
|
||||||
m_fontSize = fontSize;
|
f32 itemH = item.getDimensions().y;
|
||||||
update_dimensions();
|
if (y > visibleEnd) break;
|
||||||
if (m_parent) m_parent->m_extentsDirty = true;
|
f32 lineW = content.w;
|
||||||
}
|
if (content.w < bounds.w)
|
||||||
|
lineW = bounds.w;
|
||||||
void ListView::Item::update_dimensions(void)
|
if (item.isSelected())
|
||||||
{
|
|
||||||
if (!isValid())
|
|
||||||
return;
|
|
||||||
m_dimensions = m_parent->getWindow().getGFX().getStringDimensions(m_text, m_fontSize);
|
|
||||||
m_dimensions += Vec2 { m_padding.x + m_padding.w, m_padding.y + m_padding.h };
|
|
||||||
}
|
|
||||||
|
|
||||||
void ListView::Item::set_selected(stdvec<Item*>& selectionList)
|
|
||||||
{
|
|
||||||
for (auto& sel : selectionList)
|
|
||||||
sel->m_selected = false;
|
|
||||||
selectionList.clear();
|
|
||||||
selectionList.push_back(this);
|
|
||||||
m_selected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ListView::Item::add_selected(stdvec<Item*>& selectionList)
|
|
||||||
{
|
|
||||||
if (STDVEC_CONTAINS(selectionList, this))
|
|
||||||
return;
|
|
||||||
selectionList.push_back(this);
|
|
||||||
m_selected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ListView::Item::remove_selected(stdvec<Item*>& selectionList)
|
|
||||||
{
|
|
||||||
if (!STDVEC_CONTAINS(selectionList, this))
|
|
||||||
return;
|
|
||||||
STDVEC_REMOVE(selectionList, this);
|
|
||||||
m_selected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ListView& ListView::create(void)
|
|
||||||
{
|
|
||||||
setPadding({ 0, 0, 0, 0 });
|
|
||||||
setTypeName("ogfx::gui::widgets::ListView");
|
|
||||||
enableStopEvents();
|
|
||||||
enableBackground();
|
|
||||||
enableBorder();
|
|
||||||
setBackgroundColor({ 160, 160, 160 });
|
|
||||||
setBorderColor({ 50, 50, 50 });
|
|
||||||
setStylesheetCategoryName("list");
|
|
||||||
validate();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ListView::applyTheme(const ostd::Stylesheet& theme)
|
|
||||||
{
|
|
||||||
setSeparatorLineColor(getThemeValue<Color>(theme, "separatorLineColor", getSeparatorLineColor()));
|
|
||||||
enableShowSeparatorLine(getThemeValue<bool>(theme, "showSeparatorLine", isShowSeparatorLineEnabled()));
|
|
||||||
setDefaultLineTextColor(getThemeValue<Color>(theme, "item.defaultTextColor", getDefaultLineTextColor()));
|
|
||||||
setDefaultLineFontSize(getThemeValue<i32>(theme, "item.defaultFontSize", getDefaultLineFontSize()));
|
|
||||||
setDefaultLinePadding(getThemeValue<Rectangle>(theme, "item.defaultPadding", getDefaultLinePadding()));
|
|
||||||
setDefaultLineSelectionColor(getThemeValue<Color>(theme, "item.defaultSelectionColor", getDefaultLineSelectionColor()));
|
|
||||||
setDefaultLineSelectionTextColor(getThemeValue<Color>(theme, "item.defaultSelectionTextColor", getDefaultLineSelectionTextColor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ListView::onDraw(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
const auto& bounds = getGlobalBounds();
|
|
||||||
const auto& content = getContentExtents();
|
|
||||||
const f32 scrollY = -getScrollOffset().y;
|
|
||||||
const f32 visibleH = getContentBounds().h;
|
|
||||||
const f32 visibleStart = scrollY;
|
|
||||||
const f32 visibleEnd = scrollY + visibleH;
|
|
||||||
Color textColor;
|
|
||||||
|
|
||||||
f32 y = 0;
|
|
||||||
i32 startIdx = 0;
|
|
||||||
for (auto& item : m_list)
|
|
||||||
{
|
{
|
||||||
if (!item.isValid()) { startIdx++; continue; }
|
textColor = item.getSelectedTextColor();
|
||||||
f32 itemH = item.getDimensions().y;
|
gfx.fillRect({ Vec2 { bounds.x, bounds.y + y + item.getPadding().h } + getScrollOffset(), { lineW, itemH } }, item.getSelectedColor());
|
||||||
if (y + itemH > visibleStart) break;
|
|
||||||
y += itemH;
|
|
||||||
startIdx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i32 i = startIdx; i < (i32)m_list.size(); i++)
|
|
||||||
{
|
|
||||||
auto& item = m_list[i];
|
|
||||||
if (!item.isValid()) continue;
|
|
||||||
f32 itemH = item.getDimensions().y;
|
|
||||||
if (y > visibleEnd) break;
|
|
||||||
f32 lineW = content.w;
|
|
||||||
if (content.w < bounds.w)
|
|
||||||
lineW = bounds.w;
|
|
||||||
if (item.isSelected())
|
|
||||||
{
|
|
||||||
textColor = item.getSelectedTextColor();
|
|
||||||
gfx.fillRect({ Vec2 { bounds.x, bounds.y + y + item.getPadding().h } + getScrollOffset(), { lineW, itemH } }, item.getSelectedColor());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
textColor = item.getTextColor();
|
|
||||||
gfx.drawLine({ Vec2 { bounds.x, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset(), Vec2 { bounds.x + lineW, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset() }, getSeparatorLineColor(), 1);
|
|
||||||
gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), textColor, item.getFontSize());
|
|
||||||
y += itemH;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
textColor = item.getTextColor();
|
||||||
|
gfx.drawLine({ Vec2 { bounds.x, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset(), Vec2 { bounds.x + lineW, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset() }, getSeparatorLineColor(), 1);
|
||||||
|
gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), textColor, item.getFontSize());
|
||||||
|
y += itemH;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ListView::afterDraw(ogfx::BasicRenderer2D& gfx)
|
void ListView::afterDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
drawScrollbars(gfx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListView::onMouseReleased(const Event& event)
|
||||||
|
{
|
||||||
|
if (!isMouseInside())
|
||||||
|
return;
|
||||||
|
if (event.mouse->button != ogfx::MouseEventData::eButton::Left)
|
||||||
|
return;
|
||||||
|
if (isMouseInsideAnyScrollbar())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const Vec2 mousePos { event.mouse->position_x, event.mouse->position_y };
|
||||||
|
const Vec2 origin = getGlobalBounds().getPosition();
|
||||||
|
const Vec2 scroll = getScrollOffset();
|
||||||
|
const f32 localY = mousePos.y - origin.y - scroll.y;
|
||||||
|
|
||||||
|
f32 y = 0;
|
||||||
|
for (auto& item : m_list)
|
||||||
{
|
{
|
||||||
drawScrollbars(gfx);
|
if (!item.isValid()) continue;
|
||||||
}
|
const f32 itemH = item.getDimensions().y;
|
||||||
|
if (localY >= y && localY < y + itemH)
|
||||||
void ListView::onMouseReleased(const Event& event)
|
|
||||||
{
|
|
||||||
if (!isMouseInside())
|
|
||||||
return;
|
|
||||||
if (event.mouse->button != ogfx::MouseEventData::eButton::Left)
|
|
||||||
return;
|
|
||||||
if (isMouseInsideAnyScrollbar())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const Vec2 mousePos { event.mouse->position_x, event.mouse->position_y };
|
|
||||||
const Vec2 origin = getGlobalBounds().getPosition();
|
|
||||||
const Vec2 scroll = getScrollOffset();
|
|
||||||
const f32 localY = mousePos.y - origin.y - scroll.y;
|
|
||||||
|
|
||||||
f32 y = 0;
|
|
||||||
for (auto& item : m_list)
|
|
||||||
{
|
{
|
||||||
if (!item.isValid()) continue;
|
bool wasSelected = item.isSelected();
|
||||||
const f32 itemH = item.getDimensions().y;
|
item.set_selected(m_selectedList);
|
||||||
if (localY >= y && localY < y + itemH)
|
if (!wasSelected && callback_onSelectionChanged)
|
||||||
{
|
callback_onSelectionChanged(m_selectedList);
|
||||||
bool wasSelected = item.isSelected();
|
event.handle();
|
||||||
item.set_selected(m_selectedList);
|
break;
|
||||||
if (!wasSelected && callback_onSelectionChanged)
|
|
||||||
callback_onSelectionChanged(m_selectedList);
|
|
||||||
event.handle();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
y += itemH;
|
|
||||||
}
|
}
|
||||||
|
y += itemH;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle ListView::getContentExtents(void) const
|
Rectangle ListView::getContentExtents(void) const
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!m_extentsDirty)
|
if (!m_extentsDirty)
|
||||||
return m_cachedExtents;
|
|
||||||
f32 maxX = 0, maxY = 0;
|
|
||||||
for (auto& item : m_list)
|
|
||||||
{
|
|
||||||
if (!item.isValid()) continue;
|
|
||||||
Vec2 size = item.getDimensions();
|
|
||||||
maxX = std::max(maxX, size.x);
|
|
||||||
maxY += size.y;
|
|
||||||
}
|
|
||||||
m_cachedExtents = { 0, 0, maxX, maxY };
|
|
||||||
m_extentsDirty = false;
|
|
||||||
return m_cachedExtents;
|
return m_cachedExtents;
|
||||||
}
|
f32 maxX = 0, maxY = 0;
|
||||||
|
for (auto& item : m_list)
|
||||||
ListView::Item& ListView::getLine(const String& text)
|
|
||||||
{
|
{
|
||||||
for (auto& item : m_list)
|
if (!item.isValid()) continue;
|
||||||
if (item.getText() == text)
|
Vec2 size = item.getDimensions();
|
||||||
return item;
|
maxX = std::max(maxX, size.x);
|
||||||
return InvalidItem;
|
maxY += size.y;
|
||||||
}
|
}
|
||||||
|
m_cachedExtents = { 0, 0, maxX, maxY };
|
||||||
|
m_extentsDirty = false;
|
||||||
|
return m_cachedExtents;
|
||||||
|
}
|
||||||
|
|
||||||
ListView::Item& ListView::getLine(u32 index)
|
ListView::Item& ListView::getLine(const String& text)
|
||||||
{
|
{
|
||||||
if (index < m_list.size())
|
for (auto& item : m_list)
|
||||||
return m_list[index];
|
if (item.getText() == text)
|
||||||
return InvalidItem;
|
return item;
|
||||||
}
|
return InvalidItem;
|
||||||
|
}
|
||||||
|
|
||||||
ListView::Item& ListView::addLine(const String& text)
|
ListView::Item& ListView::getLine(u32 index)
|
||||||
{
|
{
|
||||||
Item item { *this };
|
if (index < m_list.size())
|
||||||
item.setTextColor(m_defaultLinetextColor);
|
return m_list[index];
|
||||||
item.setPadding(m_defaultLinepadding);
|
return InvalidItem;
|
||||||
item.setFontSize(m_defaultLinefontSize);
|
}
|
||||||
item.setSelectedColor(m_defaultSelectionColor);
|
|
||||||
item.setSelectedTextColor(m_defaultSelectionTextColor);
|
|
||||||
item.setText(text);
|
|
||||||
m_list.push_back(item);
|
|
||||||
if (m_list.size() > 0)
|
|
||||||
m_list[0].set_selected(m_selectedList);
|
|
||||||
m_extentsDirty = true;
|
|
||||||
return m_list.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ListView::removeLine(const String& text)
|
ListView::Item& ListView::addLine(const String& text)
|
||||||
{
|
{
|
||||||
auto it = std::find_if(m_list.begin(), m_list.end(), [&](const Item& item) {
|
Item item { *this };
|
||||||
return item.getText() == text;
|
item.setTextColor(m_defaultLinetextColor);
|
||||||
});
|
item.setPadding(m_defaultLinepadding);
|
||||||
if (it != m_list.end())
|
item.setFontSize(m_defaultLinefontSize);
|
||||||
{
|
item.setSelectedColor(m_defaultSelectionColor);
|
||||||
m_list.erase(it);
|
item.setSelectedTextColor(m_defaultSelectionTextColor);
|
||||||
return true;
|
item.setText(text);
|
||||||
}
|
m_list.push_back(item);
|
||||||
return false;
|
if (m_list.size() > 0)
|
||||||
}
|
m_list[0].set_selected(m_selectedList);
|
||||||
|
m_extentsDirty = true;
|
||||||
|
return m_list.back();
|
||||||
|
}
|
||||||
|
|
||||||
bool ListView::removeLine(u32 index)
|
bool ListView::removeLine(const String& text)
|
||||||
|
{
|
||||||
|
auto it = std::find_if(m_list.begin(), m_list.end(), [&](const Item& item) {
|
||||||
|
return item.getText() == text;
|
||||||
|
});
|
||||||
|
if (it != m_list.end())
|
||||||
{
|
{
|
||||||
if (!hasLine(index))
|
m_list.erase(it);
|
||||||
return false;
|
|
||||||
m_list.erase(m_list.begin() + index);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool ListView::hasLine(const String& text)
|
bool ListView::removeLine(u32 index)
|
||||||
{
|
{
|
||||||
for (auto& item : m_list)
|
if (!hasLine(index))
|
||||||
if (item.getText() == text)
|
|
||||||
return true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
m_list.erase(m_list.begin() + index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ListView::hasLine(u32 index)
|
bool ListView::hasLine(const String& text)
|
||||||
{
|
{
|
||||||
return m_list.size() > index;
|
for (auto& item : m_list)
|
||||||
}
|
if (item.getText() == text)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ListView::hasLine(u32 index)
|
||||||
|
{
|
||||||
|
return m_list.size() > index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,92 +28,89 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
class ListView : public ScrollableWidget
|
||||||
{
|
{
|
||||||
class ListView : public ScrollableWidget
|
public: class Item : public ostd::__i_stringeable
|
||||||
{
|
{
|
||||||
public: class Item : public ostd::__i_stringeable
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
inline Item(ListView& parent) { m_parent = &parent; }
|
|
||||||
void setText(const String& text);
|
|
||||||
void setFontSize(i32 fontSize);
|
|
||||||
|
|
||||||
inline String toString(void) const override { return m_text; }
|
|
||||||
inline bool isValid(void) const { return m_parent != nullptr && m_text.new_trim() != ""; }
|
|
||||||
|
|
||||||
inline Vec2 getDimensions(void) const { return m_dimensions; }
|
|
||||||
inline i32 getFontSize(void) const { return m_fontSize; }
|
|
||||||
inline String getText(void) const { return m_text; }
|
|
||||||
inline bool isSelected(void) const { return m_selected; }
|
|
||||||
|
|
||||||
OSTD_PARAM_GETSET(Color, TextColor, m_textColor);
|
|
||||||
OSTD_PARAM_GETSET(Rectangle, Padding, m_padding);
|
|
||||||
OSTD_PARAM_GETSET(Color, SelectedTextColor, m_selectedTextColor);
|
|
||||||
OSTD_PARAM_GETSET(Color, SelectedColor, m_selectedColor);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void update_dimensions(void);
|
|
||||||
void set_selected(stdvec<Item*>& selectionList);
|
|
||||||
void add_selected(stdvec<Item*>& selectionList);
|
|
||||||
void remove_selected(stdvec<Item*>& selectionList);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Vec2 m_dimensions { 0, 0 };
|
|
||||||
bool m_selected { false };
|
|
||||||
ListView* m_parent { nullptr };
|
|
||||||
String m_text { "" };
|
|
||||||
i32 m_fontSize { 16 };
|
|
||||||
Color m_textColor { 30, 30, 30 };
|
|
||||||
Color m_selectedTextColor { Colors::White };
|
|
||||||
Color m_selectedColor { Colors::Crimson };
|
|
||||||
Rectangle m_padding { 5, 5, 20, 0 };
|
|
||||||
|
|
||||||
friend class ListView;
|
|
||||||
};
|
|
||||||
public:
|
public:
|
||||||
inline ListView(WindowCore& window) : ScrollableWidget(window) { create(); }
|
inline Item(ListView& parent) { m_parent = &parent; }
|
||||||
ListView& create(void);
|
void setText(const String& text);
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
void setFontSize(i32 fontSize);
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
|
||||||
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
|
|
||||||
void onMouseReleased(const Event& event) override;
|
|
||||||
Rectangle getContentExtents(void) const override;
|
|
||||||
Item& getLine(const String& text);
|
|
||||||
Item& getLine(u32 index);
|
|
||||||
Item& addLine(const String& text);
|
|
||||||
bool removeLine(const String& text);
|
|
||||||
bool removeLine(u32 index);
|
|
||||||
bool hasLine(const String& text);
|
|
||||||
bool hasLine(u32 index);
|
|
||||||
inline void addLine(const Item& line) { m_list.push_back(line); if (m_list.size() == 1) m_list.back().set_selected(m_selectedList); }
|
|
||||||
inline stdvec<Item*>& getSelection(void) { return m_selectedList; }
|
|
||||||
inline void setSelectionChangedCallback(std::function<void(stdvec<Item*>& selection)> callback) { callback_onSelectionChanged = std::move(callback); }
|
|
||||||
|
|
||||||
OSTD_PARAM_GETSET(Color, SeparatorLineColor, m_lineColor);
|
inline String toString(void) const override { return m_text; }
|
||||||
OSTD_BOOL_PARAM_GETSET_E(ShowSeparatorLine, m_showLine);
|
inline bool isValid(void) const { return m_parent != nullptr && m_text.new_trim() != ""; }
|
||||||
OSTD_PARAM_GETSET(i32, DefaultLineFontSize, m_defaultLinefontSize);
|
|
||||||
OSTD_PARAM_GETSET(Color, DefaultLineTextColor, m_defaultLinetextColor);
|
inline Vec2 getDimensions(void) const { return m_dimensions; }
|
||||||
OSTD_PARAM_GETSET(Rectangle, DefaultLinePadding, m_defaultLinepadding);
|
inline i32 getFontSize(void) const { return m_fontSize; }
|
||||||
OSTD_PARAM_GETSET(Color, DefaultLineSelectionColor, m_defaultSelectionColor);
|
inline String getText(void) const { return m_text; }
|
||||||
OSTD_PARAM_GETSET(Color, DefaultLineSelectionTextColor, m_defaultSelectionTextColor);
|
inline bool isSelected(void) const { return m_selected; }
|
||||||
|
|
||||||
|
OSTD_PARAM_GETSET(Color, TextColor, m_textColor);
|
||||||
|
OSTD_PARAM_GETSET(Rectangle, Padding, m_padding);
|
||||||
|
OSTD_PARAM_GETSET(Color, SelectedTextColor, m_selectedTextColor);
|
||||||
|
OSTD_PARAM_GETSET(Color, SelectedColor, m_selectedColor);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Item InvalidItem { *this };
|
void update_dimensions(void);
|
||||||
std::deque<Item> m_list;
|
void set_selected(stdvec<Item*>& selectionList);
|
||||||
stdvec<Item*> m_selectedList;
|
void add_selected(stdvec<Item*>& selectionList);
|
||||||
std::function<void(stdvec<Item*>& selection)> callback_onSelectionChanged { nullptr };
|
void remove_selected(stdvec<Item*>& selectionList);
|
||||||
mutable Rectangle m_cachedExtents { 0, 0, 0, 0 };
|
|
||||||
mutable bool m_extentsDirty { true };
|
|
||||||
|
|
||||||
Color m_lineColor { 40, 40, 40 };
|
private:
|
||||||
bool m_showLine { true };
|
Vec2 m_dimensions { 0, 0 };
|
||||||
i32 m_defaultLinefontSize { 16 };
|
bool m_selected { false };
|
||||||
Color m_defaultLinetextColor { 30, 30, 30 };
|
ListView* m_parent { nullptr };
|
||||||
Rectangle m_defaultLinepadding { 5, 5, 20, 0 };
|
String m_text { "" };
|
||||||
Color m_defaultSelectionColor { Colors::Crimson };
|
i32 m_fontSize { 16 };
|
||||||
Color m_defaultSelectionTextColor { Colors::White };
|
Color m_textColor { 30, 30, 30 };
|
||||||
|
Color m_selectedTextColor { Colors::White };
|
||||||
|
Color m_selectedColor { Colors::Crimson };
|
||||||
|
Rectangle m_padding { 5, 5, 20, 0 };
|
||||||
|
|
||||||
|
friend class ListView;
|
||||||
};
|
};
|
||||||
}
|
public:
|
||||||
|
inline ListView(WindowCore& window) : ScrollableWidget(window) { create(); }
|
||||||
|
ListView& create(void);
|
||||||
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
|
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
|
void onMouseReleased(const Event& event) override;
|
||||||
|
Rectangle getContentExtents(void) const override;
|
||||||
|
Item& getLine(const String& text);
|
||||||
|
Item& getLine(u32 index);
|
||||||
|
Item& addLine(const String& text);
|
||||||
|
bool removeLine(const String& text);
|
||||||
|
bool removeLine(u32 index);
|
||||||
|
bool hasLine(const String& text);
|
||||||
|
bool hasLine(u32 index);
|
||||||
|
inline void addLine(const Item& line) { m_list.push_back(line); if (m_list.size() == 1) m_list.back().set_selected(m_selectedList); }
|
||||||
|
inline stdvec<Item*>& getSelection(void) { return m_selectedList; }
|
||||||
|
inline void setSelectionChangedCallback(std::function<void(stdvec<Item*>& selection)> callback) { callback_onSelectionChanged = std::move(callback); }
|
||||||
|
|
||||||
|
OSTD_PARAM_GETSET(Color, SeparatorLineColor, m_lineColor);
|
||||||
|
OSTD_BOOL_PARAM_GETSET_E(ShowSeparatorLine, m_showLine);
|
||||||
|
OSTD_PARAM_GETSET(i32, DefaultLineFontSize, m_defaultLinefontSize);
|
||||||
|
OSTD_PARAM_GETSET(Color, DefaultLineTextColor, m_defaultLinetextColor);
|
||||||
|
OSTD_PARAM_GETSET(Rectangle, DefaultLinePadding, m_defaultLinepadding);
|
||||||
|
OSTD_PARAM_GETSET(Color, DefaultLineSelectionColor, m_defaultSelectionColor);
|
||||||
|
OSTD_PARAM_GETSET(Color, DefaultLineSelectionTextColor, m_defaultSelectionTextColor);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Item InvalidItem { *this };
|
||||||
|
std::deque<Item> m_list;
|
||||||
|
stdvec<Item*> m_selectedList;
|
||||||
|
std::function<void(stdvec<Item*>& selection)> callback_onSelectionChanged { nullptr };
|
||||||
|
mutable Rectangle m_cachedExtents { 0, 0, 0, 0 };
|
||||||
|
mutable bool m_extentsDirty { true };
|
||||||
|
|
||||||
|
Color m_lineColor { 40, 40, 40 };
|
||||||
|
bool m_showLine { true };
|
||||||
|
i32 m_defaultLinefontSize { 16 };
|
||||||
|
Color m_defaultLinetextColor { 30, 30, 30 };
|
||||||
|
Rectangle m_defaultLinepadding { 5, 5, 20, 0 };
|
||||||
|
Color m_defaultSelectionColor { Colors::Crimson };
|
||||||
|
Color m_defaultSelectionTextColor { Colors::White };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,73 +25,70 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
ProgressBar& ProgressBar::create(void)
|
||||||
{
|
{
|
||||||
ProgressBar& ProgressBar::create(void)
|
setPadding({ 0, 0, 0, 0 });
|
||||||
{
|
setTypeName("ogfx::gui::ProgressBar");
|
||||||
setPadding({ 0, 0, 0, 0 });
|
disableChildren();
|
||||||
setTypeName("ogfx::gui::widgets::ProgressBar");
|
enableBackground();
|
||||||
disableChildren();
|
enableBorder();
|
||||||
enableBackground();
|
setStylesheetCategoryName("progressbar");
|
||||||
enableBorder();
|
validate();
|
||||||
setStylesheetCategoryName("progressbar");
|
return *this;
|
||||||
validate();
|
}
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProgressBar::applyTheme(const ostd::Stylesheet& theme)
|
void ProgressBar::applyTheme(const ostd::Stylesheet& theme)
|
||||||
{
|
{
|
||||||
setProgressColor(getThemeValue<Color>(theme, "progressColor", getProgressColor()));
|
setProgressColor(getThemeValue<Color>(theme, "progressColor", getProgressColor()));
|
||||||
enableShowDecimal(getThemeValue<bool>(theme, "showDecimal", isShowDecimalEnabled()));
|
enableShowDecimal(getThemeValue<bool>(theme, "showDecimal", isShowDecimalEnabled()));
|
||||||
enableShowText(getThemeValue<bool>(theme, "showText", isShowTextEnabled()));
|
enableShowText(getThemeValue<bool>(theme, "showText", isShowTextEnabled()));
|
||||||
enableProgressGradient(getThemeValue<bool>(theme, "useProgressGradient", isProgressGradientEnabled()));
|
enableProgressGradient(getThemeValue<bool>(theme, "useProgressGradient", isProgressGradientEnabled()));
|
||||||
setProgressGradient(getThemeValue<ColorGradient>(theme, "progressGradient", getProgressGradient()));
|
setProgressGradient(getThemeValue<ColorGradient>(theme, "progressGradient", getProgressGradient()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgressBar::onDraw(ogfx::BasicRenderer2D& gfx)
|
void ProgressBar::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
f32 prog = getProgressNormalized();
|
||||||
|
f32 progressw = (prog * getw());
|
||||||
|
if (isProgressGradientEnabled())
|
||||||
|
gfx.fillGradientRect({ getGlobalPosition(), { progressw, geth() } }, m_progressGradient);
|
||||||
|
else
|
||||||
|
gfx.fillRect({ getGlobalPosition(), { progressw, geth() } }, getProgressColor());
|
||||||
|
if (isShowTextEnabled())
|
||||||
{
|
{
|
||||||
f32 prog = getProgressNormalized();
|
String text = "";
|
||||||
f32 progressw = (prog * getw());
|
if (isShowDecimalEnabled())
|
||||||
if (isProgressGradientEnabled())
|
text.add(prog * 100, 2).add("%");
|
||||||
gfx.fillGradientRect({ getGlobalPosition(), { progressw, geth() } }, m_progressGradient);
|
|
||||||
else
|
else
|
||||||
gfx.fillRect({ getGlobalPosition(), { progressw, geth() } }, getProgressColor());
|
text.add(cast<i32>(std::round(prog * 100))).add("%");
|
||||||
if (isShowTextEnabled())
|
gfx.drawCenteredString(text, getGlobalBounds(), getTextColor(), getFontSize());
|
||||||
{
|
|
||||||
String text = "";
|
|
||||||
if (isShowDecimalEnabled())
|
|
||||||
text.add(prog * 100, 2).add("%");
|
|
||||||
else
|
|
||||||
text.add(cast<i32>(std::round(prog * 100))).add("%");
|
|
||||||
gfx.drawCenteredString(text, getGlobalBounds(), getTextColor(), getFontSize());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ProgressBar::setProgressNormalized(f32 normalized_value)
|
void ProgressBar::setProgressNormalized(f32 normalized_value)
|
||||||
{
|
{
|
||||||
normalized_value = std::clamp(normalized_value, 0.0f, 1.0f);
|
normalized_value = std::clamp(normalized_value, 0.0f, 1.0f);
|
||||||
f32 next = m_min + normalized_value * (m_max - m_min);
|
f32 next = m_min + normalized_value * (m_max - m_min);
|
||||||
m_progress.store(next, std::memory_order_relaxed);
|
m_progress.store(next, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgressBar::setProgress(f32 value)
|
void ProgressBar::setProgress(f32 value)
|
||||||
{
|
{
|
||||||
f32 current = m_progress.load(std::memory_order_relaxed);
|
f32 current = m_progress.load(std::memory_order_relaxed);
|
||||||
f32 next = current;
|
f32 next = current;
|
||||||
next = std::clamp(value, m_min, m_max);
|
next = std::clamp(value, m_min, m_max);
|
||||||
m_progress.store(next, std::memory_order_relaxed);
|
m_progress.store(next, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
f32 ProgressBar::getProgress(void) const
|
f32 ProgressBar::getProgress(void) const
|
||||||
{
|
{
|
||||||
return m_progress.load(std::memory_order_relaxed);
|
return m_progress.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
f32 ProgressBar::getProgressNormalized(void) const
|
f32 ProgressBar::getProgressNormalized(void) const
|
||||||
{
|
{
|
||||||
f32 s = m_progress.load(std::memory_order_relaxed);
|
f32 s = m_progress.load(std::memory_order_relaxed);
|
||||||
return (s - m_min) / (m_max - m_min);
|
return (s - m_min) / (m_max - m_min);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,38 +27,35 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
class ProgressBar : public Widget
|
||||||
{
|
{
|
||||||
class ProgressBar : public Widget
|
public:
|
||||||
{
|
inline ProgressBar(WindowCore& window, f32 min = 0.0f, f32 max = 1.0f, f32 start = 0.0f) : Widget({ 0, 0, 0, 0 }, window), m_progress(std::clamp(start, min, max)), m_min(min), m_max(max) { create(); }
|
||||||
public:
|
ProgressBar& create(void);
|
||||||
inline ProgressBar(WindowCore& window, f32 min = 0.0f, f32 max = 1.0f, f32 start = 0.0f) : Widget({ 0, 0, 0, 0 }, window), m_progress(std::clamp(start, min, max)), m_min(min), m_max(max) { create(); }
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
ProgressBar& create(void);
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
void setProgressNormalized(f32 normalized_value);
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
void setProgress(f32 value);
|
||||||
void setProgressNormalized(f32 normalized_value);
|
f32 getProgress(void) const;
|
||||||
void setProgress(f32 value);
|
f32 getProgressNormalized(void) const;
|
||||||
f32 getProgress(void) const;
|
OSTD_PARAM_GETSET(Color, ProgressColor, m_progressColor);
|
||||||
f32 getProgressNormalized(void) const;
|
OSTD_BOOL_PARAM_GETSET_E(ShowDecimal, m_showDecimal);
|
||||||
OSTD_PARAM_GETSET(Color, ProgressColor, m_progressColor);
|
OSTD_BOOL_PARAM_GETSET_E(ShowText, m_showText);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(ShowDecimal, m_showDecimal);
|
OSTD_BOOL_PARAM_GETSET_E(ProgressGradient, m_useProgressGradient);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(ShowText, m_showText);
|
OSTD_PARAM_GETSET(ColorGradient, ProgressGradient, m_progressGradient);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(ProgressGradient, m_useProgressGradient);
|
|
||||||
OSTD_PARAM_GETSET(ColorGradient, ProgressGradient, m_progressGradient);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Color m_progressColor { 255, 255, 255 };
|
Color m_progressColor { 255, 255, 255 };
|
||||||
f32 m_min { 0.0f };
|
f32 m_min { 0.0f };
|
||||||
f32 m_max { 1.0f };
|
f32 m_max { 1.0f };
|
||||||
std::atomic<f32> m_progress;
|
std::atomic<f32> m_progress;
|
||||||
bool m_showDecimal { false };
|
bool m_showDecimal { false };
|
||||||
bool m_showText { true };
|
bool m_showText { true };
|
||||||
bool m_useProgressGradient { true };
|
bool m_useProgressGradient { true };
|
||||||
ColorGradient m_progressGradient {
|
ColorGradient m_progressGradient {
|
||||||
{ Colors::Crimson.darkened(0.1f), Colors::Crimson.darkened(0.35f) },
|
{ Colors::Crimson.darkened(0.1f), Colors::Crimson.darkened(0.35f) },
|
||||||
{ 1.0f }
|
{ 1.0f }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,123 +25,120 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
RadioButton& RadioButton::create(RadioButtonGroup& group, const String& text)
|
||||||
{
|
{
|
||||||
RadioButton& RadioButton::create(RadioButtonGroup& group, const String& text)
|
m_radioGroup = &group;
|
||||||
{
|
setText(text);
|
||||||
m_radioGroup = &group;
|
setPadding({ 5, 5, 5, 5 });
|
||||||
setText(text);
|
setTypeName("ogfx::gui::RadioButton");
|
||||||
setPadding({ 5, 5, 5, 5 });
|
disableChildren();
|
||||||
setTypeName("ogfx::gui::widgets::RadioButton");
|
enableBackground(false);
|
||||||
disableChildren();
|
setStylesheetCategoryName("radioButton");
|
||||||
enableBackground(false);
|
validate();
|
||||||
setStylesheetCategoryName("radioButton");
|
return *this;
|
||||||
validate();
|
}
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RadioButton::applyTheme(const ostd::Stylesheet& theme)
|
void RadioButton::applyTheme(const ostd::Stylesheet& theme)
|
||||||
{
|
{
|
||||||
setInnerCircleColor(getThemeValue<Color>(theme, "innerCircleColor", m_innerCircleColor));
|
setInnerCircleColor(getThemeValue<Color>(theme, "innerCircleColor", m_innerCircleColor));
|
||||||
setOuterCircleColor(getThemeValue<Color>(theme, "outerCircleColor", m_outerCircleColor));
|
setOuterCircleColor(getThemeValue<Color>(theme, "outerCircleColor", m_outerCircleColor));
|
||||||
m_outerCircleBorderWidth = getThemeValue<i32>(theme, "outerCircleBorderWidth", m_outerCircleBorderWidth);
|
m_outerCircleBorderWidth = getThemeValue<i32>(theme, "outerCircleBorderWidth", m_outerCircleBorderWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RadioButton::onMouseReleased(const Event& event)
|
void RadioButton::onMouseReleased(const Event& event)
|
||||||
|
{
|
||||||
|
if (!isMouseInside())
|
||||||
|
return;
|
||||||
|
if (m_radioGroup == nullptr)
|
||||||
|
return;
|
||||||
|
m_radioGroup->set_selected(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadioButton::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
if (m_textChanged)
|
||||||
|
__update_size(gfx);
|
||||||
|
gfx.drawCircle({ getGlobalContentPosition() + Vec2 { 0, 4 }, m_circleSize }, getOuterCircleColor(), getOuterCircleBorderWidth());
|
||||||
|
if (isSelected())
|
||||||
|
gfx.fillCircle({ getGlobalContentPosition() + Vec2 { 3, 7 }, m_circleSize - 6 }, getInnerCircleColor());
|
||||||
|
gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_circleSize.x + m_spacing, 0 }, getTextColor(), getFontSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadioButton::setText(const String& text)
|
||||||
|
{
|
||||||
|
m_text = text;
|
||||||
|
m_textChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadioButton::__update_size(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
auto size = gfx.getStringDimensions(getText(), getFontSize());
|
||||||
|
m_circleSize = { cast<f32>(size.y - 6), cast<f32>(size.y - 6) };
|
||||||
|
size.x += m_spacing + m_circleSize.x;
|
||||||
|
size.x += getPadding().left();
|
||||||
|
size.x += getPadding().right();
|
||||||
|
size.y += getPadding().top();
|
||||||
|
size.y += getPadding().bottom();
|
||||||
|
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
|
||||||
|
m_textChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadioButton::__set_selected(bool selected)
|
||||||
|
{
|
||||||
|
if (m_selected == selected)
|
||||||
|
return;
|
||||||
|
m_selected = selected;
|
||||||
|
setThemeQualifier("active", m_selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
RadioButtonGroup::RadioButtonGroup(void)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
RadioButton& RadioButtonGroup::addButton(Widget& parent, const String& text, const Vec2& position)
|
||||||
|
{
|
||||||
|
m_buttons.push_back(std::unique_ptr<RadioButton>(
|
||||||
|
new RadioButton(parent.getWindow(), *this, text)
|
||||||
|
));
|
||||||
|
auto& btn = *m_buttons.back();
|
||||||
|
parent.addWidget(btn, position);
|
||||||
|
if (m_buttons.size() == 1)
|
||||||
|
set_selected(btn);
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadioButtonGroup::set_selected(RadioButton& sender)
|
||||||
|
{
|
||||||
|
if (is_button_present(sender))
|
||||||
{
|
{
|
||||||
if (!isMouseInside())
|
if (m_selected != nullptr && m_selected == &sender)
|
||||||
return;
|
return;
|
||||||
if (m_radioGroup == nullptr)
|
unselect_all();
|
||||||
return;
|
auto previous = m_selected;
|
||||||
m_radioGroup->set_selected(*this);
|
m_selected = &sender;
|
||||||
|
m_selected->__set_selected(true);
|
||||||
|
if (previous != nullptr && callback_onSelectionChanged != nullptr)
|
||||||
|
callback_onSelectionChanged(*previous, sender);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RadioButton::onDraw(ogfx::BasicRenderer2D& gfx)
|
bool RadioButtonGroup::is_button_present(RadioButton& button)
|
||||||
{
|
{
|
||||||
if (m_textChanged)
|
for (auto& btn : m_buttons)
|
||||||
__update_size(gfx);
|
if (btn.get() == &button)
|
||||||
gfx.drawCircle({ getGlobalContentPosition() + Vec2 { 0, 4 }, m_circleSize }, getOuterCircleColor(), getOuterCircleBorderWidth());
|
return true;
|
||||||
if (isSelected())
|
return false;
|
||||||
gfx.fillCircle({ getGlobalContentPosition() + Vec2 { 3, 7 }, m_circleSize - 6 }, getInnerCircleColor());
|
}
|
||||||
gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_circleSize.x + m_spacing, 0 }, getTextColor(), getFontSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RadioButton::setText(const String& text)
|
void RadioButtonGroup::unselect_all(void)
|
||||||
{
|
{
|
||||||
m_text = text;
|
for (auto& btn : m_buttons)
|
||||||
m_textChanged = true;
|
btn->__set_selected(false);
|
||||||
}
|
|
||||||
|
|
||||||
void RadioButton::__update_size(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
auto size = gfx.getStringDimensions(getText(), getFontSize());
|
|
||||||
m_circleSize = { cast<f32>(size.y - 6), cast<f32>(size.y - 6) };
|
|
||||||
size.x += m_spacing + m_circleSize.x;
|
|
||||||
size.x += getPadding().left();
|
|
||||||
size.x += getPadding().right();
|
|
||||||
size.y += getPadding().top();
|
|
||||||
size.y += getPadding().bottom();
|
|
||||||
setSize({ cast<f32>(size.x), cast<f32>(size.y) });
|
|
||||||
m_textChanged = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RadioButton::__set_selected(bool selected)
|
|
||||||
{
|
|
||||||
if (m_selected == selected)
|
|
||||||
return;
|
|
||||||
m_selected = selected;
|
|
||||||
setThemeQualifier("active", m_selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
RadioButtonGroup::RadioButtonGroup(void)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
RadioButton& RadioButtonGroup::addButton(Widget& parent, const String& text, const Vec2& position)
|
|
||||||
{
|
|
||||||
m_buttons.push_back(std::unique_ptr<RadioButton>(
|
|
||||||
new RadioButton(parent.getWindow(), *this, text)
|
|
||||||
));
|
|
||||||
auto& btn = *m_buttons.back();
|
|
||||||
parent.addWidget(btn, position);
|
|
||||||
if (m_buttons.size() == 1)
|
|
||||||
set_selected(btn);
|
|
||||||
return btn;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RadioButtonGroup::set_selected(RadioButton& sender)
|
|
||||||
{
|
|
||||||
if (is_button_present(sender))
|
|
||||||
{
|
|
||||||
if (m_selected != nullptr && m_selected == &sender)
|
|
||||||
return;
|
|
||||||
unselect_all();
|
|
||||||
auto previous = m_selected;
|
|
||||||
m_selected = &sender;
|
|
||||||
m_selected->__set_selected(true);
|
|
||||||
if (previous != nullptr && callback_onSelectionChanged != nullptr)
|
|
||||||
callback_onSelectionChanged(*previous, sender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RadioButtonGroup::is_button_present(RadioButton& button)
|
|
||||||
{
|
|
||||||
for (auto& btn : m_buttons)
|
|
||||||
if (btn.get() == &button)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RadioButtonGroup::unselect_all(void)
|
|
||||||
{
|
|
||||||
for (auto& btn : m_buttons)
|
|
||||||
btn->__set_selected(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,60 +26,57 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
class RadioButtonGroup;
|
||||||
|
class RadioButton : public Widget
|
||||||
{
|
{
|
||||||
class RadioButtonGroup;
|
public:
|
||||||
class RadioButton : public Widget
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
{
|
void onMouseReleased(const Event& event) override;
|
||||||
public:
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
void setText(const String& text);
|
||||||
void onMouseReleased(const Event& event) override;
|
inline String getText(void) const { return m_text; }
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
inline bool isSelected(void) const { return m_selected; }
|
||||||
void setText(const String& text);
|
OSTD_PARAM_GETSET(i32, OuterCircleBorderWidth, m_outerCircleBorderWidth);
|
||||||
inline String getText(void) const { return m_text; }
|
OSTD_PARAM_GETSET(Color, InnerCircleColor, m_innerCircleColor);
|
||||||
inline bool isSelected(void) const { return m_selected; }
|
OSTD_PARAM_GETSET(Color, OuterCircleColor, m_outerCircleColor);
|
||||||
OSTD_PARAM_GETSET(i32, OuterCircleBorderWidth, m_outerCircleBorderWidth);
|
|
||||||
OSTD_PARAM_GETSET(Color, InnerCircleColor, m_innerCircleColor);
|
|
||||||
OSTD_PARAM_GETSET(Color, OuterCircleColor, m_outerCircleColor);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
inline RadioButton(WindowCore& window, RadioButtonGroup& group, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(group, text); }
|
inline RadioButton(WindowCore& window, RadioButtonGroup& group, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(group, text); }
|
||||||
RadioButton& create(RadioButtonGroup& group, const String& text);
|
RadioButton& create(RadioButtonGroup& group, const String& text);
|
||||||
void __update_size(ogfx::BasicRenderer2D& gfx);
|
void __update_size(ogfx::BasicRenderer2D& gfx);
|
||||||
void __set_selected(bool selected);
|
void __set_selected(bool selected);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RadioButtonGroup* m_radioGroup { nullptr };
|
RadioButtonGroup* m_radioGroup { nullptr };
|
||||||
Vec2 m_circleSize { 0, 0 };
|
Vec2 m_circleSize { 0, 0 };
|
||||||
bool m_selected { false };
|
bool m_selected { false };
|
||||||
String m_text { "" };
|
String m_text { "" };
|
||||||
bool m_textChanged { false };
|
bool m_textChanged { false };
|
||||||
f32 m_spacing { 6 };
|
f32 m_spacing { 6 };
|
||||||
i32 m_outerCircleBorderWidth { 1 };
|
i32 m_outerCircleBorderWidth { 1 };
|
||||||
Color m_innerCircleColor { 255, 255, 255 };
|
Color m_innerCircleColor { 255, 255, 255 };
|
||||||
Color m_outerCircleColor { 255, 255, 255 };
|
Color m_outerCircleColor { 255, 255, 255 };
|
||||||
|
|
||||||
friend class RadioButtonGroup;
|
friend class RadioButtonGroup;
|
||||||
};
|
};
|
||||||
class RadioButtonGroup
|
class RadioButtonGroup
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
RadioButtonGroup(void);
|
RadioButtonGroup(void);
|
||||||
RadioButton& addButton(Widget& parent, const String& text, const Vec2& position = { 0, 0 });
|
RadioButton& addButton(Widget& parent, const String& text, const Vec2& position = { 0, 0 });
|
||||||
inline void setSelectionChangedCallback(std::function<void(RadioButton& previous, RadioButton& sender)> callback) { callback_onSelectionChanged = std::move(callback); }
|
inline void setSelectionChangedCallback(std::function<void(RadioButton& previous, RadioButton& sender)> callback) { callback_onSelectionChanged = std::move(callback); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void set_selected(RadioButton& sender);
|
void set_selected(RadioButton& sender);
|
||||||
bool is_button_present(RadioButton& button);
|
bool is_button_present(RadioButton& button);
|
||||||
void unselect_all(void);
|
void unselect_all(void);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
stdvec<std::unique_ptr<RadioButton>> m_buttons;
|
stdvec<std::unique_ptr<RadioButton>> m_buttons;
|
||||||
RadioButton* m_selected { nullptr };
|
RadioButton* m_selected { nullptr };
|
||||||
std::function<void(RadioButton& previous, RadioButton& sender)> callback_onSelectionChanged { nullptr };
|
std::function<void(RadioButton& previous, RadioButton& sender)> callback_onSelectionChanged { nullptr };
|
||||||
|
|
||||||
friend class RadioButton;
|
friend class RadioButton;
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,43 +26,40 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
RootWidget::RootWidget(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window)
|
||||||
{
|
{
|
||||||
RootWidget::RootWidget(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window)
|
setRootChild();
|
||||||
{
|
setSize(cast<f32>(window.getWindowWidth()), cast<f32>(window.getWindowHeight()));
|
||||||
setRootChild();
|
setStylesheetCategoryName("window");
|
||||||
setSize(cast<f32>(window.getWindowWidth()), cast<f32>(window.getWindowHeight()));
|
setTypeName("ogfx::gui::RootWidget");
|
||||||
setStylesheetCategoryName("window");
|
}
|
||||||
setTypeName("ogfx::gui::widgets::RootWidget");
|
|
||||||
}
|
|
||||||
|
|
||||||
void RootWidget::onWindowResized(const Event& event)
|
void RootWidget::onWindowResized(const Event& event)
|
||||||
{
|
{
|
||||||
Window& win = cast<Window&>(getWindow()); //TODO: Potentially unsage?
|
Window& win = cast<Window&>(getWindow()); //TODO: Potentially unsage?
|
||||||
f32 offset_y = 0;
|
f32 offset_y = 0;
|
||||||
if (win.m_menubar.isVisible())
|
if (win.m_menubar.isVisible())
|
||||||
offset_y += win.m_menubar.geth();
|
offset_y += win.m_menubar.geth();
|
||||||
if (win.m_toolbar.isVisible())
|
if (win.m_toolbar.isVisible())
|
||||||
offset_y += win.m_toolbar.geth();
|
offset_y += win.m_toolbar.geth();
|
||||||
sety(offset_y);
|
sety(offset_y);
|
||||||
if (win.m_statusbar.isVisible())
|
if (win.m_statusbar.isVisible())
|
||||||
offset_y += win.m_statusbar.geth();
|
offset_y += win.m_statusbar.geth();
|
||||||
setSize(cast<f32>(event.windowResized->new_width), cast<f32>(event.windowResized->new_height) - offset_y);
|
setSize(cast<f32>(event.windowResized->new_width), cast<f32>(event.windowResized->new_height) - offset_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RootWidget::applyTheme(const ostd::Stylesheet& theme)
|
void RootWidget::applyTheme(const ostd::Stylesheet& theme)
|
||||||
{
|
{
|
||||||
setTooltipBackgroundColor(getThemeValue<Color>(theme, "tooltip.backgroundColor", getTooltipBackgroundColor()));
|
setTooltipBackgroundColor(getThemeValue<Color>(theme, "tooltip.backgroundColor", getTooltipBackgroundColor()));
|
||||||
setTooltipBorderColor(getThemeValue<Color>(theme, "tooltip.borderColor", getTooltipBorderColor()));
|
setTooltipBorderColor(getThemeValue<Color>(theme, "tooltip.borderColor", getTooltipBorderColor()));
|
||||||
setTooltipTextColor(getThemeValue<Color>(theme, "tooltip.textColor", getTooltipTextColor()));
|
setTooltipTextColor(getThemeValue<Color>(theme, "tooltip.textColor", getTooltipTextColor()));
|
||||||
setTooltipBorderWidth(getThemeValue<i32>(theme, "tooltip.borderWidth", getTooltipBorderWidth()));
|
setTooltipBorderWidth(getThemeValue<i32>(theme, "tooltip.borderWidth", getTooltipBorderWidth()));
|
||||||
setTooltipFontSize(getThemeValue<i32>(theme, "tooltip.fontSize", getTooltipFontSize()));
|
setTooltipFontSize(getThemeValue<i32>(theme, "tooltip.fontSize", getTooltipFontSize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RootWidget::onDraw(ogfx::BasicRenderer2D& gfx)
|
void RootWidget::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
{
|
{
|
||||||
gfx.fillRect({ 0, 0, cast<f32>(getWindow().getWindowWidth()), cast<f32>(getWindow().getWindowHeight()) }, getBackgroundColor());
|
gfx.fillRect({ 0, 0, cast<f32>(getWindow().getWindowWidth()), cast<f32>(getWindow().getWindowHeight()) }, getBackgroundColor());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,28 +26,25 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
class RootWidget : public Widget
|
||||||
{
|
{
|
||||||
class RootWidget : public Widget
|
public:
|
||||||
{
|
RootWidget(WindowCore& window);
|
||||||
public:
|
void onWindowResized(const Event& event) override;
|
||||||
RootWidget(WindowCore& window);
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
void onWindowResized(const Event& event) override;
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
OSTD_PARAM_GETSET(Color, TooltipBackgroundColor, m_tooltipBackgroundColor);
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
OSTD_PARAM_GETSET(Color, TooltipBorderColor, m_tooltipBorderColor);
|
||||||
OSTD_PARAM_GETSET(Color, TooltipBackgroundColor, m_tooltipBackgroundColor);
|
OSTD_PARAM_GETSET(Color, TooltipTextColor, m_tooltipTextColor);
|
||||||
OSTD_PARAM_GETSET(Color, TooltipBorderColor, m_tooltipBorderColor);
|
OSTD_PARAM_GETSET(i32, TooltipBorderWidth, m_tooltipBorderWidth);
|
||||||
OSTD_PARAM_GETSET(Color, TooltipTextColor, m_tooltipTextColor);
|
OSTD_PARAM_GETSET(i32, TooltipFontSize, m_tooltipFontSize);
|
||||||
OSTD_PARAM_GETSET(i32, TooltipBorderWidth, m_tooltipBorderWidth);
|
|
||||||
OSTD_PARAM_GETSET(i32, TooltipFontSize, m_tooltipFontSize);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Color m_tooltipBackgroundColor { "#FFF7D6FF" };
|
Color m_tooltipBackgroundColor { "#FFF7D6FF" };
|
||||||
Color m_tooltipBorderColor { 50, 50, 50 };
|
Color m_tooltipBorderColor { 50, 50, 50 };
|
||||||
Color m_tooltipTextColor { 50, 50, 50 };
|
Color m_tooltipTextColor { 50, 50, 50 };
|
||||||
i32 m_tooltipBorderWidth { 1 };
|
i32 m_tooltipBorderWidth { 1 };
|
||||||
i32 m_tooltipFontSize { 20 };
|
i32 m_tooltipFontSize { 20 };
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,443 +25,440 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
VerticalScrollBar& VerticalScrollBar::create(void)
|
||||||
{
|
{
|
||||||
VerticalScrollBar& VerticalScrollBar::create(void)
|
setPadding({ 0, 0, 0, 0 });
|
||||||
|
setMargin({ 0, 0, 0, 0 });
|
||||||
|
setTypeName("ogfx::gui::Label");
|
||||||
|
disableChildren();
|
||||||
|
enableBackground(true);
|
||||||
|
enableBorder(false);
|
||||||
|
enableTopMost(true);
|
||||||
|
enableIgnoreScroll(true);
|
||||||
|
setStylesheetCategoryName("scrollbar");
|
||||||
|
validate();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VerticalScrollBar::applyTheme(const ostd::Stylesheet& theme)
|
||||||
|
{
|
||||||
|
w = getThemeValue<f32>(theme, "width", 15);
|
||||||
|
m_thumbColor = getThemeValue<Color>(theme, "thumb.color", { 120, 120, 120 });
|
||||||
|
m_thumbBorderRadius = getThemeValue<f32>(theme, "thumb.borderRadius", 16);
|
||||||
|
m_thumbBorderColor = getThemeValue<Color>(theme, "thumb.borderColor", { 150, 150, 150 });
|
||||||
|
m_thumbShowBorder = getThemeValue<bool>(theme, "thumb.showBorder", true);
|
||||||
|
m_trackColor = getThemeValue<Color>(theme, "track.color", { 70, 70, 70 });
|
||||||
|
}
|
||||||
|
|
||||||
|
void VerticalScrollBar::afterDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
if (!getParent()->needsVScroll())
|
||||||
|
return;
|
||||||
|
gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii);
|
||||||
|
gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VerticalScrollBar::onMouseDragged(const Event& event)
|
||||||
|
{
|
||||||
|
if (!m_mousePressed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (isMouseInsideThumb({ event.mouse->position_x, event.mouse->position_y }))
|
||||||
|
m_mouseDragged = true;
|
||||||
|
|
||||||
|
if (m_mouseDragged)
|
||||||
{
|
{
|
||||||
setPadding({ 0, 0, 0, 0 });
|
|
||||||
setMargin({ 0, 0, 0, 0 });
|
|
||||||
setTypeName("ogfx::gui::widgets::Label");
|
|
||||||
disableChildren();
|
|
||||||
enableBackground(true);
|
|
||||||
enableBorder(false);
|
|
||||||
enableTopMost(true);
|
|
||||||
enableIgnoreScroll(true);
|
|
||||||
setStylesheetCategoryName("scrollbar");
|
|
||||||
validate();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VerticalScrollBar::applyTheme(const ostd::Stylesheet& theme)
|
|
||||||
{
|
|
||||||
w = getThemeValue<f32>(theme, "width", 15);
|
|
||||||
m_thumbColor = getThemeValue<Color>(theme, "thumb.color", { 120, 120, 120 });
|
|
||||||
m_thumbBorderRadius = getThemeValue<f32>(theme, "thumb.borderRadius", 16);
|
|
||||||
m_thumbBorderColor = getThemeValue<Color>(theme, "thumb.borderColor", { 150, 150, 150 });
|
|
||||||
m_thumbShowBorder = getThemeValue<bool>(theme, "thumb.showBorder", true);
|
|
||||||
m_trackColor = getThemeValue<Color>(theme, "track.color", { 70, 70, 70 });
|
|
||||||
}
|
|
||||||
|
|
||||||
void VerticalScrollBar::afterDraw(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
if (!getParent()->needsVScroll())
|
|
||||||
return;
|
|
||||||
gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii);
|
|
||||||
gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VerticalScrollBar::onMouseDragged(const Event& event)
|
|
||||||
{
|
|
||||||
if (!m_mousePressed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (isMouseInsideThumb({ event.mouse->position_x, event.mouse->position_y }))
|
|
||||||
m_mouseDragged = true;
|
|
||||||
|
|
||||||
if (m_mouseDragged)
|
|
||||||
{
|
|
||||||
auto bounds = getGlobalBounds() - m_correctionOffset;
|
|
||||||
f32 newThumbY = (event.mouse->position_y - bounds.y) - m_dragGrabOffset;
|
|
||||||
set_thumb_y(newThumbY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VerticalScrollBar::onMousePressed(const Event& event)
|
|
||||||
{
|
|
||||||
m_mousePressed = true;
|
|
||||||
m_dragGrabOffset = event.mouse->position_y - m_thumbGlobalBounds.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VerticalScrollBar::onMouseReleased(const Event& event)
|
|
||||||
{
|
|
||||||
m_mousePressed = false;
|
|
||||||
if (event.mouse->mousePressedOnWidget != this)
|
|
||||||
return;
|
|
||||||
if (!m_mouseDragged)
|
|
||||||
{
|
|
||||||
auto bounds = getGlobalBounds() - m_correctionOffset;
|
|
||||||
f32 newThumbY = (event.mouse->position_y - bounds.y);
|
|
||||||
newThumbY -= m_thumbHeight / 2.0f;
|
|
||||||
set_thumb_y(newThumbY);
|
|
||||||
}
|
|
||||||
m_mouseDragged = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VerticalScrollBar::onUpdate(void)
|
|
||||||
{
|
|
||||||
update_thumb();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VerticalScrollBar::isMouseInsideThumb(const Vec2& mouse_pos)
|
|
||||||
{
|
|
||||||
return m_thumbGlobalBounds.contains(mouse_pos, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VerticalScrollBar::update_thumb(void)
|
|
||||||
{
|
|
||||||
x = getParent()->getSize().x - w;
|
|
||||||
y = 0;
|
|
||||||
h = getParent()->getGlobalPureContentBounds().h;
|
|
||||||
|
|
||||||
auto ext = getParent()->getContentExtents();
|
|
||||||
auto cont = getParent()->getContentBounds();
|
|
||||||
auto scrollOfset = getParent()->getScrollOffset();
|
|
||||||
|
|
||||||
// Thumb size is proportional to visible/total ratio
|
|
||||||
f32 visibleRatio = cont.h / ext.h; // e.g. 0.4 means 40% visible
|
|
||||||
m_thumbHeight = std::max(20.0f, geth() * visibleRatio); // min 20px for usability
|
|
||||||
|
|
||||||
// Thumb position is proportional to scroll progress
|
|
||||||
f32 scrollProgress = -scrollOfset.y / (ext.h - cont.h); // 0 to 1
|
|
||||||
m_thumbY = gety() + ((geth() - m_thumbHeight) * scrollProgress);
|
|
||||||
|
|
||||||
auto bounds = getGlobalBounds() - m_correctionOffset;
|
auto bounds = getGlobalBounds() - m_correctionOffset;
|
||||||
m_thumbGlobalBounds = { bounds.x + 2, bounds.y + gety() + m_thumbY, getw() - 4, m_thumbHeight };
|
f32 newThumbY = (event.mouse->position_y - bounds.y) - m_dragGrabOffset;
|
||||||
|
set_thumb_y(newThumbY);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void VerticalScrollBar::set_thumb_y(f32 thumby)
|
void VerticalScrollBar::onMousePressed(const Event& event)
|
||||||
|
{
|
||||||
|
m_mousePressed = true;
|
||||||
|
m_dragGrabOffset = event.mouse->position_y - m_thumbGlobalBounds.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VerticalScrollBar::onMouseReleased(const Event& event)
|
||||||
|
{
|
||||||
|
m_mousePressed = false;
|
||||||
|
if (event.mouse->mousePressedOnWidget != this)
|
||||||
|
return;
|
||||||
|
if (!m_mouseDragged)
|
||||||
{
|
{
|
||||||
if (!getParent()->needsVScroll())
|
|
||||||
return;
|
|
||||||
auto bounds = getGlobalBounds() - m_correctionOffset;
|
auto bounds = getGlobalBounds() - m_correctionOffset;
|
||||||
auto ext = getParent()->getContentExtents();
|
f32 newThumbY = (event.mouse->position_y - bounds.y);
|
||||||
auto cont = getParent()->getContentBounds();
|
newThumbY -= m_thumbHeight / 2.0f;
|
||||||
thumby = std::clamp(thumby, 0.0f, h - m_thumbHeight);
|
set_thumb_y(newThumbY);
|
||||||
m_thumbY = thumby;
|
|
||||||
f32 scrollProgress = (bounds.y + gety() + m_thumbY - bounds.y) / (bounds.h - m_thumbHeight);
|
|
||||||
f32 maxScroll = -(ext.h - cont.h);
|
|
||||||
auto parentScrollOffset = getParent()->getScrollOffset();
|
|
||||||
parentScrollOffset.y = maxScroll * scrollProgress;
|
|
||||||
getParent()->setScrollOffset(parentScrollOffset);
|
|
||||||
}
|
}
|
||||||
|
m_mouseDragged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VerticalScrollBar::onUpdate(void)
|
||||||
|
{
|
||||||
|
update_thumb();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VerticalScrollBar::isMouseInsideThumb(const Vec2& mouse_pos)
|
||||||
|
{
|
||||||
|
return m_thumbGlobalBounds.contains(mouse_pos, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VerticalScrollBar::update_thumb(void)
|
||||||
|
{
|
||||||
|
x = getParent()->getSize().x - w;
|
||||||
|
y = 0;
|
||||||
|
h = getParent()->getGlobalPureContentBounds().h;
|
||||||
|
|
||||||
|
auto ext = getParent()->getContentExtents();
|
||||||
|
auto cont = getParent()->getContentBounds();
|
||||||
|
auto scrollOfset = getParent()->getScrollOffset();
|
||||||
|
|
||||||
|
// Thumb size is proportional to visible/total ratio
|
||||||
|
f32 visibleRatio = cont.h / ext.h; // e.g. 0.4 means 40% visible
|
||||||
|
m_thumbHeight = std::max(20.0f, geth() * visibleRatio); // min 20px for usability
|
||||||
|
|
||||||
|
// Thumb position is proportional to scroll progress
|
||||||
|
f32 scrollProgress = -scrollOfset.y / (ext.h - cont.h); // 0 to 1
|
||||||
|
m_thumbY = gety() + ((geth() - m_thumbHeight) * scrollProgress);
|
||||||
|
|
||||||
|
auto bounds = getGlobalBounds() - m_correctionOffset;
|
||||||
|
m_thumbGlobalBounds = { bounds.x + 2, bounds.y + gety() + m_thumbY, getw() - 4, m_thumbHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
void VerticalScrollBar::set_thumb_y(f32 thumby)
|
||||||
|
{
|
||||||
|
if (!getParent()->needsVScroll())
|
||||||
|
return;
|
||||||
|
auto bounds = getGlobalBounds() - m_correctionOffset;
|
||||||
|
auto ext = getParent()->getContentExtents();
|
||||||
|
auto cont = getParent()->getContentBounds();
|
||||||
|
thumby = std::clamp(thumby, 0.0f, h - m_thumbHeight);
|
||||||
|
m_thumbY = thumby;
|
||||||
|
f32 scrollProgress = (bounds.y + gety() + m_thumbY - bounds.y) / (bounds.h - m_thumbHeight);
|
||||||
|
f32 maxScroll = -(ext.h - cont.h);
|
||||||
|
auto parentScrollOffset = getParent()->getScrollOffset();
|
||||||
|
parentScrollOffset.y = maxScroll * scrollProgress;
|
||||||
|
getParent()->setScrollOffset(parentScrollOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
HorizontalScrollbar& HorizontalScrollbar::create(void)
|
HorizontalScrollbar& HorizontalScrollbar::create(void)
|
||||||
|
{
|
||||||
|
setPadding({ 0, 0, 0, 0 });
|
||||||
|
setMargin({ 0, 0, 0, 0 });
|
||||||
|
setTypeName("ogfx::gui::Label");
|
||||||
|
disableChildren();
|
||||||
|
enableBackground(true);
|
||||||
|
enableBorder(false);
|
||||||
|
enableTopMost(true);
|
||||||
|
enableIgnoreScroll(true);
|
||||||
|
setStylesheetCategoryName("scrollbar");
|
||||||
|
validate();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontalScrollbar::applyTheme(const ostd::Stylesheet& theme)
|
||||||
|
{
|
||||||
|
h = getThemeValue<f32>(theme, "width", 15);
|
||||||
|
m_thumbColor = getThemeValue<Color>(theme, "thumb.color", { 120, 120, 120 });
|
||||||
|
m_thumbBorderRadius = getThemeValue<f32>(theme, "thumb.borderRadius", 16);
|
||||||
|
m_thumbBorderColor = getThemeValue<Color>(theme, "thumb.borderColor", { 150, 150, 150 });
|
||||||
|
m_thumbShowBorder = getThemeValue<bool>(theme, "thumb.showBorder", true);
|
||||||
|
m_trackColor = getThemeValue<Color>(theme, "track.color", { 70, 70, 70 });
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontalScrollbar::afterDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
if (!getParent()->needsHScroll())
|
||||||
|
return;
|
||||||
|
gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii);
|
||||||
|
gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontalScrollbar::onMouseDragged(const Event& event)
|
||||||
|
{
|
||||||
|
if (!m_mousePressed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (isMouseInsideThumb({ event.mouse->position_x, event.mouse->position_y }))
|
||||||
|
m_mouseDragged = true;
|
||||||
|
|
||||||
|
if (m_mouseDragged)
|
||||||
{
|
{
|
||||||
setPadding({ 0, 0, 0, 0 });
|
|
||||||
setMargin({ 0, 0, 0, 0 });
|
|
||||||
setTypeName("ogfx::gui::widgets::Label");
|
|
||||||
disableChildren();
|
|
||||||
enableBackground(true);
|
|
||||||
enableBorder(false);
|
|
||||||
enableTopMost(true);
|
|
||||||
enableIgnoreScroll(true);
|
|
||||||
setStylesheetCategoryName("scrollbar");
|
|
||||||
validate();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalScrollbar::applyTheme(const ostd::Stylesheet& theme)
|
|
||||||
{
|
|
||||||
h = getThemeValue<f32>(theme, "width", 15);
|
|
||||||
m_thumbColor = getThemeValue<Color>(theme, "thumb.color", { 120, 120, 120 });
|
|
||||||
m_thumbBorderRadius = getThemeValue<f32>(theme, "thumb.borderRadius", 16);
|
|
||||||
m_thumbBorderColor = getThemeValue<Color>(theme, "thumb.borderColor", { 150, 150, 150 });
|
|
||||||
m_thumbShowBorder = getThemeValue<bool>(theme, "thumb.showBorder", true);
|
|
||||||
m_trackColor = getThemeValue<Color>(theme, "track.color", { 70, 70, 70 });
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalScrollbar::afterDraw(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
if (!getParent()->needsHScroll())
|
|
||||||
return;
|
|
||||||
gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii);
|
|
||||||
gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalScrollbar::onMouseDragged(const Event& event)
|
|
||||||
{
|
|
||||||
if (!m_mousePressed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (isMouseInsideThumb({ event.mouse->position_x, event.mouse->position_y }))
|
|
||||||
m_mouseDragged = true;
|
|
||||||
|
|
||||||
if (m_mouseDragged)
|
|
||||||
{
|
|
||||||
auto bounds = getGlobalBounds() - m_correctionOffset;
|
|
||||||
f32 newThumbX = (event.mouse->position_x - bounds.x) - m_dragGrabOffset;
|
|
||||||
set_thumb_x(newThumbX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalScrollbar::onMousePressed(const Event& event)
|
|
||||||
{
|
|
||||||
m_mousePressed = true;
|
|
||||||
m_dragGrabOffset = event.mouse->position_x - m_thumbGlobalBounds.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalScrollbar::onMouseReleased(const Event& event)
|
|
||||||
{
|
|
||||||
m_mousePressed = false;
|
|
||||||
if (event.mouse->mousePressedOnWidget != this)
|
|
||||||
return;
|
|
||||||
if (!m_mouseDragged)
|
|
||||||
{
|
|
||||||
auto bounds = getGlobalBounds() - m_correctionOffset;
|
|
||||||
f32 newThumbX = (event.mouse->position_x - bounds.x);
|
|
||||||
newThumbX -= m_thumbWidth / 2.0f;
|
|
||||||
set_thumb_x(newThumbX);
|
|
||||||
}
|
|
||||||
m_mouseDragged = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalScrollbar::onUpdate(void)
|
|
||||||
{
|
|
||||||
update_thumb();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HorizontalScrollbar::isMouseInsideThumb(const Vec2& mouse_pos)
|
|
||||||
{
|
|
||||||
return m_thumbGlobalBounds.contains(mouse_pos, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalScrollbar::update_thumb(void)
|
|
||||||
{
|
|
||||||
x = 0;
|
|
||||||
y = getParent()->getSize().y - h;
|
|
||||||
w = getParent()->getGlobalBounds().w - getParent()->getVScrollbarSize();
|
|
||||||
|
|
||||||
auto ext = getParent()->getContentExtents();
|
|
||||||
auto cont = getParent()->getContentBounds();
|
|
||||||
auto scrollOfset = getParent()->getScrollOffset();
|
|
||||||
|
|
||||||
// Thumb size is proportional to visible/total ratio
|
|
||||||
f32 visibleRatio = cont.w / ext.w; // e.g. 0.4 means 40% visible
|
|
||||||
m_thumbWidth = std::max(20.0f, getw() * visibleRatio); // min 20px for usability
|
|
||||||
|
|
||||||
// Thumb position is proportional to scroll progress
|
|
||||||
f32 scrollProgress = -scrollOfset.x / (ext.w - cont.w); // 0 to 1
|
|
||||||
m_thumbX = getx() + ((getw() - m_thumbWidth) * scrollProgress);
|
|
||||||
|
|
||||||
auto bounds = getGlobalBounds() - m_correctionOffset;
|
auto bounds = getGlobalBounds() - m_correctionOffset;
|
||||||
m_thumbGlobalBounds = { bounds.x + 2 + m_thumbX, bounds.y + gety(), m_thumbWidth, geth() - 4 };
|
f32 newThumbX = (event.mouse->position_x - bounds.x) - m_dragGrabOffset;
|
||||||
m_thumbGlobalBounds = { bounds.x + getx() + m_thumbX, bounds.y + 2, m_thumbWidth, geth() - 4 };
|
set_thumb_x(newThumbX);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void HorizontalScrollbar::set_thumb_x(f32 thumbx)
|
void HorizontalScrollbar::onMousePressed(const Event& event)
|
||||||
|
{
|
||||||
|
m_mousePressed = true;
|
||||||
|
m_dragGrabOffset = event.mouse->position_x - m_thumbGlobalBounds.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontalScrollbar::onMouseReleased(const Event& event)
|
||||||
|
{
|
||||||
|
m_mousePressed = false;
|
||||||
|
if (event.mouse->mousePressedOnWidget != this)
|
||||||
|
return;
|
||||||
|
if (!m_mouseDragged)
|
||||||
{
|
{
|
||||||
if (!getParent()->needsHScroll())
|
|
||||||
return;
|
|
||||||
auto bounds = getGlobalBounds() - m_correctionOffset;
|
auto bounds = getGlobalBounds() - m_correctionOffset;
|
||||||
auto ext = getParent()->getContentExtents();
|
f32 newThumbX = (event.mouse->position_x - bounds.x);
|
||||||
auto cont = getParent()->getContentBounds();
|
newThumbX -= m_thumbWidth / 2.0f;
|
||||||
thumbx = std::clamp(thumbx, 0.0f, w - m_thumbWidth);
|
set_thumb_x(newThumbX);
|
||||||
m_thumbX = thumbx;
|
|
||||||
f32 scrollProgress = (bounds.x + getx() + m_thumbX - bounds.x) / (bounds.w - m_thumbWidth);
|
|
||||||
f32 maxScroll = -(ext.w - cont.w);
|
|
||||||
auto parentScrollOffset = getParent()->getScrollOffset();
|
|
||||||
parentScrollOffset.x = maxScroll * scrollProgress;
|
|
||||||
getParent()->setScrollOffset(parentScrollOffset);
|
|
||||||
}
|
}
|
||||||
|
m_mouseDragged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontalScrollbar::onUpdate(void)
|
||||||
|
{
|
||||||
|
update_thumb();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HorizontalScrollbar::isMouseInsideThumb(const Vec2& mouse_pos)
|
||||||
|
{
|
||||||
|
return m_thumbGlobalBounds.contains(mouse_pos, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontalScrollbar::update_thumb(void)
|
||||||
|
{
|
||||||
|
x = 0;
|
||||||
|
y = getParent()->getSize().y - h;
|
||||||
|
w = getParent()->getGlobalBounds().w - getParent()->getVScrollbarSize();
|
||||||
|
|
||||||
|
auto ext = getParent()->getContentExtents();
|
||||||
|
auto cont = getParent()->getContentBounds();
|
||||||
|
auto scrollOfset = getParent()->getScrollOffset();
|
||||||
|
|
||||||
|
// Thumb size is proportional to visible/total ratio
|
||||||
|
f32 visibleRatio = cont.w / ext.w; // e.g. 0.4 means 40% visible
|
||||||
|
m_thumbWidth = std::max(20.0f, getw() * visibleRatio); // min 20px for usability
|
||||||
|
|
||||||
|
// Thumb position is proportional to scroll progress
|
||||||
|
f32 scrollProgress = -scrollOfset.x / (ext.w - cont.w); // 0 to 1
|
||||||
|
m_thumbX = getx() + ((getw() - m_thumbWidth) * scrollProgress);
|
||||||
|
|
||||||
|
auto bounds = getGlobalBounds() - m_correctionOffset;
|
||||||
|
m_thumbGlobalBounds = { bounds.x + 2 + m_thumbX, bounds.y + gety(), m_thumbWidth, geth() - 4 };
|
||||||
|
m_thumbGlobalBounds = { bounds.x + getx() + m_thumbX, bounds.y + 2, m_thumbWidth, geth() - 4 };
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontalScrollbar::set_thumb_x(f32 thumbx)
|
||||||
|
{
|
||||||
|
if (!getParent()->needsHScroll())
|
||||||
|
return;
|
||||||
|
auto bounds = getGlobalBounds() - m_correctionOffset;
|
||||||
|
auto ext = getParent()->getContentExtents();
|
||||||
|
auto cont = getParent()->getContentBounds();
|
||||||
|
thumbx = std::clamp(thumbx, 0.0f, w - m_thumbWidth);
|
||||||
|
m_thumbX = thumbx;
|
||||||
|
f32 scrollProgress = (bounds.x + getx() + m_thumbX - bounds.x) / (bounds.w - m_thumbWidth);
|
||||||
|
f32 maxScroll = -(ext.w - cont.w);
|
||||||
|
auto parentScrollOffset = getParent()->getScrollOffset();
|
||||||
|
parentScrollOffset.x = maxScroll * scrollProgress;
|
||||||
|
getParent()->setScrollOffset(parentScrollOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ScrollableWidget& ScrollableWidget::create(void)
|
ScrollableWidget& ScrollableWidget::create(void)
|
||||||
{
|
{
|
||||||
setTypeName("ogfx::gui::widgets::ScrollableWidget");
|
setTypeName("ogfx::gui::ScrollableWidget");
|
||||||
enableVScroll(true);
|
enableVScroll(true);
|
||||||
enableHScroll(true);
|
enableHScroll(true);
|
||||||
m_vScrollbar.enableManualDraw(true);
|
m_vScrollbar.enableManualDraw(true);
|
||||||
addWidget(m_vScrollbar);
|
addWidget(m_vScrollbar);
|
||||||
m_hScrollbar.setMargin({ 0, 0, 0, 0 });
|
m_hScrollbar.setMargin({ 0, 0, 0, 0 });
|
||||||
m_hScrollbar.enableManualDraw(true);
|
m_hScrollbar.enableManualDraw(true);
|
||||||
addWidget(m_hScrollbar);
|
addWidget(m_hScrollbar);
|
||||||
m_smoothScrollTimer.create(60.0f, [&](f64 dt) -> void {
|
m_smoothScrollTimer.create(60.0f, [&](f64 dt) -> void {
|
||||||
f32 stepX = m_scrollVelocity.x * (1.0f - m_scrollSmoothFactor);
|
f32 stepX = m_scrollVelocity.x * (1.0f - m_scrollSmoothFactor);
|
||||||
f32 stepY = m_scrollVelocity.y * (1.0f - m_scrollSmoothFactor);
|
f32 stepY = m_scrollVelocity.y * (1.0f - m_scrollSmoothFactor);
|
||||||
|
|
||||||
addScrollOffset({ stepX, stepY });
|
addScrollOffset({ stepX, stepY });
|
||||||
m_scrollVelocity -= { stepX, stepY };
|
m_scrollVelocity -= { stepX, stepY };
|
||||||
|
|
||||||
if (std::abs(m_scrollVelocity.x) < 0.5f) m_scrollVelocity.x = 0;
|
if (std::abs(m_scrollVelocity.x) < 0.5f) m_scrollVelocity.x = 0;
|
||||||
if (std::abs(m_scrollVelocity.y) < 0.5f) m_scrollVelocity.y = 0;
|
if (std::abs(m_scrollVelocity.y) < 0.5f) m_scrollVelocity.y = 0;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
m_smoothScrollTimer.setStopCondition([&](void) -> bool {
|
m_smoothScrollTimer.setStopCondition([&](void) -> bool {
|
||||||
return m_scrollVelocity.x == 0 && m_scrollVelocity.y == 0;
|
return m_scrollVelocity.x == 0 && m_scrollVelocity.y == 0;
|
||||||
});
|
});
|
||||||
updateScrollbarsSize();
|
updateScrollbarsSize();
|
||||||
validate();
|
validate();
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScrollableWidget::onUpdate(void)
|
void ScrollableWidget::onUpdate(void)
|
||||||
{
|
{
|
||||||
m_smoothScrollTimer.update();
|
m_smoothScrollTimer.update();
|
||||||
if (!needsVScroll() && m_vScrollbarAdded)
|
if (!needsVScroll() && m_vScrollbarAdded)
|
||||||
{
|
|
||||||
removeWidget(m_vScrollbar);
|
|
||||||
m_vScrollbarAdded = false;
|
|
||||||
}
|
|
||||||
else if (!m_vScrollbarAdded)
|
|
||||||
{
|
|
||||||
addWidget(m_vScrollbar);
|
|
||||||
m_vScrollbarAdded = true;
|
|
||||||
}
|
|
||||||
if (!needsHScroll() && m_hScrollbarAdded)
|
|
||||||
{
|
|
||||||
removeWidget(m_hScrollbar);
|
|
||||||
m_hScrollbarAdded = false;
|
|
||||||
}
|
|
||||||
else if (!m_hScrollbarAdded)
|
|
||||||
{
|
|
||||||
addWidget(m_hScrollbar);
|
|
||||||
m_hScrollbarAdded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrollableWidget::drawScrollbars(ogfx::BasicRenderer2D& gfx)
|
|
||||||
{
|
|
||||||
m_vScrollbar.__draw(gfx);
|
|
||||||
m_hScrollbar.__draw(gfx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrollableWidget::updateScrollbarsSize(void)
|
|
||||||
{
|
|
||||||
m_vScrollbar.setMargin({ 0, getPureContentBounds().y, 0, 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrollableWidget::resetScroll(bool horizontal, bool vertical, bool propagate)
|
|
||||||
{
|
|
||||||
if (vertical)
|
|
||||||
m_scrollOffset.y = 0;
|
|
||||||
if (horizontal)
|
|
||||||
m_scrollOffset.x = 0;
|
|
||||||
if (propagate)
|
|
||||||
{
|
|
||||||
for (auto& w : getChildren().getWidgets())
|
|
||||||
{
|
|
||||||
if (w == nullptr) continue;
|
|
||||||
w->resetScroll(horizontal, vertical, propagate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScrollableWidget::isMouseInsideVScrollbar(void) const
|
|
||||||
{
|
|
||||||
return m_vScrollbar.isMouseInside();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScrollableWidget::isMouseInsideHScrollbar(void) const
|
|
||||||
{
|
|
||||||
return m_hScrollbar.isMouseInside();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrollableWidget::onMouseScrolled(const Event& event)
|
|
||||||
{
|
|
||||||
if (isVScrollEnabled())
|
|
||||||
{
|
|
||||||
bool mouseInsideHScrollbar = m_hScrollbar.isMouseInside() && needsHScroll();
|
|
||||||
if (std::abs(event.mouse->scrollAmount.y) > 0 && !mouseInsideHScrollbar)
|
|
||||||
{
|
|
||||||
m_scrollVelocity.y += m_scrollSpeed * event.mouse->scrollAmount.y * m_scrollSpeedMultiplier;
|
|
||||||
if (m_smoothScrollTimer.isStopped())
|
|
||||||
m_smoothScrollTimer.restart();
|
|
||||||
event.handle();
|
|
||||||
}
|
|
||||||
else if (std::abs(event.mouse->scrollAmount.y) > 0)
|
|
||||||
{
|
|
||||||
m_scrollVelocity.x += m_scrollSpeed * event.mouse->scrollAmount.y * m_scrollSpeedMultiplier;
|
|
||||||
if (m_smoothScrollTimer.isStopped())
|
|
||||||
m_smoothScrollTimer.restart();
|
|
||||||
event.handle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isHScrollEnabled())
|
|
||||||
{
|
|
||||||
if (std::abs(event.mouse->scrollAmount.x) > 0)
|
|
||||||
{
|
|
||||||
m_scrollVelocity.x += m_scrollSpeed * event.mouse->scrollAmount.x * m_scrollSpeedMultiplier;
|
|
||||||
if (m_smoothScrollTimer.isStopped())
|
|
||||||
m_smoothScrollTimer.restart();
|
|
||||||
event.handle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrollableWidget::setScrollOffset(const Vec2& offset)
|
|
||||||
{
|
|
||||||
auto ext = getContentExtents();
|
|
||||||
auto cont = getContentBounds();
|
|
||||||
f32 maxScrollY = -(ext.h - cont.h);
|
|
||||||
f32 maxScrollX = -(ext.w - cont.w);
|
|
||||||
|
|
||||||
m_scrollOffset = offset;
|
|
||||||
|
|
||||||
if (m_scrollOffset.y < maxScrollY) m_scrollOffset.y = maxScrollY;
|
|
||||||
if (m_scrollOffset.y > 0) m_scrollOffset.y = 0;
|
|
||||||
if (m_scrollOffset.x < maxScrollX) m_scrollOffset.x = maxScrollX;
|
|
||||||
if (m_scrollOffset.x > 0) m_scrollOffset.x = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrollableWidget::addScrollOffset(const Vec2& offset)
|
|
||||||
{
|
|
||||||
auto ext = getContentExtents();
|
|
||||||
auto cont = getContentBounds();
|
|
||||||
f32 maxScrollY = -(ext.h - cont.h);
|
|
||||||
f32 maxScrollX = -(ext.w - cont.w);
|
|
||||||
|
|
||||||
m_scrollOffset += offset;
|
|
||||||
|
|
||||||
if (m_scrollOffset.y < maxScrollY) m_scrollOffset.y = maxScrollY;
|
|
||||||
if (m_scrollOffset.y > 0) m_scrollOffset.y = 0;
|
|
||||||
if (m_scrollOffset.x < maxScrollX) m_scrollOffset.x = maxScrollX;
|
|
||||||
if (m_scrollOffset.x > 0) m_scrollOffset.x = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScrollableWidget::needsVScroll(void) const
|
|
||||||
{
|
|
||||||
return isVScrollEnabled() && getContentExtents().h > getContentBounds().h;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScrollableWidget::needsHScroll(void) const
|
|
||||||
{
|
|
||||||
return isHScrollEnabled() && getContentExtents().w > getContentBounds().w;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrollableWidget::onWidgetAdded(Widget& child)
|
|
||||||
{
|
{
|
||||||
removeWidget(m_vScrollbar);
|
removeWidget(m_vScrollbar);
|
||||||
|
m_vScrollbarAdded = false;
|
||||||
|
}
|
||||||
|
else if (!m_vScrollbarAdded)
|
||||||
|
{
|
||||||
|
addWidget(m_vScrollbar);
|
||||||
|
m_vScrollbarAdded = true;
|
||||||
|
}
|
||||||
|
if (!needsHScroll() && m_hScrollbarAdded)
|
||||||
|
{
|
||||||
removeWidget(m_hScrollbar);
|
removeWidget(m_hScrollbar);
|
||||||
addWidget(m_vScrollbar, { 0, 0 }, true);
|
m_hScrollbarAdded = false;
|
||||||
addWidget(m_hScrollbar, { 0, 0 }, true);
|
|
||||||
}
|
}
|
||||||
|
else if (!m_hScrollbarAdded)
|
||||||
f32 ScrollableWidget::getVScrollbarSize(void) const
|
|
||||||
{
|
{
|
||||||
if (!isVScrollEnabled())
|
addWidget(m_hScrollbar);
|
||||||
return 0;
|
m_hScrollbarAdded = true;
|
||||||
if (!needsVScroll())
|
|
||||||
return 0;
|
|
||||||
return m_vScrollbar.getw();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
f32 ScrollableWidget::getHScrollbarSize(void) const
|
void ScrollableWidget::drawScrollbars(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
m_vScrollbar.__draw(gfx);
|
||||||
|
m_hScrollbar.__draw(gfx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollableWidget::updateScrollbarsSize(void)
|
||||||
|
{
|
||||||
|
m_vScrollbar.setMargin({ 0, getPureContentBounds().y, 0, 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollableWidget::resetScroll(bool horizontal, bool vertical, bool propagate)
|
||||||
|
{
|
||||||
|
if (vertical)
|
||||||
|
m_scrollOffset.y = 0;
|
||||||
|
if (horizontal)
|
||||||
|
m_scrollOffset.x = 0;
|
||||||
|
if (propagate)
|
||||||
{
|
{
|
||||||
if (!isHScrollEnabled())
|
for (auto& w : getChildren().getWidgets())
|
||||||
return 0;
|
{
|
||||||
if (!needsHScroll())
|
if (w == nullptr) continue;
|
||||||
return 0;
|
w->resetScroll(horizontal, vertical, propagate);
|
||||||
return m_hScrollbar.geth();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ScrollableWidget::isMouseInsideVScrollbar(void) const
|
||||||
|
{
|
||||||
|
return m_vScrollbar.isMouseInside();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScrollableWidget::isMouseInsideHScrollbar(void) const
|
||||||
|
{
|
||||||
|
return m_hScrollbar.isMouseInside();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollableWidget::onMouseScrolled(const Event& event)
|
||||||
|
{
|
||||||
|
if (isVScrollEnabled())
|
||||||
|
{
|
||||||
|
bool mouseInsideHScrollbar = m_hScrollbar.isMouseInside() && needsHScroll();
|
||||||
|
if (std::abs(event.mouse->scrollAmount.y) > 0 && !mouseInsideHScrollbar)
|
||||||
|
{
|
||||||
|
m_scrollVelocity.y += m_scrollSpeed * event.mouse->scrollAmount.y * m_scrollSpeedMultiplier;
|
||||||
|
if (m_smoothScrollTimer.isStopped())
|
||||||
|
m_smoothScrollTimer.restart();
|
||||||
|
event.handle();
|
||||||
|
}
|
||||||
|
else if (std::abs(event.mouse->scrollAmount.y) > 0)
|
||||||
|
{
|
||||||
|
m_scrollVelocity.x += m_scrollSpeed * event.mouse->scrollAmount.y * m_scrollSpeedMultiplier;
|
||||||
|
if (m_smoothScrollTimer.isStopped())
|
||||||
|
m_smoothScrollTimer.restart();
|
||||||
|
event.handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isHScrollEnabled())
|
||||||
|
{
|
||||||
|
if (std::abs(event.mouse->scrollAmount.x) > 0)
|
||||||
|
{
|
||||||
|
m_scrollVelocity.x += m_scrollSpeed * event.mouse->scrollAmount.x * m_scrollSpeedMultiplier;
|
||||||
|
if (m_smoothScrollTimer.isStopped())
|
||||||
|
m_smoothScrollTimer.restart();
|
||||||
|
event.handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollableWidget::setScrollOffset(const Vec2& offset)
|
||||||
|
{
|
||||||
|
auto ext = getContentExtents();
|
||||||
|
auto cont = getContentBounds();
|
||||||
|
f32 maxScrollY = -(ext.h - cont.h);
|
||||||
|
f32 maxScrollX = -(ext.w - cont.w);
|
||||||
|
|
||||||
|
m_scrollOffset = offset;
|
||||||
|
|
||||||
|
if (m_scrollOffset.y < maxScrollY) m_scrollOffset.y = maxScrollY;
|
||||||
|
if (m_scrollOffset.y > 0) m_scrollOffset.y = 0;
|
||||||
|
if (m_scrollOffset.x < maxScrollX) m_scrollOffset.x = maxScrollX;
|
||||||
|
if (m_scrollOffset.x > 0) m_scrollOffset.x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollableWidget::addScrollOffset(const Vec2& offset)
|
||||||
|
{
|
||||||
|
auto ext = getContentExtents();
|
||||||
|
auto cont = getContentBounds();
|
||||||
|
f32 maxScrollY = -(ext.h - cont.h);
|
||||||
|
f32 maxScrollX = -(ext.w - cont.w);
|
||||||
|
|
||||||
|
m_scrollOffset += offset;
|
||||||
|
|
||||||
|
if (m_scrollOffset.y < maxScrollY) m_scrollOffset.y = maxScrollY;
|
||||||
|
if (m_scrollOffset.y > 0) m_scrollOffset.y = 0;
|
||||||
|
if (m_scrollOffset.x < maxScrollX) m_scrollOffset.x = maxScrollX;
|
||||||
|
if (m_scrollOffset.x > 0) m_scrollOffset.x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScrollableWidget::needsVScroll(void) const
|
||||||
|
{
|
||||||
|
return isVScrollEnabled() && getContentExtents().h > getContentBounds().h;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScrollableWidget::needsHScroll(void) const
|
||||||
|
{
|
||||||
|
return isHScrollEnabled() && getContentExtents().w > getContentBounds().w;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScrollableWidget::onWidgetAdded(Widget& child)
|
||||||
|
{
|
||||||
|
removeWidget(m_vScrollbar);
|
||||||
|
removeWidget(m_hScrollbar);
|
||||||
|
addWidget(m_vScrollbar, { 0, 0 }, true);
|
||||||
|
addWidget(m_hScrollbar, { 0, 0 }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
f32 ScrollableWidget::getVScrollbarSize(void) const
|
||||||
|
{
|
||||||
|
if (!isVScrollEnabled())
|
||||||
|
return 0;
|
||||||
|
if (!needsVScroll())
|
||||||
|
return 0;
|
||||||
|
return m_vScrollbar.getw();
|
||||||
|
}
|
||||||
|
|
||||||
|
f32 ScrollableWidget::getHScrollbarSize(void) const
|
||||||
|
{
|
||||||
|
if (!isHScrollEnabled())
|
||||||
|
return 0;
|
||||||
|
if (!needsHScroll())
|
||||||
|
return 0;
|
||||||
|
return m_hScrollbar.geth();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,123 +27,120 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
class VerticalScrollBar : public Widget
|
||||||
{
|
{
|
||||||
class VerticalScrollBar : public Widget
|
public:
|
||||||
{
|
inline VerticalScrollBar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
|
||||||
public:
|
VerticalScrollBar& create(void);
|
||||||
inline VerticalScrollBar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
VerticalScrollBar& create(void);
|
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
void onMouseDragged(const Event& event) override;
|
||||||
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
|
void onMousePressed(const Event& event) override;
|
||||||
void onMouseDragged(const Event& event) override;
|
void onMouseReleased(const Event& event) override;
|
||||||
void onMousePressed(const Event& event) override;
|
void onUpdate(void) override;
|
||||||
void onMouseReleased(const Event& event) override;
|
bool isMouseInsideThumb(const Vec2& mouse_pos);
|
||||||
void onUpdate(void) override;
|
|
||||||
bool isMouseInsideThumb(const Vec2& mouse_pos);
|
|
||||||
|
|
||||||
inline void setx(f32 xx) override { }
|
inline void setx(f32 xx) override { }
|
||||||
inline void sety(f32 yy) override { }
|
inline void sety(f32 yy) override { }
|
||||||
inline void setw(f32 ww) override { }
|
inline void setw(f32 ww) override { }
|
||||||
inline void seth(f32 hh) override { }
|
inline void seth(f32 hh) override { }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void update_thumb(void);
|
void update_thumb(void);
|
||||||
void set_thumb_y(f32 thumby);
|
void set_thumb_y(f32 thumby);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
f32 m_thumbHeight { 0 };
|
f32 m_thumbHeight { 0 };
|
||||||
f32 m_thumbY { 0 };
|
f32 m_thumbY { 0 };
|
||||||
Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 };
|
Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 };
|
||||||
bool m_mousePressed { false };
|
bool m_mousePressed { false };
|
||||||
bool m_mouseDragged { false };
|
bool m_mouseDragged { false };
|
||||||
f32 m_dragGrabOffset { 0 };
|
f32 m_dragGrabOffset { 0 };
|
||||||
Rectangle m_correctionOffset { 1, 1, 0, 0 };
|
Rectangle m_correctionOffset { 1, 1, 0, 0 };
|
||||||
|
|
||||||
Rectangle m_trackBorderRadii { 0, 0, 0, 0 };
|
Rectangle m_trackBorderRadii { 0, 0, 0, 0 };
|
||||||
f32 m_thumbBorderRadius { 16 };
|
f32 m_thumbBorderRadius { 16 };
|
||||||
Color m_trackColor { 70, 70, 70 };
|
Color m_trackColor { 70, 70, 70 };
|
||||||
Color m_thumbColor { 120, 120, 120 };
|
Color m_thumbColor { 120, 120, 120 };
|
||||||
Color m_thumbBorderColor { 150, 150, 150 };
|
Color m_thumbBorderColor { 150, 150, 150 };
|
||||||
bool m_thumbShowBorder { true };
|
bool m_thumbShowBorder { true };
|
||||||
};
|
};
|
||||||
class HorizontalScrollbar : public Widget
|
class HorizontalScrollbar : public Widget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
inline HorizontalScrollbar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
|
inline HorizontalScrollbar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
|
||||||
HorizontalScrollbar& create(void);
|
HorizontalScrollbar& create(void);
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
|
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
void onMouseDragged(const Event& event) override;
|
void onMouseDragged(const Event& event) override;
|
||||||
void onMousePressed(const Event& event) override;
|
void onMousePressed(const Event& event) override;
|
||||||
void onMouseReleased(const Event& event) override;
|
void onMouseReleased(const Event& event) override;
|
||||||
void onUpdate(void) override;
|
void onUpdate(void) override;
|
||||||
bool isMouseInsideThumb(const Vec2& mouse_pos);
|
bool isMouseInsideThumb(const Vec2& mouse_pos);
|
||||||
|
|
||||||
inline void setx(f32 xx) override { }
|
inline void setx(f32 xx) override { }
|
||||||
inline void sety(f32 yy) override { }
|
inline void sety(f32 yy) override { }
|
||||||
inline void setw(f32 ww) override { }
|
inline void setw(f32 ww) override { }
|
||||||
inline void seth(f32 hh) override { }
|
inline void seth(f32 hh) override { }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void update_thumb(void);
|
void update_thumb(void);
|
||||||
void set_thumb_x(f32 thumbx);
|
void set_thumb_x(f32 thumbx);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
f32 m_thumbWidth { 0 };
|
f32 m_thumbWidth { 0 };
|
||||||
f32 m_thumbX { 0 };
|
f32 m_thumbX { 0 };
|
||||||
Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 };
|
Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 };
|
||||||
bool m_mousePressed { false };
|
bool m_mousePressed { false };
|
||||||
bool m_mouseDragged { false };
|
bool m_mouseDragged { false };
|
||||||
f32 m_dragGrabOffset { 0 };
|
f32 m_dragGrabOffset { 0 };
|
||||||
Rectangle m_correctionOffset { 0, 0, 0, 0 };
|
Rectangle m_correctionOffset { 0, 0, 0, 0 };
|
||||||
|
|
||||||
Rectangle m_trackBorderRadii { 0, 0, 0, 0 };
|
Rectangle m_trackBorderRadii { 0, 0, 0, 0 };
|
||||||
f32 m_thumbBorderRadius { 16 };
|
f32 m_thumbBorderRadius { 16 };
|
||||||
Color m_trackColor { 70, 70, 70 };
|
Color m_trackColor { 70, 70, 70 };
|
||||||
Color m_thumbColor { 120, 120, 120 };
|
Color m_thumbColor { 120, 120, 120 };
|
||||||
Color m_thumbBorderColor { 150, 150, 150 };
|
Color m_thumbBorderColor { 150, 150, 150 };
|
||||||
bool m_thumbShowBorder { true };
|
bool m_thumbShowBorder { true };
|
||||||
};
|
};
|
||||||
class ScrollableWidget : public Widget
|
class ScrollableWidget : public Widget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
inline ScrollableWidget(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
|
inline ScrollableWidget(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
|
||||||
ScrollableWidget& create(void);
|
ScrollableWidget& create(void);
|
||||||
virtual void onUpdate(void) override;
|
virtual void onUpdate(void) override;
|
||||||
void drawScrollbars(ogfx::BasicRenderer2D& gfx);
|
void drawScrollbars(ogfx::BasicRenderer2D& gfx);
|
||||||
void updateScrollbarsSize(void);
|
void updateScrollbarsSize(void);
|
||||||
void resetScroll(bool horizontal = true, bool vertical = true, bool propagate = true) override;
|
void resetScroll(bool horizontal = true, bool vertical = true, bool propagate = true) override;
|
||||||
bool isMouseInsideVScrollbar(void) const;
|
bool isMouseInsideVScrollbar(void) const;
|
||||||
bool isMouseInsideHScrollbar(void) const;
|
bool isMouseInsideHScrollbar(void) const;
|
||||||
inline bool isMouseInsideAnyScrollbar(void) const { return isMouseInsideVScrollbar() || isMouseInsideHScrollbar(); }
|
inline bool isMouseInsideAnyScrollbar(void) const { return isMouseInsideVScrollbar() || isMouseInsideHScrollbar(); }
|
||||||
virtual void onMouseScrolled(const Event& event) override;
|
virtual void onMouseScrolled(const Event& event) override;
|
||||||
virtual void setScrollOffset(const Vec2& offset) override;
|
virtual void setScrollOffset(const Vec2& offset) override;
|
||||||
virtual void addScrollOffset(const Vec2& offset) override;
|
virtual void addScrollOffset(const Vec2& offset) override;
|
||||||
virtual bool needsVScroll(void) const override;
|
virtual bool needsVScroll(void) const override;
|
||||||
virtual bool needsHScroll(void) const override;
|
virtual bool needsHScroll(void) const override;
|
||||||
virtual void onWidgetAdded(Widget& child) override;
|
virtual void onWidgetAdded(Widget& child) override;
|
||||||
virtual f32 getVScrollbarSize(void) const override;
|
virtual f32 getVScrollbarSize(void) const override;
|
||||||
virtual f32 getHScrollbarSize(void) const override;
|
virtual f32 getHScrollbarSize(void) const override;
|
||||||
inline Vec2 getScrollOffset(void) const override { return m_scrollOffset; }
|
inline Vec2 getScrollOffset(void) const override { return m_scrollOffset; }
|
||||||
inline void setScrollSpeed(f32 speed) { m_scrollSpeed = speed; }
|
inline void setScrollSpeed(f32 speed) { m_scrollSpeed = speed; }
|
||||||
inline f32 getScrollSpeed(void) const { return m_scrollSpeed; }
|
inline f32 getScrollSpeed(void) const { return m_scrollSpeed; }
|
||||||
inline void setScrollSmoothFactor(f32 speed) { m_scrollSmoothFactor = std::clamp(speed, 0.0f, 1.0f); }
|
inline void setScrollSmoothFactor(f32 speed) { m_scrollSmoothFactor = std::clamp(speed, 0.0f, 1.0f); }
|
||||||
inline f32 getScrollSmoothFactor(void) const { return m_scrollSmoothFactor; }
|
inline f32 getScrollSmoothFactor(void) const { return m_scrollSmoothFactor; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
String m_title { "Panel" };
|
String m_title { "Panel" };
|
||||||
Vec2 m_scrollOffset { 0, 0 };
|
Vec2 m_scrollOffset { 0, 0 };
|
||||||
VerticalScrollBar m_vScrollbar { getWindow() };
|
VerticalScrollBar m_vScrollbar { getWindow() };
|
||||||
HorizontalScrollbar m_hScrollbar { getWindow() };
|
HorizontalScrollbar m_hScrollbar { getWindow() };
|
||||||
ostd::StepTimer m_smoothScrollTimer;
|
ostd::StepTimer m_smoothScrollTimer;
|
||||||
f32 m_scrollSpeed { 0.8f };
|
f32 m_scrollSpeed { 0.8f };
|
||||||
Vec2 m_scrollVelocity { 0.0f, 0.0f };
|
Vec2 m_scrollVelocity { 0.0f, 0.0f };
|
||||||
f32 m_scrollSmoothFactor { 0.7f };
|
f32 m_scrollSmoothFactor { 0.7f };
|
||||||
f32 m_scrollSpeedMultiplier { 15.0f };
|
f32 m_scrollSpeedMultiplier { 15.0f };
|
||||||
bool m_hScrollbarAdded { false };
|
bool m_hScrollbarAdded { false };
|
||||||
bool m_vScrollbarAdded { false };
|
bool m_vScrollbarAdded { false };
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,169 +25,166 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
Slider& Slider::create(bool vertical, f32 min, f32 max, f32 step)
|
||||||
{
|
{
|
||||||
Slider& Slider::create(bool vertical, f32 min, f32 max, f32 step)
|
setPadding({ 0, 0, 0, 0 });
|
||||||
{
|
setTypeName("ogfx::gui::Slider");
|
||||||
setPadding({ 0, 0, 0, 0 });
|
disableChildren();
|
||||||
setTypeName("ogfx::gui::widgets::Slider");
|
disableBackground();
|
||||||
disableChildren();
|
disableBorder();
|
||||||
disableBackground();
|
setContentOffset({ 0, 0 });
|
||||||
disableBorder();
|
setStylesheetCategoryName("slider");
|
||||||
setContentOffset({ 0, 0 });
|
setMinValue(min);
|
||||||
setStylesheetCategoryName("slider");
|
setMaxValue(max);
|
||||||
setMinValue(min);
|
setStep(step);
|
||||||
setMaxValue(max);
|
validate();
|
||||||
setStep(step);
|
return *this;
|
||||||
validate();
|
}
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Slider::applyTheme(const ostd::Stylesheet& theme)
|
void Slider::applyTheme(const ostd::Stylesheet& theme)
|
||||||
{
|
{
|
||||||
setHandleColor(getThemeValue<Color>(theme, "handle.color", getHandleColor()));
|
setHandleColor(getThemeValue<Color>(theme, "handle.color", getHandleColor()));
|
||||||
setHandleSize(getThemeValue<Vec2>(theme, "handle.size", getHandleSize()));
|
setHandleSize(getThemeValue<Vec2>(theme, "handle.size", getHandleSize()));
|
||||||
setTrackColor(getThemeValue<Color>(theme, "track.color", getTrackColor()));
|
setTrackColor(getThemeValue<Color>(theme, "track.color", getTrackColor()));
|
||||||
setTrackProgressColor(getThemeValue<Color>(theme, "track.progressColor", getTrackProgressColor()));
|
setTrackProgressColor(getThemeValue<Color>(theme, "track.progressColor", getTrackProgressColor()));
|
||||||
setTrackWidth(getThemeValue<f32>(theme, "track.width", getTrackWidth()));
|
setTrackWidth(getThemeValue<f32>(theme, "track.width", getTrackWidth()));
|
||||||
enableDrawTicks(getThemeValue<bool>(theme, "showStepTicks", isDrawTicksEnabled()));
|
enableDrawTicks(getThemeValue<bool>(theme, "showStepTicks", isDrawTicksEnabled()));
|
||||||
setStepTickHeight(getThemeValue<f32>(theme, "stepTickHeight", getStepTickHeight()));
|
setStepTickHeight(getThemeValue<f32>(theme, "stepTickHeight", getStepTickHeight()));
|
||||||
setStepTickColor(getThemeValue<Color>(theme, "stepTickColor", getStepTickColor()));
|
setStepTickColor(getThemeValue<Color>(theme, "stepTickColor", getStepTickColor()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Slider::onMouseReleased(const Event& event)
|
void Slider::onMouseReleased(const Event& event)
|
||||||
|
{
|
||||||
|
if (!isMouseInside())
|
||||||
|
return;
|
||||||
|
if (isMiddleClickShortcutEnabled() && event.mouse->button == MouseEventData::eButton::Middle)
|
||||||
{
|
{
|
||||||
if (!isMouseInside())
|
set_value(snap_to_step((m_min + m_max) * 0.5f));
|
||||||
return;
|
return;
|
||||||
if (isMiddleClickShortcutEnabled() && event.mouse->button == MouseEventData::eButton::Middle)
|
}
|
||||||
{
|
set_value(mouse_position_to_value({ event.mouse->position_x, event.mouse->position_y }));
|
||||||
set_value(snap_to_step((m_min + m_max) * 0.5f));
|
}
|
||||||
return;
|
|
||||||
}
|
void Slider::onMouseDragged(const Event& event)
|
||||||
|
{
|
||||||
|
if (!isMouseInside())
|
||||||
|
return;
|
||||||
|
if (event.mouse->button == MouseEventData::eButton::Left)
|
||||||
set_value(mouse_position_to_value({ event.mouse->position_x, event.mouse->position_y }));
|
set_value(mouse_position_to_value({ event.mouse->position_x, event.mouse->position_y }));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Slider::onMouseDragged(const Event& event)
|
void Slider::onMouseScrolled(const Event& event)
|
||||||
|
{
|
||||||
|
if (!isMouseInside())
|
||||||
|
return;
|
||||||
|
if (!isScrollWheelShortcutEnabled())
|
||||||
|
return;
|
||||||
|
if (getStep() > 0)
|
||||||
{
|
{
|
||||||
if (!isMouseInside())
|
if (event.mouse->scroll == MouseEventData::eScrollDirection::Down ||
|
||||||
return;
|
event.mouse->scroll == MouseEventData::eScrollDirection::Right)
|
||||||
if (event.mouse->button == MouseEventData::eButton::Left)
|
set_value(getValue() + (getStep() * -1));
|
||||||
set_value(mouse_position_to_value({ event.mouse->position_x, event.mouse->position_y }));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Slider::onMouseScrolled(const Event& event)
|
|
||||||
{
|
|
||||||
if (!isMouseInside())
|
|
||||||
return;
|
|
||||||
if (!isScrollWheelShortcutEnabled())
|
|
||||||
return;
|
|
||||||
if (getStep() > 0)
|
|
||||||
{
|
|
||||||
if (event.mouse->scroll == MouseEventData::eScrollDirection::Down ||
|
|
||||||
event.mouse->scroll == MouseEventData::eScrollDirection::Right)
|
|
||||||
set_value(getValue() + (getStep() * -1));
|
|
||||||
else
|
|
||||||
set_value(getValue() + getStep());
|
|
||||||
}
|
|
||||||
else if (event.mouse->scroll == MouseEventData::eScrollDirection::Down ||
|
|
||||||
event.mouse->scroll == MouseEventData::eScrollDirection::Up)
|
|
||||||
set_value(getValue() + (0.1f * event.mouse->scrollAmount.y));
|
|
||||||
else
|
else
|
||||||
set_value(getValue() + (0.1f * event.mouse->scrollAmount.x));
|
set_value(getValue() + getStep());
|
||||||
event.handle();
|
|
||||||
}
|
}
|
||||||
|
else if (event.mouse->scroll == MouseEventData::eScrollDirection::Down ||
|
||||||
|
event.mouse->scroll == MouseEventData::eScrollDirection::Up)
|
||||||
|
set_value(getValue() + (0.1f * event.mouse->scrollAmount.y));
|
||||||
|
else
|
||||||
|
set_value(getValue() + (0.1f * event.mouse->scrollAmount.x));
|
||||||
|
event.handle();
|
||||||
|
}
|
||||||
|
|
||||||
void Slider::onDraw(ogfx::BasicRenderer2D& gfx)
|
void Slider::onDraw(ogfx::BasicRenderer2D& gfx)
|
||||||
|
{
|
||||||
|
const auto& bounds = getGlobalContentBounds();
|
||||||
|
f32 value = m_value;
|
||||||
|
f32 t = (value - m_min) / (m_max - m_min); // normalized [0, 1]
|
||||||
|
|
||||||
|
auto l_centerh = [&](f32 value) -> f32 {
|
||||||
|
return (geth() / 2.0f) - (value / 2.0f);
|
||||||
|
};
|
||||||
|
auto l_centerw = [&](f32 value) -> f32 {
|
||||||
|
return (getw() / 2.0f) - (value / 2.0f);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHorizontal())
|
||||||
{
|
{
|
||||||
const auto& bounds = getGlobalContentBounds();
|
if (m_step > 0.0f && m_drawTicks)
|
||||||
f32 value = m_value;
|
|
||||||
f32 t = (value - m_min) / (m_max - m_min); // normalized [0, 1]
|
|
||||||
|
|
||||||
auto l_centerh = [&](f32 value) -> f32 {
|
|
||||||
return (geth() / 2.0f) - (value / 2.0f);
|
|
||||||
};
|
|
||||||
auto l_centerw = [&](f32 value) -> f32 {
|
|
||||||
return (getw() / 2.0f) - (value / 2.0f);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isHorizontal())
|
|
||||||
{
|
{
|
||||||
if (m_step > 0.0f && m_drawTicks)
|
int tickCount = (int)std::round((m_max - m_min) / m_step) + 1;
|
||||||
|
for (int i = 0; i < tickCount; i++)
|
||||||
{
|
{
|
||||||
int tickCount = (int)std::round((m_max - m_min) / m_step) + 1;
|
f32 v = m_min + i * m_step;
|
||||||
for (int i = 0; i < tickCount; i++)
|
v = std::min(v, m_max);
|
||||||
{
|
f32 tickT = (v - m_min) / (m_max - m_min);
|
||||||
f32 v = m_min + i * m_step;
|
f32 tickX = bounds.x + bounds.w * tickT;
|
||||||
v = std::min(v, m_max);
|
gfx.fillRect({ tickX - 1, bounds.y + l_centerh(getStepTickHeight()) - getStepTickHeight(), 2, getStepTickHeight() }, getStepTickColor());
|
||||||
f32 tickT = (v - m_min) / (m_max - m_min);
|
|
||||||
f32 tickX = bounds.x + bounds.w * tickT;
|
|
||||||
gfx.fillRect({ tickX - 1, bounds.y + l_centerh(getStepTickHeight()) - getStepTickHeight(), 2, getStepTickHeight() }, getStepTickColor());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Vec2 correction { -2, 0 };
|
|
||||||
Vec2 trackPos = bounds.getPosition() + Vec2 { 0, l_centerh(getTrackWidth()) };
|
|
||||||
gfx.fillRect({ trackPos + correction, bounds.w + (correction.x * -2), getTrackWidth() }, getTrackColor());
|
|
||||||
gfx.fillRect({ trackPos + correction, (bounds.w + (correction.x * -2)) * t, getTrackWidth() }, getTrackProgressColor());
|
|
||||||
f32 handleX = bounds.x + bounds.w * t - (getHandleSize().x / 2.0f);
|
|
||||||
gfx.fillRect({ handleX, bounds.y + l_centerh(getHandleSize().y), getHandleSize() }, getHandleColor());
|
|
||||||
}
|
}
|
||||||
else
|
Vec2 correction { -2, 0 };
|
||||||
|
Vec2 trackPos = bounds.getPosition() + Vec2 { 0, l_centerh(getTrackWidth()) };
|
||||||
|
gfx.fillRect({ trackPos + correction, bounds.w + (correction.x * -2), getTrackWidth() }, getTrackColor());
|
||||||
|
gfx.fillRect({ trackPos + correction, (bounds.w + (correction.x * -2)) * t, getTrackWidth() }, getTrackProgressColor());
|
||||||
|
f32 handleX = bounds.x + bounds.w * t - (getHandleSize().x / 2.0f);
|
||||||
|
gfx.fillRect({ handleX, bounds.y + l_centerh(getHandleSize().y), getHandleSize() }, getHandleColor());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (m_step > 0.0f && m_drawTicks)
|
||||||
{
|
{
|
||||||
if (m_step > 0.0f && m_drawTicks)
|
int tickCount = (int)std::round((m_max - m_min) / m_step) + 1;
|
||||||
|
for (int i = 0; i < tickCount; i++)
|
||||||
{
|
{
|
||||||
int tickCount = (int)std::round((m_max - m_min) / m_step) + 1;
|
f32 v = m_min + i * m_step;
|
||||||
for (int i = 0; i < tickCount; i++)
|
v = std::min(v, m_max);
|
||||||
{
|
f32 tickT = (v - m_min) / (m_max - m_min);
|
||||||
f32 v = m_min + i * m_step;
|
f32 tickY = bounds.y + bounds.h * (1.0f - tickT);
|
||||||
v = std::min(v, m_max);
|
gfx.fillRect({ bounds.x + l_centerw(getStepTickHeight()) - getStepTickHeight(), tickY - 1, getStepTickHeight(), 2 }, getStepTickColor());
|
||||||
f32 tickT = (v - m_min) / (m_max - m_min);
|
|
||||||
f32 tickY = bounds.y + bounds.h * (1.0f - tickT);
|
|
||||||
gfx.fillRect({ bounds.x + l_centerw(getStepTickHeight()) - getStepTickHeight(), tickY - 1, getStepTickHeight(), 2 }, getStepTickColor());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Vec2 correction { 0, -2 };
|
|
||||||
Vec2 trackPos = bounds.getPosition() + Vec2 { l_centerw(getTrackWidth()), 0 };
|
|
||||||
gfx.fillRect({ trackPos + correction, getTrackWidth(), bounds.h + (correction.y * -2) }, getTrackColor());
|
|
||||||
f32 progressH = (bounds.h + (correction.y * -2)) * t;
|
|
||||||
gfx.fillRect({ trackPos.x + correction.x, trackPos.y + correction.y + (bounds.h + (correction.y * -2)) - progressH, getTrackWidth(), progressH }, getTrackProgressColor());
|
|
||||||
Vec2 swappedSize = getHandleSize().swap();
|
|
||||||
f32 handleY = bounds.y + bounds.h * (1.0f - t) - (swappedSize.y / 2.0f);
|
|
||||||
gfx.fillRect({ bounds.x + l_centerw(swappedSize.x), handleY, swappedSize }, getHandleColor());
|
|
||||||
}
|
}
|
||||||
|
Vec2 correction { 0, -2 };
|
||||||
|
Vec2 trackPos = bounds.getPosition() + Vec2 { l_centerw(getTrackWidth()), 0 };
|
||||||
|
gfx.fillRect({ trackPos + correction, getTrackWidth(), bounds.h + (correction.y * -2) }, getTrackColor());
|
||||||
|
f32 progressH = (bounds.h + (correction.y * -2)) * t;
|
||||||
|
gfx.fillRect({ trackPos.x + correction.x, trackPos.y + correction.y + (bounds.h + (correction.y * -2)) - progressH, getTrackWidth(), progressH }, getTrackProgressColor());
|
||||||
|
Vec2 swappedSize = getHandleSize().swap();
|
||||||
|
f32 handleY = bounds.y + bounds.h * (1.0f - t) - (swappedSize.y / 2.0f);
|
||||||
|
gfx.fillRect({ bounds.x + l_centerw(swappedSize.x), handleY, swappedSize }, getHandleColor());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Slider::enableVertical(bool enable)
|
void Slider::enableVertical(bool enable)
|
||||||
{
|
{
|
||||||
if (isVertical() && enable) return;
|
if (isVertical() && enable) return;
|
||||||
if (isHorizontal() && !enable) return;
|
if (isHorizontal() && !enable) return;
|
||||||
m_vertical = enable;
|
m_vertical = enable;
|
||||||
setSize(getSize().swap());
|
setSize(getSize().swap());
|
||||||
}
|
}
|
||||||
|
|
||||||
f32 Slider::snap_to_step(f32 val)
|
f32 Slider::snap_to_step(f32 val)
|
||||||
{
|
{
|
||||||
if (m_step <= 0.0f) return std::clamp(val, m_min, m_max); // full precision
|
if (m_step <= 0.0f) return std::clamp(val, m_min, m_max); // full precision
|
||||||
f32 snapped = std::round((val - m_min) / m_step) * m_step + m_min;
|
f32 snapped = std::round((val - m_min) / m_step) * m_step + m_min;
|
||||||
return std::clamp(snapped, m_min, m_max);
|
return std::clamp(snapped, m_min, m_max);
|
||||||
}
|
}
|
||||||
|
|
||||||
f32 Slider::mouse_position_to_value(Vec2 mousePosition)
|
f32 Slider::mouse_position_to_value(Vec2 mousePosition)
|
||||||
{
|
{
|
||||||
const auto& bounds = getGlobalBounds();
|
const auto& bounds = getGlobalBounds();
|
||||||
f32 raw = isHorizontal() ? (mousePosition.x - bounds.x) / bounds.w : 1.0f - (mousePosition.y - bounds.y) / bounds.h;
|
f32 raw = isHorizontal() ? (mousePosition.x - bounds.x) / bounds.w : 1.0f - (mousePosition.y - bounds.y) / bounds.h;
|
||||||
raw = std::clamp(raw, 0.0f, 1.0f);
|
raw = std::clamp(raw, 0.0f, 1.0f);
|
||||||
return m_min + raw * (m_max - m_min);
|
return m_min + raw * (m_max - m_min);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Slider::set_value(f32 val)
|
void Slider::set_value(f32 val)
|
||||||
{
|
{
|
||||||
if (m_value == val)
|
if (m_value == val)
|
||||||
return;
|
return;
|
||||||
f32 old = m_value;
|
f32 old = m_value;
|
||||||
m_value = snap_to_step(val);
|
m_value = snap_to_step(val);
|
||||||
if (callback_onValueChanged)
|
if (callback_onValueChanged)
|
||||||
callback_onValueChanged(old, m_value);
|
callback_onValueChanged(old, m_value);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,70 +26,67 @@ namespace ogfx
|
||||||
{
|
{
|
||||||
namespace gui
|
namespace gui
|
||||||
{
|
{
|
||||||
namespace widgets
|
class Slider : public Widget
|
||||||
{
|
{
|
||||||
class Slider : public Widget
|
public:
|
||||||
{
|
inline Slider(WindowCore& window, bool vertical = false, f32 min = 0.0f, f32 max = 1.0f, f32 step = 0.1f) : Widget({ 0, 0, 0, 0 }, window) { create(vertical, min, max, step); }
|
||||||
public:
|
Slider& create(bool vertical, f32 min, f32 max, f32 step);
|
||||||
inline Slider(WindowCore& window, bool vertical = false, f32 min = 0.0f, f32 max = 1.0f, f32 step = 0.1f) : Widget({ 0, 0, 0, 0 }, window) { create(vertical, min, max, step); }
|
void applyTheme(const ostd::Stylesheet& theme) override;
|
||||||
Slider& create(bool vertical, f32 min, f32 max, f32 step);
|
void onMouseReleased(const Event& event) override;
|
||||||
void applyTheme(const ostd::Stylesheet& theme) override;
|
void onMouseDragged(const Event& event) override;
|
||||||
void onMouseReleased(const Event& event) override;
|
void onMouseScrolled(const Event& event) override;
|
||||||
void onMouseDragged(const Event& event) override;
|
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
||||||
void onMouseScrolled(const Event& event) override;
|
inline f32 getValue(void) const { return m_value; }
|
||||||
void onDraw(ogfx::BasicRenderer2D& gfx) override;
|
inline void setValueChangedCallback(std::function<void(f32 oldValue, f32 newValue)> callback) { callback_onValueChanged = std::move(callback); }
|
||||||
inline f32 getValue(void) const { return m_value; }
|
|
||||||
inline void setValueChangedCallback(std::function<void(f32 oldValue, f32 newValue)> callback) { callback_onValueChanged = std::move(callback); }
|
|
||||||
|
|
||||||
void enableVertical(bool enable = true);
|
void enableVertical(bool enable = true);
|
||||||
inline bool isVertical(void) const { return m_vertical; }
|
inline bool isVertical(void) const { return m_vertical; }
|
||||||
inline void disableVertical(void) { enableVertical(false); }
|
inline void disableVertical(void) { enableVertical(false); }
|
||||||
inline bool isHorizontal(void) const { return !isVertical(); }
|
inline bool isHorizontal(void) const { return !isVertical(); }
|
||||||
inline void enableHorizontal(bool enable = true) { enableVertical(!enable); }
|
inline void enableHorizontal(bool enable = true) { enableVertical(!enable); }
|
||||||
inline void disableHorizontal(void) { enableHorizontal(false); }
|
inline void disableHorizontal(void) { enableHorizontal(false); }
|
||||||
|
|
||||||
OSTD_BOOL_PARAM_GETSET_E(MiddleClickShortcut, m_middleClickShortcut);
|
OSTD_BOOL_PARAM_GETSET_E(MiddleClickShortcut, m_middleClickShortcut);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(ScrollWheelShortcut, m_enableScrollWheel);
|
OSTD_BOOL_PARAM_GETSET_E(ScrollWheelShortcut, m_enableScrollWheel);
|
||||||
OSTD_PARAM_GETSET(f32, MinValue, m_min);
|
OSTD_PARAM_GETSET(f32, MinValue, m_min);
|
||||||
OSTD_PARAM_GETSET(f32, MaxValue, m_max);
|
OSTD_PARAM_GETSET(f32, MaxValue, m_max);
|
||||||
OSTD_PARAM_GETSET(f32, Step, m_step);
|
OSTD_PARAM_GETSET(f32, Step, m_step);
|
||||||
|
|
||||||
OSTD_PARAM_GETSET(f32, TrackWidth, m_trackWidth);
|
OSTD_PARAM_GETSET(f32, TrackWidth, m_trackWidth);
|
||||||
OSTD_PARAM_GETSET(Color, TrackColor, m_trackColor);
|
OSTD_PARAM_GETSET(Color, TrackColor, m_trackColor);
|
||||||
OSTD_PARAM_GETSET(Color, TrackProgressColor, m_trackProgressColor);
|
OSTD_PARAM_GETSET(Color, TrackProgressColor, m_trackProgressColor);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(DrawTicks, m_drawTicks);
|
OSTD_BOOL_PARAM_GETSET_E(DrawTicks, m_drawTicks);
|
||||||
OSTD_PARAM_GETSET(f32, StepTickHeight, m_tickHeight);
|
OSTD_PARAM_GETSET(f32, StepTickHeight, m_tickHeight);
|
||||||
OSTD_PARAM_GETSET(Color, StepTickColor, m_tickColor);
|
OSTD_PARAM_GETSET(Color, StepTickColor, m_tickColor);
|
||||||
OSTD_PARAM_GETSET(Color, HandleColor, m_handleColor);
|
OSTD_PARAM_GETSET(Color, HandleColor, m_handleColor);
|
||||||
OSTD_PARAM_GETSET(Vec2, HandleSize, m_handleSize);
|
OSTD_PARAM_GETSET(Vec2, HandleSize, m_handleSize);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
f32 snap_to_step(f32 val);
|
f32 snap_to_step(f32 val);
|
||||||
f32 mouse_position_to_value(Vec2 mousePosition);
|
f32 mouse_position_to_value(Vec2 mousePosition);
|
||||||
void set_value(f32 val);
|
void set_value(f32 val);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
f32 m_value { 0.0f };
|
f32 m_value { 0.0f };
|
||||||
std::function<void(f32 oldValue, f32 newValue)> callback_onValueChanged { nullptr };
|
std::function<void(f32 oldValue, f32 newValue)> callback_onValueChanged { nullptr };
|
||||||
|
|
||||||
f32 m_min { 0.0f };
|
f32 m_min { 0.0f };
|
||||||
f32 m_max { 1.0f };
|
f32 m_max { 1.0f };
|
||||||
f32 m_step { 0.1f };
|
f32 m_step { 0.1f };
|
||||||
bool m_vertical { false };
|
bool m_vertical { false };
|
||||||
bool m_middleClickShortcut { true };
|
bool m_middleClickShortcut { true };
|
||||||
bool m_enableScrollWheel { true };
|
bool m_enableScrollWheel { true };
|
||||||
|
|
||||||
f32 m_trackWidth { 6 };
|
f32 m_trackWidth { 6 };
|
||||||
Color m_trackColor { "#500000FF" };
|
Color m_trackColor { "#500000FF" };
|
||||||
Color m_trackProgressColor { "#AA0000FF" };
|
Color m_trackProgressColor { "#AA0000FF" };
|
||||||
|
|
||||||
bool m_drawTicks { true };
|
bool m_drawTicks { true };
|
||||||
f32 m_tickHeight { 4 };
|
f32 m_tickHeight { 4 };
|
||||||
Color m_tickColor { Colors::Crimson };
|
Color m_tickColor { Colors::Crimson };
|
||||||
|
|
||||||
Color m_handleColor { Colors::Crimson };
|
Color m_handleColor { Colors::Crimson };
|
||||||
Vec2 m_handleSize { 10, 18 };
|
Vec2 m_handleSize { 10, 18 };
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,17 @@ namespace ogfx
|
||||||
if (!result) return false;
|
if (!result) return false;
|
||||||
if (!__skip_callback)
|
if (!__skip_callback)
|
||||||
onWidgetAdded(child);
|
onWidgetAdded(child);
|
||||||
|
if (m_layout && !__skip_callback)
|
||||||
|
relayout();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Widget::removeWidget(Widget& child)
|
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
|
Vec2 Widget::getGlobalPosition(void) const
|
||||||
|
|
@ -105,6 +110,14 @@ namespace ogfx
|
||||||
|
|
||||||
Rectangle Widget::getContentExtents(void) const
|
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;
|
f32 maxX = 0, maxY = 0;
|
||||||
for (auto* child : m_widgets.getWidgets())
|
for (auto* child : m_widgets.getWidgets())
|
||||||
{
|
{
|
||||||
|
|
@ -216,6 +229,28 @@ namespace ogfx
|
||||||
m_visible = v;
|
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)
|
void Widget::setCallback(eCallback type, EventCallback callback)
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
|
|
@ -440,6 +475,8 @@ namespace ogfx
|
||||||
callback_onWindowResized(event);
|
callback_onWindowResized(event);
|
||||||
onWindowResized(event);
|
onWindowResized(event);
|
||||||
}
|
}
|
||||||
|
if (m_layout)
|
||||||
|
relayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Widget::__onWindowFocused(const Event& event)
|
void Widget::__onWindowFocused(const Event& event)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@
|
||||||
#include <ostd/io/Stylesheet.hpp>
|
#include <ostd/io/Stylesheet.hpp>
|
||||||
#include <ostd/utils/Defines.hpp>
|
#include <ostd/utils/Defines.hpp>
|
||||||
#include <ostd/utils/Time.hpp>
|
#include <ostd/utils/Time.hpp>
|
||||||
|
#include <ogfx/gui/LayoutHint.hpp>
|
||||||
|
#include <ogfx/gui/Layout.hpp>
|
||||||
|
#include <memory>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
namespace ogfx
|
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 ===============================
|
// =============================== DIMENSIONS ===============================
|
||||||
virtual Vec2 getGlobalPosition(void) const;
|
virtual Vec2 getGlobalPosition(void) const;
|
||||||
virtual Vec2 getGlobalContentPosition(void) const;
|
virtual Vec2 getGlobalContentPosition(void) const;
|
||||||
|
|
@ -126,6 +156,7 @@ namespace ogfx
|
||||||
inline static ostd::BaseObject* getDragAndDropData(void) { return s_dragAndDropData; }
|
inline static ostd::BaseObject* getDragAndDropData(void) { return s_dragAndDropData; }
|
||||||
inline WindowCore& getWindow(void) { return *m_window; }
|
inline WindowCore& getWindow(void) { return *m_window; }
|
||||||
inline Widget* getParent(void) { return m_parent; }
|
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 i32 getZIndex(void) const { return m_zIndex; }
|
||||||
inline ogfx::MouseEventData::eButton getPressedMouseButton(void) const { return m_pressedButton; }
|
inline ogfx::MouseEventData::eButton getPressedMouseButton(void) const { return m_pressedButton; }
|
||||||
inline const stdvec<String>& getThemeIDList(void) const { return m_themeIDList; }
|
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(TopMost, m_topMost);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(IgnoreScroll, m_ignoreScroll);
|
OSTD_BOOL_PARAM_GETSET_E(IgnoreScroll, m_ignoreScroll);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(VScroll, m_vScrollEnabled);
|
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(BackgroundGradient, m_useBackgroundGradient);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(Theming, m_enableTheming);
|
OSTD_BOOL_PARAM_GETSET_E(Theming, m_enableTheming);
|
||||||
OSTD_BOOL_PARAM_GETSET_E(Tooltip, m_enableTooltip);
|
OSTD_BOOL_PARAM_GETSET_E(Tooltip, m_enableTooltip);
|
||||||
|
|
@ -199,6 +230,8 @@ namespace ogfx
|
||||||
WindowCore* m_window { nullptr };
|
WindowCore* m_window { nullptr };
|
||||||
Widget* m_parent { nullptr };
|
Widget* m_parent { nullptr };
|
||||||
WidgetManager m_widgets;
|
WidgetManager m_widgets;
|
||||||
|
std::unique_ptr<Layout> m_layout;
|
||||||
|
LayoutHint m_layoutHint;
|
||||||
|
|
||||||
bool m_rootChild { false };
|
bool m_rootChild { false };
|
||||||
bool m_focused { false };
|
bool m_focused { false };
|
||||||
|
|
@ -340,6 +373,7 @@ namespace ogfx
|
||||||
inline virtual void onWindowResized(const Event& event) { }
|
inline virtual void onWindowResized(const Event& event) { }
|
||||||
inline virtual void onWindowFocused(const Event& event) { }
|
inline virtual void onWindowFocused(const Event& event) { }
|
||||||
inline virtual void onWindowFocusLost(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)
|
if (m_vertexCount + 4 >= MaxVertices || m_indexCount + 6 >= MaxIndices)
|
||||||
flushBatch();
|
flushBatch();
|
||||||
if (m_texture != nullptr)
|
|
||||||
flushBatch();
|
|
||||||
m_texture = nullptr;
|
|
||||||
|
|
||||||
SDL_FColor c0 = COLOR_CAST(colors[i]);
|
SDL_FColor c0 = COLOR_CAST(colors[i]);
|
||||||
SDL_FColor c1 = COLOR_CAST(colors[i + 1]);
|
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 gety(void) const { return y; }
|
||||||
inline virtual f32 getw(void) const { return w; }
|
inline virtual f32 getw(void) const { return w; }
|
||||||
inline virtual f32 geth(void) const { return h; }
|
inline virtual f32 geth(void) const { return h; }
|
||||||
inline virtual f32 getCenterX(void) const { return getx() + getw() / 2; }
|
inline f32 getCenterX(void) const { return getx() + getw() / 2; }
|
||||||
inline virtual f32 getCenterY(void) const { return gety() + geth() / 2; }
|
inline f32 getCenterY(void) const { return gety() + geth() / 2; }
|
||||||
|
|
||||||
inline virtual void setx(f32 xx) { x = xx; }
|
inline virtual void setx(f32 xx) { x = xx; }
|
||||||
inline virtual void sety(f32 yy) { y = yy; }
|
inline virtual void sety(f32 yy) { y = yy; }
|
||||||
inline virtual void setw(f32 ww) { w = ww; }
|
inline virtual void setw(f32 ww) { w = ww; }
|
||||||
inline virtual void seth(f32 hh) { h = hh; }
|
inline virtual void seth(f32 hh) { h = hh; }
|
||||||
|
|
||||||
inline virtual Vec2 getPosition(void) const { return Vec2(getx(), gety()); }
|
inline Vec2 getPosition(void) const { return Vec2(getx(), gety()); }
|
||||||
inline virtual Vec2 getSize(void) const { return Vec2(getw(), geth()); }
|
inline Vec2 getSize(void) const { return Vec2(getw(), geth()); }
|
||||||
inline virtual Vec2 getCenter(void) const { return Vec2(getx() + getw() / 2, gety() + geth() / 2); }
|
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 void setPosition(const Vec2& pos) { setx(pos.x); sety(pos.y); }
|
||||||
inline virtual void setPosition(f32 xx, f32 yy) { setx(xx); sety(yy); }
|
inline void setPosition(f32 xx, f32 yy) { setx(xx); sety(yy); }
|
||||||
inline virtual void setSize(Vec2 size) { setw(size.x); seth(size.y); }
|
inline void setSize(Vec2 size) { setw(size.x); seth(size.y); }
|
||||||
inline virtual void setSize(f32 ww, f32 hh) { setw(ww); seth(hh); }
|
inline 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 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 f32 addx(f32 xx) { setx(getx() + xx); return getx(); }
|
||||||
inline virtual f32 addy(f32 yy) { sety(gety() + yy); return gety(); }
|
inline f32 addy(f32 yy) { sety(gety() + yy); return gety(); }
|
||||||
inline virtual Vec2 addPos(f32 xx, f32 yy) { return Vec2(addx(xx), addy(yy)); }
|
inline 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 Vec2 addPos(Vec2 pos) { return addPos(pos.x, pos.y); }
|
||||||
inline virtual f32 addw(f32 ww) { setw(getw() + ww); return getw(); }
|
inline f32 addw(f32 ww) { setw(getw() + ww); return getw(); }
|
||||||
inline virtual f32 addh(f32 hh) { seth(geth() + hh); return geth(); }
|
inline f32 addh(f32 hh) { seth(geth() + hh); return geth(); }
|
||||||
inline virtual Vec2 addSize(f32 ww, f32 hh) { addw(ww), addh(hh); return getSize(); }
|
inline 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 Vec2 addSize(Vec2 size) { addSize(size.x, size.y); return getSize(); }
|
||||||
|
|
||||||
inline virtual f32 subx(f32 xx) { setx(getx() - xx); return getx(); }
|
inline f32 subx(f32 xx) { setx(getx() - xx); return getx(); }
|
||||||
inline virtual f32 suby(f32 yy) { sety(gety() - yy); return gety(); }
|
inline f32 suby(f32 yy) { sety(gety() - yy); return gety(); }
|
||||||
inline virtual Vec2 subPos(f32 xx, f32 yy) { return Vec2(subx(xx), suby(yy)); }
|
inline 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 Vec2 subPos(Vec2 pos) { return subPos(pos.x, pos.y); }
|
||||||
inline virtual f32 subw(f32 ww) { setw(getw() - ww); return getw(); }
|
inline f32 subw(f32 ww) { setw(getw() - ww); return getw(); }
|
||||||
inline virtual f32 subh(f32 hh) { seth(geth() - hh); return geth(); }
|
inline f32 subh(f32 hh) { seth(geth() - hh); return geth(); }
|
||||||
inline virtual Vec2 subSize(f32 ww, f32 hh) { return Vec2(subw(ww), subh(hh)); }
|
inline 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 Vec2 subSize(Vec2 size) { return subPos(size.x, size.y); }
|
||||||
|
|
||||||
inline virtual f32 mulx(f32 xx) { setx(getx() * xx); return getx(); }
|
inline f32 mulx(f32 xx) { setx(getx() * xx); return getx(); }
|
||||||
inline virtual f32 muly(f32 yy) { sety(gety() * yy); return gety(); }
|
inline f32 muly(f32 yy) { sety(gety() * yy); return gety(); }
|
||||||
inline virtual Vec2 mulPos(f32 xx, f32 yy) { return Vec2(mulx(xx), muly(yy)); }
|
inline 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 Vec2 mulPos(Vec2 pos) { return mulPos(pos.x, pos.y); }
|
||||||
inline virtual f32 mulw(f32 ww) { setw(getw() * ww); return getw(); }
|
inline f32 mulw(f32 ww) { setw(getw() * ww); return getw(); }
|
||||||
inline virtual f32 mulh(f32 hh) { seth(geth() * hh); return geth(); }
|
inline f32 mulh(f32 hh) { seth(geth() * hh); return geth(); }
|
||||||
inline virtual Vec2 mulSize(f32 ww, f32 hh) { return Vec2(mulw(ww), mulh(hh)); }
|
inline 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 Vec2 mulSize(Vec2 size) { return mulPos(size.x, size.y); }
|
||||||
|
|
||||||
inline virtual f32 divx(f32 xx) { setx(getx() / xx); return getx(); }
|
inline f32 divx(f32 xx) { setx(getx() / xx); return getx(); }
|
||||||
inline virtual f32 divy(f32 yy) { sety(gety() / yy); return gety(); }
|
inline f32 divy(f32 yy) { sety(gety() / yy); return gety(); }
|
||||||
inline virtual Vec2 divPos(f32 xx, f32 yy) { return Vec2(divx(xx), divy(yy)); }
|
inline 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 Vec2 divPos(Vec2 pos) { return divPos(pos.x, pos.y); }
|
||||||
inline virtual f32 divw(f32 ww) { setw(getw() / ww); return getw(); }
|
inline f32 divw(f32 ww) { setw(getw() / ww); return getw(); }
|
||||||
inline virtual f32 divh(f32 hh) { seth(geth() / hh); return geth(); }
|
inline f32 divh(f32 hh) { seth(geth() / hh); return geth(); }
|
||||||
inline virtual Vec2 divSize(f32 ww, f32 hh) { return Vec2(divw(ww), divh(hh)); }
|
inline 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 Vec2 divSize(Vec2 size) { return divPos(size.x, size.y); }
|
||||||
|
|
||||||
inline virtual Vec2 topLeft(void) const { return getPosition(); }
|
inline Vec2 topLeft(void) const { return getPosition(); }
|
||||||
inline virtual Vec2 topRight(void) const { return Vec2(getx() + getw(), gety()); }
|
inline Vec2 topRight(void) const { return Vec2(getx() + getw(), gety()); }
|
||||||
inline virtual Vec2 bottomLeft(void) const { return Vec2(getx(), gety() + geth()); }
|
inline Vec2 bottomLeft(void) const { return Vec2(getx(), gety() + geth()); }
|
||||||
inline virtual Vec2 bottomRight(void) const { return Vec2(getx() + getw(), gety() + geth()); }
|
inline Vec2 bottomRight(void) const { return Vec2(getx() + getw(), gety() + geth()); }
|
||||||
inline virtual Rectangle edgeRect(void) const { return { getx(), gety(), getx() + getw(), gety() + getw() }; }
|
inline Rectangle edgeRect(void) const { return { getx(), gety(), getx() + getw(), gety() + getw() }; }
|
||||||
|
|
||||||
inline virtual f32 left(void) const { return getx(); }
|
inline f32 left(void) const { return getx(); }
|
||||||
inline virtual f32 right(void) const { return getw(); }
|
inline f32 right(void) const { return getw(); }
|
||||||
inline virtual f32 top(void) const { return gety(); }
|
inline f32 top(void) const { return gety(); }
|
||||||
inline virtual f32 bottom(void) const { return geth(); }
|
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(" }"); }
|
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);
|
f32 bottomY = std::min(y + h, rect.y + rect.h);
|
||||||
return { leftX, topY, rightX - leftX, bottomY - topY };
|
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)
|
if (includeBounds)
|
||||||
return p.x >= x && p.y >= y && p.x <= x + w && p.y <= y + h;
|
return p.x >= x && p.y >= y && p.x <= x + w && p.y <= y + h;
|
||||||
else
|
else
|
||||||
return p.x > x && p.y > y && p.x < x + w && p.y < y + h;
|
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()))); }
|
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;
|
ostd::ConsoleOutputHandler out;
|
||||||
using namespace ogfx::gui::widgets;
|
using namespace ogfx::gui;
|
||||||
|
|
||||||
class Window : public ogfx::gui::Window
|
class TestWindow : public Window
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
inline Window(void) { }
|
inline TestWindow(void) { }
|
||||||
inline void onInitialize(void) override
|
inline void onInitialize(void) override
|
||||||
{
|
{
|
||||||
ogfx::AnimationData ad;
|
ogfx::AnimationData ad;
|
||||||
|
|
@ -55,6 +55,8 @@ class Window : public ogfx::gui::Window
|
||||||
setTheme(m_theme);
|
setTheme(m_theme);
|
||||||
|
|
||||||
getToolBar().setHeight(30);
|
getToolBar().setHeight(30);
|
||||||
|
getToolBar().setLayout<BoxLayout>(BoxLayout::Orientation::Horizontal);
|
||||||
|
getToolBar().getLayout()->setSpacing(10);
|
||||||
getStatusBar().setHeight(24);
|
getStatusBar().setHeight(24);
|
||||||
showMenuBar();
|
showMenuBar();
|
||||||
showToolBar();
|
showToolBar();
|
||||||
|
|
@ -72,7 +74,7 @@ class Window : public ogfx::gui::Window
|
||||||
iconsAD.still = true;
|
iconsAD.still = true;
|
||||||
|
|
||||||
m_label1.setText("Show Panel2");
|
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_check1.setChecked(!m_check1.isChecked());
|
||||||
});
|
});
|
||||||
m_label1.disableTheming();
|
m_label1.disableTheming();
|
||||||
|
|
@ -95,7 +97,7 @@ class Window : public ogfx::gui::Window
|
||||||
m_btn1.enableTooltip();
|
m_btn1.enableTooltip();
|
||||||
m_btn1.setTooltipText("Test tooltip");
|
m_btn1.setTooltipText("Test tooltip");
|
||||||
m_btn1.setText("TEST BUTTON");
|
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";
|
std::cout << showOpenFileDialog(filters) << "\n";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -148,7 +150,7 @@ class Window : public ogfx::gui::Window
|
||||||
m_list.addLine("Item 3333333");
|
m_list.addLine("Item 3333333");
|
||||||
m_list.getLine(10).setFontSize(40);
|
m_list.getLine(10).setFontSize(40);
|
||||||
m_list.getLine(160).setTextColor(Colors::Crimson);
|
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";
|
std::cout << *(selection[0]) << "\n";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -171,9 +173,6 @@ class Window : public ogfx::gui::Window
|
||||||
auto& t2 = m_tabs.addTab("Tab2 Test");
|
auto& t2 = m_tabs.addTab("Tab2 Test");
|
||||||
auto& t3 = m_tabs.addTab("Long Tab 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 });
|
t1.addWidget(m_check1, { 30, 30 });
|
||||||
m_radioGroup.addButton(t1, "Radio this out!", { 30, 80 });
|
m_radioGroup.addButton(t1, "Radio this out!", { 30, 80 });
|
||||||
m_radioGroup.addButton(t1, "Radio Opt. 2", { 30, 110 });
|
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_list, { 30, 300 });
|
||||||
t1.addWidget(m_panel2, { 500, 100 });
|
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_panel3.addWidget(m_label4);
|
||||||
|
|
||||||
m_panel1.addWidget(m_label2);
|
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";
|
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)
|
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();
|
.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);
|
auto& mmd = cast<ogfx::MouseEventData&>(signal.userData);
|
||||||
if (mmd.button == ogfx::MouseEventData::eButton::Right)
|
if (mmd.button == ogfx::MouseEventData::eButton::Right)
|
||||||
{
|
showContextMenu(m_menu, { mmd.position_x, mmd.position_y });
|
||||||
setContextMenu(m_menu);
|
|
||||||
showContextMenu({ mmd.position_x, mmd.position_y });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (signal.ID == ostd::BuiltinSignals::WindowResized)
|
else if (signal.ID == ostd::BuiltinSignals::WindowResized)
|
||||||
{
|
{
|
||||||
auto& wrd = cast<ogfx::WindowResizedData&>(signal.userData);
|
auto& wrd = cast<ogfx::WindowResizedData&>(signal.userData);
|
||||||
m_tabs.setSize(cast<f32>(getWindowWidth()), cast<f32>(getWindowHeight() - getMenuBar().geth() - getToolBar().geth() - getStatusBar().geth()));
|
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.setPosition(0, -1);
|
||||||
m_tabs.refreshCurrentTab();
|
m_tabs.refreshCurrentTab();
|
||||||
|
|
||||||
|
|
@ -312,7 +327,7 @@ class Window : public ogfx::gui::Window
|
||||||
|
|
||||||
enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };
|
enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };
|
||||||
|
|
||||||
ogfx::gui::ContextMenu::Instance m_menu { {
|
ContextMenu::Instance m_menu { {
|
||||||
{ "File", -1, {
|
{ "File", -1, {
|
||||||
{ "New", MenuId::New },
|
{ "New", MenuId::New },
|
||||||
{ "Open...", MenuId::Open },
|
{ "Open...", MenuId::Open },
|
||||||
|
|
@ -369,7 +384,7 @@ class Window : public ogfx::gui::Window
|
||||||
Help_About,
|
Help_About,
|
||||||
};
|
};
|
||||||
|
|
||||||
ogfx::gui::ContextMenu::Instance fileMenu { {
|
ContextMenu::Instance fileMenu { {
|
||||||
{ "New", TestMenuId::File_New },
|
{ "New", TestMenuId::File_New },
|
||||||
{ "Open...", TestMenuId::File_Open },
|
{ "Open...", TestMenuId::File_Open },
|
||||||
{ "Open Recent", {
|
{ "Open Recent", {
|
||||||
|
|
@ -390,7 +405,7 @@ class Window : public ogfx::gui::Window
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
ogfx::gui::ContextMenu::Instance editMenu { {
|
ContextMenu::Instance editMenu { {
|
||||||
{ "Undo", TestMenuId::Edit_Undo },
|
{ "Undo", TestMenuId::Edit_Undo },
|
||||||
{ "Redo", TestMenuId::Edit_Redo },
|
{ "Redo", TestMenuId::Edit_Redo },
|
||||||
{ "Cut", TestMenuId::Edit_Cut },
|
{ "Cut", TestMenuId::Edit_Cut },
|
||||||
|
|
@ -400,7 +415,7 @@ class Window : public ogfx::gui::Window
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
ogfx::gui::ContextMenu::Instance viewMenu { {
|
ContextMenu::Instance viewMenu { {
|
||||||
{ "Zoom In", TestMenuId::View_ZoomIn },
|
{ "Zoom In", TestMenuId::View_ZoomIn },
|
||||||
{ "Zoom Out", TestMenuId::View_ZoomOut },
|
{ "Zoom Out", TestMenuId::View_ZoomOut },
|
||||||
{ "Reset Zoom", TestMenuId::View_ZoomReset },
|
{ "Reset Zoom", TestMenuId::View_ZoomReset },
|
||||||
|
|
@ -412,7 +427,7 @@ class Window : public ogfx::gui::Window
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
ogfx::gui::ContextMenu::Instance helpMenu { {
|
ContextMenu::Instance helpMenu { {
|
||||||
{ "Documentation", TestMenuId::Help_Documentation },
|
{ "Documentation", TestMenuId::Help_Documentation },
|
||||||
{ "About", TestMenuId::Help_About } },
|
{ "About", TestMenuId::Help_About } },
|
||||||
nullptr
|
nullptr
|
||||||
|
|
@ -428,7 +443,7 @@ i32 main(i32 argc, char** argv)
|
||||||
{
|
{
|
||||||
ostd::Random::autoSeed();
|
ostd::Random::autoSeed();
|
||||||
ostd::initialize();
|
ostd::initialize();
|
||||||
Window window;
|
TestWindow window;
|
||||||
window.initialize(1200, 800, "OmniaFramework - Test Window");
|
window.initialize(1200, 800, "OmniaFramework - Test Window");
|
||||||
window.setClearColor({ 0, 0, 0 });
|
window.setClearColor({ 0, 0, 0 });
|
||||||
window.setPosition({ 50, 50 });
|
window.setPosition({ 50, 50 });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue