Began implementing some little widget tree system.

This commit is contained in:
Patrick 2024-09-19 00:58:27 +02:00
parent d7c15660e5
commit b29fefa6ec
11 changed files with 437 additions and 65 deletions

View File

@ -69,6 +69,15 @@ void UIApp::init(const AppInitArgs& args)
{ {
mGamepad.open(gamepads[0]); mGamepad.open(gamepads[0]);
} }
// init the UI
mWidgetTree.init({.renderer = &mUIRenderer});
mLabel = mWidgetTree.getRootWidget().emplaceChild<Label>({
.text = "Test-Text!\nSecond $f00line$fff?",
.color = glm::vec4(0.f, 1.f, 0.f, 1.f),
.posX = 100,
.posY = 100
});
} }
void UIApp::update(const AppUpdateArgs& args) void UIApp::update(const AppUpdateArgs& args)
@ -76,6 +85,8 @@ void UIApp::update(const AppUpdateArgs& args)
Application::update(args); Application::update(args);
processInput(args); processInput(args);
mLabel->setText(std::format("Rotation: {}{}\n$rUI vertices: $f00{}", mRotation > 180.f ? "$00f": "$fff" , mRotation, mNumVertices));
mWidgetTree.revalidateWidgets();
// begin rendering // begin rendering
sdlpp::GPUCommandBuffer cmdBuffer = mDevice.acquireCommandBuffer(); sdlpp::GPUCommandBuffer cmdBuffer = mDevice.acquireCommandBuffer();

View File

@ -6,8 +6,10 @@
#include <glm/vec2.hpp> #include <glm/vec2.hpp>
#include "./ui/ui_renderer.hpp"
#include "../application.hpp" #include "../application.hpp"
#include "../gui/label.hpp"
#include "../gui/widget.hpp"
#include "../gui/ui_renderer.hpp"
#include "../sdlpp/gamepad.hpp" #include "../sdlpp/gamepad.hpp"
namespace sdl_gpu_test namespace sdl_gpu_test
@ -22,6 +24,9 @@ private:
sdlpp::GPUSampler mSampler; sdlpp::GPUSampler mSampler;
UIRenderer mUIRenderer; UIRenderer mUIRenderer;
WidgetTree mWidgetTree;
Label* mLabel;
sdlpp::Gamepad mGamepad; sdlpp::Gamepad mGamepad;
Uint32 mNumVertices = 0; Uint32 mNumVertices = 0;

View File

@ -5,6 +5,9 @@ src_files = Split("""
main.cpp main.cpp
application.cpp application.cpp
gui/label.cpp
gui/ui_renderer.cpp
gui/widget.cpp
util/bitmap.cpp util/bitmap.cpp
util/font_map.cpp util/font_map.cpp
util/mesh.cpp util/mesh.cpp
@ -16,7 +19,6 @@ src_files = Split("""
4_textured_cube/app.cpp 4_textured_cube/app.cpp
5_input/app.cpp 5_input/app.cpp
6_ui/app.cpp 6_ui/app.cpp
6_ui/ui/ui_renderer.cpp
""") """)
shader_files = env.Glob("#assets/shaders/glsl/*.frag") \ shader_files = env.Glob("#assets/shaders/glsl/*.frag") \

View File

@ -76,6 +76,7 @@ void Application::run(std::span<const char*> args)
.secondsSinceStart = std::chrono::duration_cast<std::chrono::duration<float>>(frameTime - startTime).count(), .secondsSinceStart = std::chrono::duration_cast<std::chrono::duration<float>>(frameTime - startTime).count(),
.tickSeconds = std::chrono::duration_cast<std::chrono::duration<float>>(frameTime - lastFrameTime).count() .tickSeconds = std::chrono::duration_cast<std::chrono::duration<float>>(frameTime - lastFrameTime).count()
}); });
mTaskLoop.tick();
lastFrameTime = frameTime; lastFrameTime = frameTime;
} }
cleanup({}); cleanup({});

View File

