532 lines
15 KiB
C++
532 lines
15 KiB
C++
/*
|
|
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 "Widget.hpp"
|
|
#include "gui/Events.hpp"
|
|
#include "io/Memory.hpp"
|
|
#include "../..//render/BasicRenderer.hpp"
|
|
#include "../Window.hpp"
|
|
|
|
namespace ogfx
|
|
{
|
|
namespace gui
|
|
{
|
|
ostd::BaseObject* Widget::s_dragAndDropData { nullptr };
|
|
bool Widget::s_hasDragAndDropData { false };
|
|
|
|
Widget::Widget(const Rectangle& bounds, WindowCore& window) : Rectangle(bounds), m_widgets(window, *this)
|
|
{
|
|
m_window = &window;
|
|
}
|
|
|
|
bool Widget::addWidget(Widget& child, const Vec2& position, bool __skip_callback)
|
|
{
|
|
if (!m_allowChildren)
|
|
return false;
|
|
if (position.x != 0 || position.y != 0)
|
|
child.setPosition(position);
|
|
child.reloadTheme();
|
|
bool result = m_widgets.addWidget(child);
|
|
if (!result) return false;
|
|
if (!__skip_callback)
|
|
onWidgetAdded(child);
|
|
if (m_layout && !__skip_callback)
|
|
relayout();
|
|
return result;
|
|
}
|
|
|
|
bool Widget::removeWidget(Widget& child)
|
|
{
|
|
bool result = m_widgets.removeWidget(child);
|
|
if (result && m_layout)
|
|
relayout();
|
|
return result;
|
|
}
|
|
|
|
Vec2 Widget::getGlobalPosition(void) const
|
|
{
|
|
Vec2 glob = getPosition();
|
|
if (!m_rootChild && m_parent != nullptr)
|
|
{
|
|
glob += m_parent->getGlobalPosition();
|
|
if (!isIgnoreScrollEnabled())
|
|
{
|
|
glob += m_parent->getPadding().getPosition();
|
|
glob += m_parent->getContentOffset();
|
|
glob += m_parent->getScrollOffset();
|
|
}
|
|
}
|
|
glob += m_margin.getPosition();
|
|
return glob;
|
|
}
|
|
|
|
Vec2 Widget::getGlobalContentPosition(void) const
|
|
{
|
|
return getGlobalPosition() + getContentBounds().getPosition();
|
|
}
|
|
|
|
Rectangle Widget::getGlobalBounds(void) const
|
|
{
|
|
return { getGlobalPosition(), getSize() + (m_margin.getSize() * 2) };
|
|
}
|
|
|
|
Rectangle Widget::getContentBounds(void) const
|
|
{
|
|
auto pad = getPadding();
|
|
return { Vec2 { m_contentOffset.x, m_contentOffset.y } + pad.getPosition(), (getSize() - m_contentOffset - (pad.getSize() * 2)) };
|
|
}
|
|
|
|
Rectangle Widget::getPureContentBounds(void) const
|
|
{
|
|
return { { m_contentOffset.x, m_contentOffset.y }, (getSize() - m_contentOffset) };
|
|
}
|
|
|
|
Rectangle Widget::getGlobalContentBounds(void) const
|
|
{
|
|
return { getGlobalContentPosition(), getContentBounds().getSize() };
|
|
}
|
|
|
|
Rectangle Widget::getGlobalPureContentBounds(void) const
|
|
{
|
|
return { getGlobalPosition() + getContentOffset(), getPureContentBounds().getSize() };
|
|
}
|
|
|
|
Rectangle Widget::getContentExtents(void) const
|
|
{
|
|
// If a layout owns this widget, prefer its measure() — it knows
|
|
// the real desired extent (e.g. a vertical box with 50 items
|
|
// reports 50*itemHeight even if only 5 are visible).
|
|
if (m_layout)
|
|
{
|
|
Vec2 desired = m_layout->measure(*this);
|
|
return { 0, 0, desired.x, desired.y };
|
|
}
|
|
f32 maxX = 0, maxY = 0;
|
|
for (auto* child : m_widgets.getWidgets())
|
|
{
|
|
if (!child || child->isInvalid()) continue;
|
|
if (child->isIgnoreScrollEnabled()) continue;
|
|
if (!child->isVisible()) continue;
|
|
Vec2 localPos = getPadding().getPosition() + child->getPosition() + child->getMargin().getPosition() + child->getContentBounds().getPosition();
|
|
maxX = std::max(maxX, localPos.x + child->getw() + child->getMargin().w);
|
|
maxY = std::max(maxY, localPos.y + child->geth() + child->getMargin().h);
|
|
}
|
|
return { 0, 0, maxX, maxY };
|
|
}
|
|
|
|
bool Widget::contains(Vec2 p, bool includeBounds) const
|
|
{
|
|
return Rectangle(getGlobalPosition(), getSize()).contains(p, includeBounds);
|
|
}
|
|
|
|
void Widget::enable(bool enable)
|
|
{
|
|
m_enabled = enable;
|
|
setThemeQualifier("disabled", !enable);
|
|
}
|
|
|
|
void Widget::addThemeOverride(const String& fullKey, ostd::Stylesheet::TypeVariant value)
|
|
{
|
|
m_themeOverrides.push_back({ fullKey, value });
|
|
}
|
|
|
|
void Widget::reloadTheme(bool propagate)
|
|
{
|
|
if (getWindow().theme() == nullptr)
|
|
return;
|
|
if (!isThemingEnabled())
|
|
return;
|
|
|
|
auto& theme = *getWindow().theme();
|
|
applyTheme(theme);
|
|
ostd::Stylesheet& const_cast_theme = const_cast<ostd::Stylesheet&>(theme);
|
|
|
|
struct tBackup {
|
|
const ostd::Stylesheet::TypeVariant* ptr;
|
|
ostd::Stylesheet::TypeVariant val;
|
|
String key;
|
|
};
|
|
|
|
stdvec<tBackup> backup;
|
|
for (auto& rule : m_themeOverrides)
|
|
{
|
|
auto currentValuePtr = theme.getFull(rule.fullKey);
|
|
ostd::Stylesheet::TypeVariant currentValue;
|
|
if (currentValuePtr)
|
|
currentValue = *currentValuePtr;
|
|
backup.push_back({ currentValuePtr, currentValue, rule.fullKey });
|
|
const_cast_theme.setFull(rule.fullKey, rule.value);
|
|
}
|
|
__applyTheme(const_cast_theme, propagate);
|
|
for (auto&[ptr, val, key] : backup)
|
|
{
|
|
if (ptr == nullptr)
|
|
const_cast_theme.removeRule(key);
|
|
else
|
|
const_cast_theme.setFull(key, val);
|
|
}
|
|
}
|
|
|
|
void Widget::setThemeQualifier(const String& qualifier, bool value)
|
|
{
|
|
for (auto& [name, state] : m_qualifierList)
|
|
{
|
|
if (name == qualifier)
|
|
{
|
|
if (state == value) return;
|
|
state = value;
|
|
reloadTheme();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Widget::getThemeQualifier(const String& qualifier) const
|
|
{
|
|
for (auto& [name, state] : m_qualifierList)
|
|
{
|
|
if (name == qualifier)
|
|
return state;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Widget::addThemeID(const String& id)
|
|
{
|
|
if (STDVEC_CONTAINS(m_themeIDList, id))
|
|
return false;
|
|
m_themeIDList.push_back(id);
|
|
return true;
|
|
}
|
|
|
|
bool Widget::removeThemeID(const String& id)
|
|
{
|
|
if (!STDVEC_CONTAINS(m_themeIDList, id))
|
|
return false;
|
|
STDVEC_REMOVE(m_themeIDList, id);
|
|
return true;
|
|
}
|
|
|
|
void Widget::setVisible(bool 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)
|
|
{
|
|
switch (type)
|
|
{
|
|
case eCallback::MousePressed: callback_onMousePressed = std::move(callback); break;
|
|
case eCallback::MouseReleased: callback_onMouseReleased = std::move(callback); break;
|
|
case eCallback::MouseMoved: callback_onMouseMoved = std::move(callback); break;
|
|
case eCallback::MouseScrolled: callback_onMouseScrolled = std::move(callback); break;
|
|
case eCallback::DragAndDrop: callback_onDragAndDrop = std::move(callback); break;
|
|
case eCallback::MouseEntered: callback_onMouseEntered = std::move(callback); break;
|
|
case eCallback::MouseExited: callback_onMouseExited = std::move(callback); break;
|
|
case eCallback::MouseDragged: callback_onMouseDragged = std::move(callback); break;
|
|
case eCallback::KeyPressed: callback_onKeyPressed = std::move(callback); break;
|
|
case eCallback::KeyReleased: callback_onKeyReleased = std::move(callback); break;
|
|
case eCallback::TextEntered: callback_onTextEntered = std::move(callback); break;
|
|
case eCallback::WindowClosed: callback_onWindowClosed = std::move(callback); break;
|
|
case eCallback::WindowResized: callback_onWindowResized = std::move(callback); break;
|
|
case eCallback::WindowFocused: callback_onWindowFocused = std::move(callback); break;
|
|
case eCallback::WindowFocusLost: callback_onWindowFocusLost = std::move(callback); break;
|
|
case eCallback::ActionPerformed: callback_onActionPerformed = std::move(callback); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
void Widget::__draw(ogfx::BasicRenderer2D& gfx)
|
|
{
|
|
if (!isVisible())
|
|
return;
|
|
beforeDraw(gfx);
|
|
if (m_useBackgroundGradient)
|
|
gfx.fillGradientRect({ getGlobalPosition(), getSize() }, m_backgroundGradient);
|
|
else if (m_showBackground)
|
|
gfx.fillRoundRect({ getGlobalPosition(), getSize() }, m_backgroundColor, m_borderRadius);
|
|
onDraw(gfx);
|
|
|
|
// gfx.fillRect(getGlobalPureContentBounds(), { 0, 255, 0, 120 });
|
|
const bool needsContentClip = !m_rootChild;
|
|
if (needsContentClip)
|
|
gfx.pushClippingRect(getGlobalPureContentBounds(), true);
|
|
if (hasChildren())
|
|
m_widgets.draw(gfx);
|
|
if (needsContentClip)
|
|
gfx.popClippingRect();
|
|
if (m_showBorder)
|
|
gfx.drawRoundRect({ getGlobalPosition(), getSize() }, m_borderColor, m_borderRadius, m_borderWidth);
|
|
afterDraw(gfx);
|
|
}
|
|
|
|
void Widget::__update(void)
|
|
{
|
|
onUpdate();
|
|
if (hasChildren())
|
|
m_widgets.update();
|
|
}
|
|
|
|
void Widget::__onMousePressed(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isMousePressedEventEnabled()) return;
|
|
setThemeQualifier("pressed");
|
|
if (hasChildren())
|
|
m_widgets.onMousePressed(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onMousePressed)
|
|
callback_onMousePressed(event);
|
|
onMousePressed(event);
|
|
m_pressedButton = event.mouse->button;
|
|
}
|
|
}
|
|
|
|
void Widget::__onMouseReleased(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isMouseReleasedEventEnabled()) return;
|
|
setThemeQualifier("pressed", false);
|
|
m_pressedButton = ogfx::MouseEventData::eButton::None;
|
|
if (hasChildren())
|
|
m_widgets.onMouseReleased(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onMouseReleased)
|
|
callback_onMouseReleased(event);
|
|
onMouseReleased(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__onDragAndDrop(const Event& event)
|
|
{
|
|
if (hasChildren())
|
|
m_widgets.onMouseReleased(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onDragAndDrop)
|
|
callback_onDragAndDrop(event);
|
|
onDragAndDrop(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__onMouseMoved(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isMouseMovedEventEnabled()) return;
|
|
if (hasChildren())
|
|
m_widgets.onMouseMoved(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onMouseMoved)
|
|
callback_onMouseMoved(event);
|
|
onMouseMoved(event);
|
|
}
|
|
if (isTooltipEnabled() && isMouseInside())
|
|
getWindow().restartTooltipTimer();
|
|
}
|
|
|
|
void Widget::__onMouseScrolled(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isMouseScrolledEventEnabled()) return;
|
|
if (hasChildren())
|
|
m_widgets.onMouseScrolled(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onMouseScrolled)
|
|
callback_onMouseScrolled(event);
|
|
onMouseScrolled(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__onMouseEntered(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isMouseEnteredEventEnabled()) return;
|
|
setThemeQualifier("hover");
|
|
if (callback_onMouseEntered)
|
|
callback_onMouseEntered(event);
|
|
onMouseEntered(event);
|
|
if (isTooltipEnabled())
|
|
getWindow().startTooltipTimer(getTooltipText());
|
|
}
|
|
|
|
void Widget::__onMouseExited(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isMouseExitedEventEnabled()) return;
|
|
setThemeQualifier("hover", false);
|
|
if (callback_onMouseExited)
|
|
callback_onMouseExited(event);
|
|
onMouseExited(event);
|
|
if (isTooltipEnabled())
|
|
getWindow().stopTooltipTimer();
|
|
}
|
|
|
|
void Widget::__onMouseDragged(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isMouseDraggedEventEnabled()) return;
|
|
if (hasChildren())
|
|
m_widgets.onMouseDragged(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onMouseDragged)
|
|
callback_onMouseDragged(event);
|
|
onMouseDragged(event);
|
|
}
|
|
if (isTooltipEnabled() && isMouseInside())
|
|
getWindow().restartTooltipTimer();
|
|
}
|
|
|
|
void Widget::__onKeyPressed(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isKeyPressedEventEnabled()) return;
|
|
if (hasChildren())
|
|
m_widgets.onKeyPressed(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onKeyPressed)
|
|
callback_onKeyPressed(event);
|
|
onKeyPressed(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__onKeyReleased(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isKeyReleasedEventEnabled()) return;
|
|
m_pressedButton = ogfx::MouseEventData::eButton::None;
|
|
if (hasChildren())
|
|
m_widgets.onKeyReleased(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onKeyReleased)
|
|
callback_onKeyReleased(event);
|
|
onKeyReleased(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__onTextEntered(const Event& event)
|
|
{
|
|
if (event.isHandled() || !isTextEnteredEventEnabled()) return;
|
|
if (hasChildren())
|
|
m_widgets.onTextEntered(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onTextEntered)
|
|
callback_onTextEntered(event);
|
|
onTextEntered(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__onWindowClosed(const Event& event)
|
|
{
|
|
if (hasChildren())
|
|
m_widgets.onWindowClosed(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onWindowClosed)
|
|
callback_onWindowClosed(event);
|
|
onWindowClosed(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__onWindowResized(const Event& event)
|
|
{
|
|
if (hasChildren())
|
|
m_widgets.onWindowResized(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onWindowResized)
|
|
callback_onWindowResized(event);
|
|
onWindowResized(event);
|
|
}
|
|
if (m_layout)
|
|
relayout();
|
|
}
|
|
|
|
void Widget::__onWindowFocused(const Event& event)
|
|
{
|
|
if (hasChildren())
|
|
m_widgets.onWindowFocused(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onWindowFocused)
|
|
callback_onWindowFocused(event);
|
|
onWindowFocused(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__onWindowFocusLost(const Event& event)
|
|
{
|
|
if (hasChildren())
|
|
m_widgets.onWindowFocusLost(event);
|
|
if (!event.isHandled())
|
|
{
|
|
if (callback_onWindowFocusLost)
|
|
callback_onWindowFocusLost(event);
|
|
onWindowFocusLost(event);
|
|
}
|
|
}
|
|
|
|
void Widget::__applyTheme(const ostd::Stylesheet& theme, bool propagate)
|
|
{
|
|
if (propagate && hasChildren())
|
|
m_widgets.onThemeApplied(theme);
|
|
apply_common_theme_values(theme);
|
|
applyTheme(theme);
|
|
}
|
|
|
|
void Widget::apply_common_theme_values(const ostd::Stylesheet& theme)
|
|
{
|
|
setTextColor(getThemeValue<Color>(theme, "textColor", m_textColor));
|
|
setBackgroundColor(getThemeValue<Color>(theme, "backgroundColor", m_backgroundColor));
|
|
setFontSize(getThemeValue<i32>(theme, "fontSize", m_fontSize));
|
|
setBorderRadius(getThemeValue<i32>(theme, "borderRadius", m_borderRadius));
|
|
setBorderWidth(getThemeValue<i32>(theme, "borderWidth", m_borderWidth));
|
|
enableBorder(getThemeValue<bool>(theme, "showBorder", m_showBorder));
|
|
setBorderColor(getThemeValue<Color>(theme, "borderColor", m_borderColor));
|
|
enableBackground(getThemeValue<bool>(theme, "showBackground", m_showBackground));
|
|
setPadding(getThemeValue<Rectangle>(theme, "padding", m_padding));
|
|
setMargin(getThemeValue<Rectangle>(theme, "margin", m_margin));
|
|
enableBackgroundGradient(getThemeValue<bool>(theme, "useBackgroundGradient", m_useBackgroundGradient));
|
|
setBackgroundGradient(getThemeValue<ColorGradient>(theme, "backgroundGradient", m_backgroundGradient));
|
|
if (isBackgroundGradientEnabled() && isBackgroundEnabled())
|
|
enableBackground(false);
|
|
}
|
|
}
|
|
}
|