@ -6,6 +6,7 @@
#include <cstring> #include <cstring>
#include <mijin/async/coroutine.hpp>
#include <mijin/virtual_filesystem/stacked.hpp> #include <mijin/virtual_filesystem/stacked.hpp>
#include "./sdlpp/event.hpp" #include "./sdlpp/event.hpp"
@ -36,6 +37,7 @@ protected:
sdlpp::Window mWindow; sdlpp::Window mWindow;
sdlpp::GPUDevice mDevice; sdlpp::GPUDevice mDevice;
mijin::StackedFileSystemAdapter mFileSystem; mijin::StackedFileSystemAdapter mFileSystem;
mijin::SimpleTaskLoop mTaskLoop;
bool mRunning = true; bool mRunning = true;
public: public:
virtual ~Application() noexcept = default; virtual ~Application() noexcept = default;
@ -49,6 +51,9 @@ public:
[[nodiscard]] [[nodiscard]]
mijin::StackedFileSystemAdapter& getFileSystem() noexcept { return mFileSystem; } mijin::StackedFileSystemAdapter& getFileSystem() noexcept { return mFileSystem; }
[[nodiscard]]
mijin::SimpleTaskLoop& getTaskLoop() noexcept { return mTaskLoop; }
virtual void init(const AppInitArgs& args); virtual void init(const AppInitArgs& args);
virtual void cleanup(const AppCleanupArgs& args); virtual void cleanup(const AppCleanupArgs& args);
virtual void update(const AppUpdateArgs& args); virtual void update(const AppUpdateArgs& args);

View File

@ -0,0 +1,75 @@
#include "./label.hpp"
namespace sdl_gpu_test
{
Label::Label(LabelCreateArgs args) : mText(std::move(args.text)), mColor(args.color), mPosX(args.posX), mPosY(args.posY)
{
}
void Label::setText(std::string text)
{
if (text != mText)
{
mText = std::move(text);
invalidate();
}
}
void Label::setColor(const glm::vec4& color)
{
if (color != mColor)
{
mColor = color;
invalidate();
}
}
void Label::setPosX(int posX)
{
if (posX != mPosX)
{
mPosX = posX;
invalidate();
}
}
void Label::setPosY(int posY)
{
if (posY != mPosY)
{
mPosY = posY;
invalidate();
}
}
void Label::handleEnteredTree()
{
update();
}
void Label::revalidate()
{
update();
}
void Label::update()
{
if (mTree == nullptr)
{
return;
}
if (mPrimitiveID != UIRenderer::UNSET_PRIMITIVE_ID)
{
mTree->getRenderer().removePrimitive(mPrimitiveID);
}
mTree->getRenderer().drawText({
.x = mPosX,
.y = mPosY,
.text = mText,
.color = mColor
}, &mPrimitiveID);
}
}

View File

@ -0,0 +1,58 @@
#pragma once
#if !defined(SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_GUI_LABEL_HPP_INCLUDED)
#define SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_GUI_LABEL_HPP_INCLUDED 1
#include <glm/vec4.hpp>
#include "./widget.hpp"
namespace sdl_gpu_test
{
struct LabelCreateArgs
{
std::string text;
glm::vec4 color = {1.f, 1.f, 1.f, 1.f};
int posX = 0;
int posY = 0;
};
class Label : public Widget
{
public:
using create_args_t = LabelCreateArgs;
private:
std::string mText;
glm::vec4 mColor = {1.f, 1.f, 1.f, 1.f};
int mPosX = 0;
int mPosY = 0;
UIRenderer::primitive_id_t mPrimitiveID = UIRenderer::UNSET_PRIMITIVE_ID;
public:
explicit Label(LabelCreateArgs args);
[[nodiscard]]
const std::string& getText() const noexcept { return mText; }
[[nodiscard]]
const glm::vec4& getColor() const noexcept { return mColor; }
[[nodiscard]]
int getPosX() const noexcept { return mPosX; }
[[nodiscard]]
int getPosY() const noexcept { return mPosY; }
void setText(std::string text);
void setColor(const glm::vec4& color);
void setPosX(int posX);
void setPosY(int posY);
void handleEnteredTree() override;
void revalidate() override;
private:
void update();
};
} // namespace sdl_gpu_test
#endif // !defined(SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_GUI_LABEL_HPP_INCLUDED)

View File

@ -72,7 +72,7 @@ void UIRenderer::init(Application& application)
.offset = offsetof(UIVertex, color) .offset = offsetof(UIVertex, color)
} }
}; };
mUIPipeline.create(device, { mPipeline.create(device, {
.vertexShader = vertexShader, .vertexShader = vertexShader,
.fragmentShader = fragmentShader, .fragmentShader = fragmentShader,
.vertexInputState = { .vertexInputState = {
@ -92,34 +92,24 @@ void UIRenderer::init(Application& application)
.textureHeight = 256 .textureHeight = 256
}); });
// render some test text
drawText({
.x = 100,
.y = 100,
.text = "Test-Text!\nSecond $f00line$fff?"
});
// create UI vertex buffer
mUIVertexBuffer.create(device, {
.usage = {.vertex = true},
.size = static_cast<Uint32>(mUIVertices.size() * sizeof(UIVertex))
});
application.uploadVertexData(mUIVertexBuffer, std::span(mUIVertices));
// create texture and sampler // create texture and sampler
sdlpp::GPUTextureCreateArgs textureArgs = { sdlpp::GPUTextureCreateArgs textureArgs = {
.format = sdlpp::GPUTextureFormat::R8G8B8A8_UNORM_SRGB, .format = sdlpp::GPUTextureFormat::R8G8B8A8_UNORM_SRGB,
.usage = {.sampler = true} .usage = {.sampler = true}
}; };
mUITexture = application.loadTexture("bitmaps/ui.png", textureArgs); mTexture = application.loadTexture("bitmaps/ui.png", textureArgs);
mUISampler.create(device, {}); mSampler.create(device, {});
} }
void UIRenderer::render(const UIRendererRenderArgs& args) void UIRenderer::render(const UIRendererRenderArgs& args)
{ {
if (!mUIVertices.empty()) if (!mVertices.empty())
{ {
if (mVerticesDirty)
{
uploadVertices();
}
const UIVertexShaderParameters uiVertexShaderParameters = { const UIVertexShaderParameters uiVertexShaderParameters = {
.windowSize = { .windowSize = {
static_cast<float>(args.targetTextureWidth), static_cast<float>(args.targetTextureHeight) static_cast<float>(args.targetTextureWidth), static_cast<float>(args.targetTextureHeight)
@ -137,19 +127,24 @@ void UIRenderer::render(const UIRendererRenderArgs& args)
static const glm::vec4 WHITE(1.f, 1.f, 1.f, 1.f); static const glm::vec4 WHITE(1.f, 1.f, 1.f, 1.f);
args.cmdBuffer->pushFragmentUniformData(0, std::span(&WHITE, 1)); args.cmdBuffer->pushFragmentUniformData(0, std::span(&WHITE, 1));
args.cmdBuffer->pushVertexUniformData(0, std::span(&uiVertexShaderParameters, 1)); args.cmdBuffer->pushVertexUniformData(0, std::span(&uiVertexShaderParameters, 1));
renderPass.bindFragmentSampler({.texture = mUITexture, .sampler = mUISampler}); renderPass.bindFragmentSampler({.texture = mTexture, .sampler = mSampler});
renderPass.bindGraphicsPipeline(mUIPipeline); renderPass.bindGraphicsPipeline(mPipeline);
renderPass.bindVertexBuffer({.buffer = mUIVertexBuffer}); renderPass.bindVertexBuffer({.buffer = mVertexBuffer});
renderPass.drawPrimitives({.numVertices = static_cast<Uint32>(mUIVertices.size())}); renderPass.drawPrimitives({.numVertices = static_cast<Uint32>(mVertices.size())});
renderPass.end(); renderPass.end();
} }
} }
void UIRenderer::drawText(const DrawTextArgs& args) void UIRenderer::drawText(const DrawTextArgs& args, primitive_id_t* outPrimitiveId)
{ {
glm::vec4 color = args.color; glm::vec4 color = args.color;
unsigned posX = args.x; int posX = args.x;
unsigned posY = args.y; int posY = args.y;
if (outPrimitiveId != nullptr && *outPrimitiveId == UNSET_PRIMITIVE_ID)
{
*outPrimitiveId = mNextOwner++;
}
for (std::size_t pos = 0; pos < args.text.size(); ++pos) for (std::size_t pos = 0; pos < args.text.size(); ++pos)
{ {
@ -167,20 +162,32 @@ void UIRenderer::drawText(const DrawTextArgs& args)
break; break;
} }
chr = args.text[pos]; chr = args.text[pos];
if (pos < args.text.size() - 2 switch (chr)
&& mijin::isHexadecimalChar(chr)
&& mijin::isHexadecimalChar(args.text[pos + 1])
&& mijin::isHexadecimalChar(args.text[pos + 2]))
{ {
unsigned num = 0; case 'r':
(void) mijin::toNumber(args.text.substr(pos, 3), num, 16); color = args.color;
const std::uint8_t red = static_cast<std::uint8_t>(num >> 8); break;
const std::uint8_t green = static_cast<std::uint8_t>((num >> 4) & 0xF); default:
const std::uint8_t blue = static_cast<std::uint8_t>(num & 0xF); if (pos < args.text.size() - 2
color.r = static_cast<float>(red) / 15.f; && mijin::isHexadecimalChar(chr)
color.g = static_cast<float>(green) / 15.f; && mijin::isHexadecimalChar(args.text[pos + 1])
color.b = static_cast<float>(blue) / 15.f; && mijin::isHexadecimalChar(args.text[pos + 2]))
pos += 2; {
unsigned num = 0;
(void) mijin::toNumber(args.text.substr(pos, 3), num, 16);
const std::uint8_t red = static_cast<std::uint8_t>(num >> 8);
const std::uint8_t green = static_cast<std::uint8_t>((num >> 4) & 0xF);
const std::uint8_t blue = static_cast<std::uint8_t>(num & 0xF);
color.r = static_cast<float>(red) / 15.f;
color.g = static_cast<float>(green) / 15.f;
color.b = static_cast<float>(blue) / 15.f;
pos += 2;
}
else
{
// what?
}
break;
} }
break; break;
case '\\': case '\\':
@ -197,14 +204,19 @@ void UIRenderer::drawText(const DrawTextArgs& args)
.y = posY, .y = posY,
.chr = chr, .chr = chr,
.color = color .color = color
}); }, outPrimitiveId);
break; break;
} }
} }
} }
unsigned UIRenderer::drawChar(const DrawCharArgs& args) unsigned UIRenderer::drawChar(const DrawCharArgs& args, primitive_id_t* outPrimitiveId)
{ {
if (outPrimitiveId != nullptr && *outPrimitiveId == UNSET_PRIMITIVE_ID)
{
*outPrimitiveId = mNextOwner++;
}
const UVFontMapEntry& entry = mFontMap.entries[args.chr < 0 ? '_' : args.chr]; // TODO: more chars const UVFontMapEntry& entry = mFontMap.entries[args.chr < 0 ? '_' : args.chr]; // TODO: more chars
const glm::vec2 topLeft = {args.x + entry.xOffset, args.y + entry.yOffset}; const glm::vec2 topLeft = {args.x + entry.xOffset, args.y + entry.yOffset};
drawQuadInternal({ drawQuadInternal({
@ -213,12 +225,31 @@ unsigned UIRenderer::drawChar(const DrawCharArgs& args)
.uvTopLeft = {entry.uvX, entry.uvY}, .uvTopLeft = {entry.uvX, entry.uvY},
.uvBottomRight = {entry.uvX + entry.uvWidth, entry.uvY + entry.uvHeight}, .uvBottomRight = {entry.uvX + entry.uvWidth, entry.uvY + entry.uvHeight},
.color = args.color .color = args.color
}); }, outPrimitiveId == nullptr ? UNSET_PRIMITIVE_ID : *outPrimitiveId);
return entry.xAdvance; return entry.xAdvance;
} }
void UIRenderer::drawQuadInternal(const DrawQuadInternalArgs& args) bool UIRenderer::removePrimitive(primitive_id_t primitiveId)
{
bool didRemove = false;
for (int triangleIdx = static_cast<int>(mTriangleOwners.size()) - 1; triangleIdx >= 0; --triangleIdx)
{
if (mTriangleOwners[triangleIdx] == primitiveId)
{
mTriangleOwners.erase(mTriangleOwners.begin() + triangleIdx);
mVertices.erase(mVertices.begin() + 3 * triangleIdx, mVertices.begin() + 3 * (triangleIdx + 1));
didRemove = true;
}
}
if (didRemove)
{
mVerticesDirty = true;
}
return didRemove;
}
void UIRenderer::drawQuadInternal(const DrawQuadInternalArgs& args, primitive_id_t primitiveId)
{ {
const UIVertex topLeft = { const UIVertex topLeft = {
.pos = args.topLeft, .pos = args.topLeft,
@ -241,11 +272,41 @@ void UIRenderer::drawQuadInternal(const DrawQuadInternalArgs& args)
.color = args.color .color = args.color
}; };
mUIVertices.push_back(topLeft); addTriangleInternal(topLeft, bottomLeft, topRight, primitiveId);
mUIVertices.push_back(bottomLeft); addTriangleInternal(bottomRight, topRight, bottomLeft, primitiveId);
mUIVertices.push_back(topRight);
mUIVertices.push_back(bottomRight); mVerticesDirty = true;
mUIVertices.push_back(topRight); }
mUIVertices.push_back(bottomLeft);
void UIRenderer::addTriangleInternal(const UIVertex& v0, const UIVertex& v1, const UIVertex& v2, UIRenderer::primitive_id_t primitiveId)
{
mVertices.push_back(v0);
mVertices.push_back(v1);
mVertices.push_back(v2);
mTriangleOwners.push_back(primitiveId);
}
void UIRenderer::uploadVertices()
{
if (mVertexBufferSize < mVertices.size())
{
if (mVertexBufferSize > 0)
{
mVertexBuffer.destroy();
}
mVertexBuffer.create(mApplication->getDevice(), {
.usage = {.vertex = true},
.size = static_cast<Uint32>(mVertices.size() * sizeof(UIVertex))
});
mVertexBufferSize = mVertices.size();
}
if (!mVertices.empty())
{
mApplication->uploadVertexData(mVertexBuffer, std::span(mVertices));
}
mVerticesDirty = false;
} }
} }

View File

@ -6,9 +6,9 @@
#include <glm/vec2.hpp> #include <glm/vec2.hpp>
#include <glm/vec4.hpp> #include <glm/vec4.hpp>
#include "../../application.hpp" #include "../application.hpp"
#include "../../sdlpp/gpu.hpp" #include "../sdlpp/gpu.hpp"
#include "../../util/font_map.hpp" #include "../util/font_map.hpp"
namespace sdl_gpu_test::inline app6 namespace sdl_gpu_test::inline app6
{ {
@ -21,16 +21,16 @@ struct UIVertex
struct DrawTextArgs struct DrawTextArgs
{ {
unsigned x = 0; int x = 0;
unsigned y = 0; int y = 0;
std::string_view text; std::string_view text;
glm::vec4 color = glm::vec4(1.f); glm::vec4 color = glm::vec4(1.f);
}; };
struct DrawCharArgs struct DrawCharArgs
{ {
unsigned x = 0; int x = 0;
unsigned y = 0; int y = 0;
char chr; char chr;
glm::vec4 color = glm::vec4(1.f); glm::vec4 color = glm::vec4(1.f);
}; };
@ -45,20 +45,29 @@ struct UIRendererRenderArgs
class UIRenderer class UIRenderer
{ {
public:
using primitive_id_t = std::uint64_t;
static constexpr std::uint64_t UNSET_PRIMITIVE_ID = 0;
private: private:
Application* mApplication = nullptr; Application* mApplication = nullptr;
sdlpp::GPUBuffer mUIVertexBuffer; sdlpp::GPUBuffer mVertexBuffer;
sdlpp::GPUGraphicsPipeline mUIPipeline; sdlpp::GPUGraphicsPipeline mPipeline;
std::vector<UIVertex> mUIVertices; sdlpp::GPUTexture mTexture;
sdlpp::GPUTexture mUITexture; sdlpp::GPUSampler mSampler;
sdlpp::GPUSampler mUISampler; std::vector<UIVertex> mVertices;
std::vector<primitive_id_t> mTriangleOwners;
primitive_id_t mNextOwner = 1;
UVFontMap mFontMap; UVFontMap mFontMap;
bool mVerticesDirty = true;
std::size_t mVertexBufferSize = 0;
public: public:
void init(Application& application); void init(Application& application);
void render(const UIRendererRenderArgs& args); void render(const UIRendererRenderArgs& args);
void drawText(const DrawTextArgs& args); void drawText(const DrawTextArgs& args, primitive_id_t* outPrimitiveId = nullptr);
unsigned drawChar(const DrawCharArgs& args); unsigned drawChar(const DrawCharArgs& args, primitive_id_t* outPrimitiveId = nullptr);
bool removePrimitive(primitive_id_t primitiveId);
private: private:
struct DrawQuadInternalArgs struct DrawQuadInternalArgs
{ {
@ -68,7 +77,9 @@ private:
glm::vec2 uvBottomRight; glm::vec2 uvBottomRight;
glm::vec4 color = glm::vec4(1.f); glm::vec4 color = glm::vec4(1.f);
}; };
void drawQuadInternal(const DrawQuadInternalArgs& args); void drawQuadInternal(const DrawQuadInternalArgs& args, primitive_id_t primitiveId);
void addTriangleInternal(const UIVertex& v0, const UIVertex& v1, const UIVertex& v2, primitive_id_t primitiveId);
void uploadVertices();
}; };
} // namespace sdl_gpu_test::inline app6 } // namespace sdl_gpu_test::inline app6

View File

@ -0,0 +1,67 @@
#include "./widget.hpp"
#include <mijin/debug/assert.hpp>
namespace sdl_gpu_test
{
void Widget::enterTree(WidgetTree* tree, ParentWidget* parent)
{
MIJIN_ASSERT(mTree == nullptr && mParent == nullptr, "Widget is already in a tree.");
MIJIN_ASSERT_FATAL(tree != nullptr, "Missing argument: tree");
mTree = tree;
mParent = parent;
handleEnteredTree();
}
void Widget::invalidate()
{
if (mTree != nullptr)
{
mTree->invalidateWidget(this);
}
}
void ParentWidget::handleEnteredTree()
{
for (widget_ptr_t& child : mChildren)
{
child->enterTree(mTree, this);
}
}
Widget* ParentWidget::addChild(widget_ptr_t&& child)
{
mChildren.push_back(std::move(child));
if (mTree != nullptr)
{
mChildren.back()->enterTree(mTree, this);
}
return mChildren.back().get();
}
void WidgetTree::init(const WidgetTreeInitArgs& args)
{
MIJIN_ASSERT_FATAL(args.renderer != nullptr, "Missing argument: renderer.");
mRenderer = args.renderer;
mRootWidget.enterTree(this, nullptr);
}
void WidgetTree::revalidateWidgets()
{
for (Widget* widget : mInvalidWidgets)
{
widget->revalidate();
}
mInvalidWidgets.clear();
}
void WidgetTree::invalidateWidget(Widget* widget) noexcept
{
mInvalidWidgets.insert(widget);
}
}

View File

@ -0,0 +1,76 @@
#pragma once
#if !defined(SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_GUI_WIDGET_HPP_INCLUDED)
#define SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_GUI_WIDGET_HPP_INCLUDED 1
#include <memory>
#include <vector>
#include <unordered_set>
#include "./ui_renderer.hpp"
namespace sdl_gpu_test
{
class Widget
{
protected:
class WidgetTree* mTree = nullptr;
class ParentWidget* mParent = nullptr;
public:
virtual ~Widget() noexcept = default;
virtual void handleEnteredTree() {}
virtual void revalidate() {}
void invalidate();
private:
void enterTree(class WidgetTree* tree, class ParentWidget* parent);
friend class ParentWidget;
friend class WidgetTree;
};
using widget_ptr_t = std::unique_ptr<Widget>;
class ParentWidget : public Widget
{
private:
std::vector<widget_ptr_t> mChildren;
public:
void handleEnteredTree() override;
Widget* addChild(widget_ptr_t&& child);
template<typename TChild>
TChild* emplaceChild(typename TChild::create_args_t args)
{
return static_cast<TChild*>(addChild(std::make_unique<TChild>(args)));
}
};
struct WidgetTreeInitArgs
{
UIRenderer* renderer = nullptr;
};
class WidgetTree
{
private:
UIRenderer* mRenderer = nullptr;
ParentWidget mRootWidget;
std::unordered_set<Widget*> mInvalidWidgets;
public:
void init(const WidgetTreeInitArgs& args);
void revalidateWidgets();
[[nodiscard]]
UIRenderer& getRenderer() const noexcept { return *mRenderer; }
[[nodiscard]]
ParentWidget& getRootWidget() noexcept { return mRootWidget; }
void invalidateWidget(Widget* widget) noexcept;
};
} // namespace sdl_gpu_test
#endif // !defined(SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_GUI_WIDGET_HPP_INCLUDED)