infinipaint/src/GUIStuff/GUIManager.cpp

945 lines
40 KiB
C++
Raw Normal View History

2025-07-06 20:43:53 +03:00
#include "GUIManager.hpp"
#include <include/core/SkPaint.h>
#include <include/core/SkRRect.h>
#include <include/core/SkFont.h>
#include <include/core/SkFontMetrics.h>
#include <include/core/SkPath.h>
2025-11-18 18:15:50 +03:00
#include <include/core/SkPathBuilder.h>
2025-07-06 20:43:53 +03:00
#include "Elements/SVGIcon.hpp"
2025-11-08 20:50:23 +03:00
#include "Elements/FontPicker.hpp"
2025-07-06 20:43:53 +03:00
#include "Elements/SelectableButton.hpp"
2025-09-28 16:32:57 +03:00
#include "Elements/RotateWheel.hpp"
#include "Elements/TextParagraph.hpp"
2025-07-06 20:43:53 +03:00
#include "Elements/MovableTabList.hpp"
#include "Elements/Element.hpp"
#include "Elements/TextParagraph.hpp"
2025-07-06 20:43:53 +03:00
#include <Helpers/ConvertVec.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <cmath>
#include <limits>
#include <modules/skparagraph/src/ParagraphBuilderImpl.h>
#include <modules/skparagraph/include/ParagraphStyle.h>
#include <modules/skparagraph/include/FontCollection.h>
#include <modules/skparagraph/include/TextStyle.h>
#include <modules/skunicode/include/SkUnicode_icu.h>
#include "../FontData.hpp"
2025-07-06 20:43:53 +03:00
namespace GUIStuff {
GUIManager::GUIManager()
{
Clay_SetMaxMeasureTextCacheWordCount(100000);
clayArena = Clay_CreateArenaWithCapacityAndMemory(Clay_MinMemorySize(), malloc(Clay_MinMemorySize()));
clayInstance = Clay_Initialize(clayArena, Clay_Dimensions(1.0f, 1.0f), (Clay_ErrorHandler)clay_error_handler);
Clay_SetMeasureTextFunction(clay_skia_measure_text, this);
//Clay_SetDebugModeEnabled(true);
}
Clay_Dimensions GUIManager::clay_skia_measure_text(Clay_StringSlice str, Clay_TextElementConfig* config, void* userData) {
GUIManager* window = static_cast<GUIManager*>(userData);
skia::textlayout::ParagraphStyle pStyle;
pStyle.setTextAlign(skia::textlayout::TextAlign::kLeft);
skia::textlayout::TextStyle tStyle;
tStyle.setFontSize(config->fontSize);
2025-11-16 10:42:01 +03:00
tStyle.setFontFamilies(window->io->fonts->get_default_font_families());
pStyle.setTextStyle(tStyle);
2025-11-16 10:42:01 +03:00
skia::textlayout::ParagraphBuilderImpl a(pStyle, window->io->fonts->collection, SkUnicodes::ICU::Make());
a.addText(str.chars, str.length);
std::unique_ptr<skia::textlayout::Paragraph> p = a.Build();
p->layout(std::numeric_limits<float>::max());
return Clay_Dimensions(p->getMaxIntrinsicWidth(), p->getHeight());
2025-07-06 20:43:53 +03:00
}
void GUIManager::begin() {
for(auto& [id, e] : elements)
e.isUsedThisFrame = false;
2025-07-06 20:43:53 +03:00
io->mouse.pos = io->mouse.globalPos - windowPos;
io->strArena = &strArena;
Clay_SetLayoutDimensions(Clay_Dimensions(windowSize.x(), windowSize.y()));
Clay_SetPointerState(Clay_Vector2((float)io->mouse.pos.x(), (float)io->mouse.pos.y()), io->mouse.leftHeld);
Clay_UpdateScrollContainers(false, Clay_Vector2(io->mouse.scroll.y(), io->mouse.scroll.y()), io->deltaTime * 2.0f);
Clay_SetCurrentContext(clayInstance);
strArena.reset();
Clay_BeginLayout();
}
void GUIManager::end() {
renderCommands = Clay_EndLayout();
if(!idStack.empty())
throw std::runtime_error("[GUIManager::end] ID Stack is not empty on end (push_id and pop_id calls not equal)");
std::erase_if(elements, [](auto& p) {
return !p.second.isUsedThisFrame;
});
2025-07-06 20:43:53 +03:00
}
void GUIManager::draw(SkCanvas* canvas) {
canvas->save();
canvas->translate(windowPos.x(), windowPos.y());
for(size_t i = 0; i < static_cast<size_t>(renderCommands.length); i++) {
Clay_RenderCommand* command = Clay_RenderCommandArray_Get(&renderCommands, i);
Clay_BoundingBox bb = command->boundingBox;
switch(command->commandType) {
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
Clay_RectangleRenderData* config = &command->renderData.rectangle;
SkVector radii[4] = {
{config->cornerRadius.topLeft, config->cornerRadius.topLeft},
{config->cornerRadius.topRight, config->cornerRadius.topRight},
{config->cornerRadius.bottomRight, config->cornerRadius.bottomRight},
{config->cornerRadius.bottomLeft, config->cornerRadius.bottomLeft}
};
SkRRect rrect;
rrect.setRectRadii(
SkRect::MakeXYWH(bb.x, bb.y, bb.width, bb.height),
radii
);
SkPaint paint;
paint.setStyle(SkPaint::kFill_Style);
paint.setColor4f(convert_vec4<SkColor4f>(config->backgroundColor));
canvas->drawRRect(rrect, paint);
break;
}
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
Clay_TextRenderData* config = &command->renderData.text;
skia::textlayout::ParagraphStyle pStyle;
pStyle.setTextAlign(skia::textlayout::TextAlign::kLeft);
skia::textlayout::TextStyle tStyle;
tStyle.setFontSize(config->fontSize);
2025-11-16 10:42:01 +03:00
tStyle.setFontFamilies(io->fonts->get_default_font_families());
2025-11-01 18:07:39 +03:00
tStyle.setColor(convert_vec4<SkColor4f>(config->textColor).toSkColor());
pStyle.setTextStyle(tStyle);
2025-11-16 10:42:01 +03:00
skia::textlayout::ParagraphBuilderImpl a(pStyle, io->fonts->collection, SkUnicodes::ICU::Make());
a.addText(config->stringContents.chars, config->stringContents.length);
std::unique_ptr<skia::textlayout::Paragraph> p = a.Build();
p->layout(std::numeric_limits<float>::max());
p->paint(canvas, bb.x, bb.y);
2025-07-06 20:43:53 +03:00
break;
}
case CLAY_RENDER_COMMAND_TYPE_BORDER: {
Clay_BorderRenderData* config = &command->renderData.border;
SkPaint p;
p.setColor4f(convert_vec4<SkColor4f>(config->color));
p.setStyle(SkPaint::kStroke_Style);
float halfLineWidth = 0.0f;
// Top Left corner
if (config->cornerRadius.topLeft > 0.0f) {
2025-11-18 18:15:50 +03:00
SkPathBuilder path;
2025-07-06 20:43:53 +03:00
float lineWidth = config->width.top;
p.setStrokeWidth(lineWidth);
path.moveTo(bb.x + halfLineWidth, bb.y + config->cornerRadius.topLeft + halfLineWidth);
2025-11-18 18:15:50 +03:00
path.arcTo(SkPoint{(bb.x + halfLineWidth), (bb.y + halfLineWidth)}, SkPoint{(bb.x + config->cornerRadius.topLeft + halfLineWidth), (bb.y + halfLineWidth)}, config->cornerRadius.topLeft);
canvas->drawPath(path.detach(), p);
2025-07-06 20:43:53 +03:00
}
// Top border
if (config->width.top > 0.0f) {
2025-11-18 18:15:50 +03:00
SkPathBuilder path;
2025-07-06 20:43:53 +03:00
float lineWidth = config->width.top;
p.setStrokeWidth(lineWidth);
path.moveTo((bb.x + config->cornerRadius.topLeft + halfLineWidth), (bb.y + halfLineWidth));
path.lineTo((bb.x + bb.width - config->cornerRadius.topRight - halfLineWidth), (bb.y + halfLineWidth));
2025-11-18 18:15:50 +03:00
canvas->drawPath(path.detach(), p);
2025-07-06 20:43:53 +03:00
}
// Top Right Corner
if (config->cornerRadius.topRight > 0.0f) {
2025-11-18 18:15:50 +03:00
SkPathBuilder path;
2025-07-06 20:43:53 +03:00
float lineWidth = config->width.top;
p.setStrokeWidth(lineWidth);
path.moveTo((bb.x + bb.width - config->cornerRadius.topRight - halfLineWidth), (bb.y + halfLineWidth));
2025-11-18 18:15:50 +03:00
path.arcTo(SkPoint{(bb.x + bb.width - halfLineWidth), (bb.y + halfLineWidth)}, SkPoint{(bb.x + bb.width - halfLineWidth), (bb.y + config->cornerRadius.topRight + halfLineWidth)}, config->cornerRadius.topRight);
canvas->drawPath(path.detach(), p);
2025-07-06 20:43:53 +03:00
}
// Right border
if (config->width.right > 0.0f) {
2025-11-18 18:15:50 +03:00
SkPathBuilder path;
2025-07-06 20:43:53 +03:00
float lineWidth = config->width.right;
p.setStrokeWidth(lineWidth);
path.moveTo((bb.x + bb.width - halfLineWidth), (bb.y + config->cornerRadius.topRight + halfLineWidth));
path.lineTo((bb.x + bb.width - halfLineWidth), (bb.y + bb.height - config->cornerRadius.bottomRight - halfLineWidth));
2025-11-18 18:15:50 +03:00
canvas->drawPath(path.detach(), p);
2025-07-06 20:43:53 +03:00
}
// Bottom Right Corner
if (config->cornerRadius.bottomRight > 0.0f) {
2025-11-18 18:15:50 +03:00
SkPathBuilder path;
2025-07-06 20:43:53 +03:00
float lineWidth = config->width.bottom;
p.setStrokeWidth(lineWidth);
path.moveTo((bb.x + bb.width - halfLineWidth), (bb.y + bb.height - config->cornerRadius.bottomRight - halfLineWidth));
2025-11-18 18:15:50 +03:00
path.arcTo(SkPoint{(bb.x + bb.width - halfLineWidth), (bb.y + bb.height - halfLineWidth)}, SkPoint{(bb.x + bb.width - config->cornerRadius.bottomRight - halfLineWidth), (bb.y + bb.height - halfLineWidth)}, config->cornerRadius.bottomRight);
canvas->drawPath(path.detach(), p);
2025-07-06 20:43:53 +03:00
}
// Bottom Border
if (config->width.bottom > 0.0f) {
2025-11-18 18:15:50 +03:00
SkPathBuilder path;
2025-07-06 20:43:53 +03:00
float lineWidth = config->width.bottom;
p.setStrokeWidth(lineWidth);
path.moveTo((bb.x + config->cornerRadius.bottomLeft + halfLineWidth), (bb.y + bb.height - halfLineWidth));
path.lineTo((bb.x + bb.width - config->cornerRadius.bottomRight - halfLineWidth), (bb.y + bb.height - halfLineWidth));
2025-11-18 18:15:50 +03:00
canvas->drawPath(path.detach(), p);
2025-07-06 20:43:53 +03:00
}
// Bottom Left Corner
if (config->cornerRadius.bottomLeft > 0.0f) {
2025-11-18 18:15:50 +03:00
SkPathBuilder path;
2025-07-06 20:43:53 +03:00
float lineWidth = config->width.bottom;
p.setStrokeWidth(lineWidth);
path.moveTo((bb.x + config->cornerRadius.bottomLeft + halfLineWidth), (bb.y + bb.height - halfLineWidth));
2025-11-18 18:15:50 +03:00
path.arcTo(SkPoint{(bb.x + halfLineWidth), (bb.y + bb.height - halfLineWidth)}, SkPoint{(bb.x + halfLineWidth), (bb.y + bb.height - config->cornerRadius.bottomLeft - halfLineWidth)}, config->cornerRadius.bottomLeft);
canvas->drawPath(path.detach(), p);
2025-07-06 20:43:53 +03:00
}
// Left Border
if (config->width.left > 0.0f) {
2025-11-18 18:15:50 +03:00
SkPathBuilder path;
2025-07-06 20:43:53 +03:00
float lineWidth = config->width.left;
p.setStrokeWidth(lineWidth);
path.moveTo((bb.x + halfLineWidth), (bb.y + bb.height - config->cornerRadius.bottomLeft - halfLineWidth));
path.lineTo((bb.x + halfLineWidth), (bb.y + config->cornerRadius.topRight + halfLineWidth));
2025-11-18 18:15:50 +03:00
canvas->drawPath(path.detach(), p);
2025-07-06 20:43:53 +03:00
}
break;
}
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
2025-11-04 10:58:26 +03:00
Clay_ClipRenderData* clip = &command->renderData.clip;
if(!clip->horizontal) {
bb.x = 0.0f;
bb.width = std::numeric_limits<float>::max();
}
if(!clip->vertical) {
bb.y = 0.0f;
bb.height = std::numeric_limits<float>::max();
}
2025-07-06 20:43:53 +03:00
canvas->save();
2025-11-01 20:47:34 +03:00
SkRect clipRect = SkRect::MakeXYWH(bb.x - 1.0f, bb.y - 1.0f, bb.width + 1.0f, bb.height + 1.0f);
2025-07-06 20:43:53 +03:00
canvas->clipRect(clipRect);
break;
}
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
canvas->restore();
break;
}
case CLAY_RENDER_COMMAND_TYPE_CUSTOM: {
Element* customElement = (Element*)command->renderData.custom.customData;
customElement->clay_draw(canvas, *io, command);
break;
}
default:
break;
}
}
canvas->restore();
}
void GUIManager::top_to_bottom_window_popup_layout(Clay_SizingAxis x, Clay_SizingAxis y, const std::function<void()>& elemUpdate) {
CLAY({.layout = {
.sizing = {.width = x, .height = y },
.padding = CLAY_PADDING_ALL(io->theme->padding1),
.childGap = io->theme->childGap1,
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = convert_vec4<Clay_Color>(io->theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(10),
.floating = { .attachTo = CLAY_ATTACH_TO_PARENT },
2025-11-01 20:47:34 +03:00
.border = {.color = convert_vec4<Clay_Color>(io->theme->fillColor2), .width = CLAY_BORDER_OUTSIDE(1)}
2025-07-06 20:43:53 +03:00
}) {
if(Clay_Hovered())
io->hoverObstructed = true;
elemUpdate();
}
}
void GUIManager::left_to_right_layout(Clay_SizingAxis x, Clay_SizingAxis y, const std::function<void()>& elemUpdate) {
CLAY({.layout = {
.sizing = {.width = x, .height = y },
.childGap = io->theme->childGap1,
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER},
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
}) {
elemUpdate();
}
}
void GUIManager::left_to_right_line_layout(const std::function<void()>& elemUpdate) {
left_to_right_layout(CLAY_SIZING_GROW(0), CLAY_SIZING_FIT(0), elemUpdate);
}
void GUIManager::left_to_right_line_centered_layout(const std::function<void()>& elemUpdate) {
CLAY({.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
.childGap = io->theme->childGap1,
.childAlignment = {.x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER},
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
}) {
elemUpdate();
}
}
2025-07-06 20:43:53 +03:00
void GUIManager::text_label_size(const std::string& val, float modifier) {
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io->theme->frontColor1), .fontSize = static_cast<uint16_t>(io->fontSize * modifier)}));
}
void GUIManager::text_label_color(const std::string& val, const SkColor4f& color) {
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(color), .fontSize = io->fontSize }));
}
2025-11-01 18:07:39 +03:00
void GUIManager::text_label_light(const std::string& val) {
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io->theme->frontColor2), .fontSize = io->fontSize }));
}
2025-07-06 20:43:53 +03:00
void GUIManager::text_label(const std::string& val) {
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io->theme->frontColor1), .fontSize = io->fontSize }));
}
void GUIManager::text_label_centered(const std::string& val) {
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
.childAlignment = {.x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER},
}
}) {
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io->theme->frontColor1), .fontSize = io->fontSize, .textAlignment = CLAY_TEXT_ALIGN_CENTER}));
2025-07-06 20:43:53 +03:00
}
}
2025-11-01 18:07:39 +03:00
void GUIManager::text_label_light_centered(const std::string& val) {
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
.childAlignment = {.x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER},
}
}) {
CLAY_TEXT(strArena.std_str_to_clay_str(val), CLAY_TEXT_CONFIG({.textColor = convert_vec4<Clay_Color>(io->theme->frontColor2), .fontSize = io->fontSize, .textAlignment = CLAY_TEXT_ALIGN_CENTER}));
2025-11-01 18:07:39 +03:00
}
}
2025-07-06 20:43:53 +03:00
bool GUIManager::radio_button(const std::string& id, bool val, const std::function<void()>& elemUpdate) {
push_id(id);
RadioButton* e = insert_element<RadioButton>();
e->update(*io, val, elemUpdate);
pop_id();
return e->selection.clicked;
}
void GUIManager::checkbox(const std::string& id, bool* val, const std::function<void()>& elemUpdate) {
push_id(id);
CheckBox* e = insert_element<CheckBox>();
e->update(*io, *val, elemUpdate);
if(e->selection.clicked)
*val = !(*val);
pop_id();
}
2025-11-08 20:50:23 +03:00
void GUIManager::input_text_field(const std::string& id, const std::string& name, std::string* val, bool updateEveryEdit, const std::function<void(SelectionHelper&)>& elemUpdate) {
2025-07-06 20:43:53 +03:00
left_to_right_line_layout([&]() {
text_label(name);
2025-11-08 20:50:23 +03:00
input_text(id, val, updateEveryEdit, elemUpdate);
2025-07-06 20:43:53 +03:00
});
}
void GUIManager::input_path_field(const std::string& id, const std::string& name, std::filesystem::path* val, std::filesystem::file_type fileTypeRestriction, const std::function<void(SelectionHelper&)>& elemUpdate) {
2025-07-06 20:43:53 +03:00
left_to_right_line_layout([&]() {
text_label(name);
input_path(id, val, fileTypeRestriction, elemUpdate);
});
}
void GUIManager::checkbox_field(const std::string& id, const std::string& name, bool* val, const std::function<void()>& elemUpdate) {
left_to_right_line_layout([&]() {
checkbox(id, val, elemUpdate);
text_label(name);
});
}
bool GUIManager::radio_button_field(const std::string& id, const std::string& name, bool val, const std::function<void()>& elemUpdate) {
bool toRet;
left_to_right_line_layout([&]() {
toRet = radio_button(id, val, elemUpdate);
text_label(name);
});
return toRet;
}
bool GUIManager::input_color_component_255(const std::string& id, float* val, const std::function<void(SelectionHelper&)>& elemUpdate) {
2025-07-06 20:43:53 +03:00
push_id(id);
bool isUpdating = insert_element<TextBox<float>>()->update(*io, val,
2025-07-06 20:43:53 +03:00
[&](const std::string& str) {
int roundTo255;
std::stringstream ss;
ss << str;
ss >> roundTo255;
return std::clamp<float>(static_cast<float>(roundTo255) / 255.0f, 0.0f, 1.0f);
},
[&](const float& colorFloat) {
int convertTo255 = colorFloat * 255;
std::stringstream ss;
ss << convertTo255;
return ss.str();
2025-09-06 19:06:30 +03:00
}, true, false, elemUpdate);
2025-07-06 20:43:53 +03:00
pop_id();
return isUpdating;
2025-07-06 20:43:53 +03:00
}
2025-11-08 20:50:23 +03:00
bool GUIManager::input_text(const std::string& id, std::string* val, bool updateEveryEdit, const std::function<void(SelectionHelper&)>& elemUpdate) {
2025-07-06 20:43:53 +03:00
push_id(id);
2025-11-08 20:50:23 +03:00
bool toRet = insert_element<TextBox<std::string>>()->update(*io, val,
2025-07-06 20:43:53 +03:00
[&](const std::string& str) {
return str;
},
[&](const std::string& str) {
return str;
2025-11-08 20:50:23 +03:00
}, true, updateEveryEdit, elemUpdate);
2025-07-06 20:43:53 +03:00
pop_id();
2025-11-08 20:50:23 +03:00
return toRet;
2025-07-06 20:43:53 +03:00
}
bool GUIManager::selectable_button(const std::string& id, const std::function<void(SelectionHelper&, bool)>& elemUpdate, GUIStuff::SelectableButton::DrawType drawType, bool isSelected) {
2025-07-06 20:43:53 +03:00
push_id(id);
SelectableButton* e = insert_element<SelectableButton>();
e->update(*io, drawType, elemUpdate, isSelected);
2025-07-06 20:43:53 +03:00
bool toRet = e->selection.clicked;
pop_id();
return toRet;
}
void GUIManager::svg_icon(const std::string& id, const std::string& svgPath, bool isHighlighted, const std::function<void()>& elemUpdate) {
push_id(id);
insert_element<SVGIcon>()->update(*io, svgPath, isHighlighted, elemUpdate);
pop_id();
}
bool GUIManager::svg_icon_button(const std::string& id, const std::string& svgPath, bool isSelected, float size, bool hasBorder, const std::function<void()>& elemUpdate) {
2025-07-06 20:43:53 +03:00
bool toRet = false;
push_id(id);
CLAY ({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(size), .height = CLAY_SIZING_FIXED(size) } } }) {
toRet = selectable_button("0", [&](SelectionHelper& s, bool is) {
svg_icon("1", svgPath, is || s.held || s.hovered);
if(elemUpdate)
elemUpdate();
}, SelectableButton::DrawType::FILLED, isSelected);
}
pop_id();
return toRet;
}
bool GUIManager::svg_icon_button_transparent(const std::string& id, const std::string& svgPath, bool isSelected, float size, bool hasBorder, const std::function<void()>& elemUpdate) {
bool toRet = false;
push_id(id);
CLAY ({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(size), .height = CLAY_SIZING_FIXED(size) } } }) {
toRet = selectable_button("0", [&](SelectionHelper& s, bool is) {
svg_icon("1", svgPath, is || s.held || s.hovered);
if(elemUpdate)
elemUpdate();
2025-11-18 21:39:14 +03:00
}, SelectableButton::DrawType::TRANSPARENT_ALL, isSelected);
2025-07-06 20:43:53 +03:00
}
pop_id();
return toRet;
}
bool GUIManager::text_button_sized(const std::string& id, const std::string& text, Clay_SizingAxis x, Clay_SizingAxis y, bool isSelected, const std::function<void()>& elemUpdate) {
bool toRet = false;
CLAY ({.layout = {.sizing = {.width = x, .height = y } } }) {
toRet = selectable_button(id, [&](SelectionHelper& s, bool iS) {
text_label(text);
if(elemUpdate)
elemUpdate();
}, SelectableButton::DrawType::FILLED, isSelected);
2025-07-06 20:43:53 +03:00
}
return toRet;
}
bool GUIManager::text_button(const std::string& id, const std::string& text, bool isSelected, const std::function<void()>& elemUpdate) {
bool toRet = false;
CLAY ({.layout = {.sizing = {.width = CLAY_SIZING_FIT(0), .height = CLAY_SIZING_FIT(0) } } }) {
toRet = selectable_button(id, [&](SelectionHelper& s, bool iS) {
text_label(text);
if(elemUpdate)
elemUpdate();
}, SelectableButton::DrawType::FILLED, isSelected);
2025-07-06 20:43:53 +03:00
}
return toRet;
}
bool GUIManager::text_button_wide(const std::string& id, const std::string& text, bool isSelected, const std::function<void()>& elemUpdate) {
bool toRet = false;
CLAY ({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
.childAlignment = {.x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER},
}}) {
2025-07-06 20:43:53 +03:00
toRet = selectable_button(id, [&](SelectionHelper& s, bool iS) {
text_label(text);
if(elemUpdate)
elemUpdate();
}, SelectableButton::DrawType::FILLED, isSelected);
}
return toRet;
}
bool GUIManager::text_button_left_transparent(const std::string& id, const std::string& text, bool isSelected, const std::function<void()>& elemUpdate) {
bool toRet = false;
CLAY ({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER},
}}) {
toRet = selectable_button(id, [&](SelectionHelper& s, bool iS) {
CLAY ({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}
}}) {
text_label(text);
if(elemUpdate)
elemUpdate();
}
2025-11-18 21:39:14 +03:00
}, SelectableButton::DrawType::TRANSPARENT_ALL, isSelected);
2025-07-06 20:43:53 +03:00
}
return toRet;
}
2025-11-05 14:55:22 +03:00
bool GUIManager::input_scalar(const std::string& id, uint8_t* val, uint8_t min, uint8_t max, int decimalPrecision, const std::function<void(SelectionHelper&)>& elemUpdate) {
bool isUpdating = false;
2025-07-06 20:43:53 +03:00
push_id(id);
2025-11-05 14:55:22 +03:00
isUpdating = insert_element<TextBox<uint8_t>>()->update(*io, val,
2025-07-06 20:43:53 +03:00
[&](const std::string& a) {
if(a.empty())
return min;
uint32_t toRet;
std::stringstream ss;
ss << a;
ss >> toRet;
if(ss.fail())
return min;
return std::clamp<uint8_t>(toRet, min, max);
},
[&](const uint8_t& a) {
return std::to_string(static_cast<uint32_t>(a));
2025-09-06 19:06:30 +03:00
}, true, false, elemUpdate);
2025-07-06 20:43:53 +03:00
pop_id();
2025-11-05 14:55:22 +03:00
return isUpdating;
2025-07-06 20:43:53 +03:00
}
std::string GUIManager::get_clay_unique_id() {
std::string toRet = "CLAY_UNIQUE_ID_" + std::to_string(clayUniqueIDCounter++);
return toRet;
}
void GUIManager::input_path(const std::string& id, std::filesystem::path* val, std::filesystem::file_type fileTypeRestriction, const std::function<void(SelectionHelper&)>& elemUpdate) {
2025-07-06 20:43:53 +03:00
push_id(id);
insert_element<TextBox<std::filesystem::path>>()->update(*io, val,
[&](const std::string& a) {
std::filesystem::path toRet = std::filesystem::path(std::u8string_view(reinterpret_cast<const char8_t*>(a.c_str()), a.length()));
2025-07-06 20:43:53 +03:00
if(fileTypeRestriction != std::filesystem::file_type::none && std::filesystem::status(toRet).type() != fileTypeRestriction)
return std::optional<std::filesystem::path>(std::nullopt);
return std::optional<std::filesystem::path>(toRet);
},
[&](const std::filesystem::path& a) {
return a.string();
2025-09-06 19:06:30 +03:00
}, true, false, elemUpdate);
2025-07-06 20:43:53 +03:00
pop_id();
}
void GUIManager::tab_list(const std::string& id, const std::vector<std::pair<std::string, std::string>>& tabNames, size_t& selectedTab, std::optional<size_t>& closedTab, const std::function<void()>& elemUpdate) {
push_id(id);
insert_element<MovableTabList>()->update(*io, tabNames, selectedTab, closedTab, elemUpdate);
pop_id();
}
2025-11-08 20:50:23 +03:00
bool GUIManager::font_picker(const std::string& id, std::string* fontName) {
push_id(id);
bool toRet = insert_element<FontPicker>()->update(*io, fontName, this);
pop_id();
return toRet;
}
void GUIManager::dropdown_select(const std::string& id, size_t* val, const std::vector<std::string>& selections, float width, const std::function<void()>& hoverboxElemUpdate) {
2025-07-06 20:43:53 +03:00
push_id(id);
struct DropdownData {
std::string uniqueID;
bool isOpen = false;
};
DropdownData& dropdownData = insert_any_with_id_with_function<DropdownData>(0, [&]() {
return DropdownData {
.uniqueID = get_clay_unique_id()
};
});
2025-07-06 20:43:53 +03:00
left_to_right_layout(CLAY_SIZING_FIXED(width), CLAY_SIZING_FIT(0), [&]() {
bool click = selectable_button(id, [&](SelectionHelper& s, bool iS) {
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0) },
.padding = {.left = 4, .right = 4},
.childGap = io->theme->childGap1,
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER},
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
}) {
text_label(selections[*val]);
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}}) {}
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_FIT(SMALL_BUTTON_SIZE), .height = CLAY_SIZING_FIT(SMALL_BUTTON_SIZE)}
2025-07-06 20:43:53 +03:00
}
}) {
svg_icon("dropico", "data/icons/droparrow.svg", dropdownData.isOpen);
2025-07-06 20:43:53 +03:00
}
}
}, SelectableButton::DrawType::FILLED, dropdownData.isOpen);
if(dropdownData.isOpen) {
Clay_ElementId clayID = Clay_GetElementId(strArena.std_str_to_clay_str(dropdownData.uniqueID));
Clay_ElementData dropdownElemData = Clay_GetElementData(clayID);
float calculatedDropdownMaxHeight = 0.0f;
if(dropdownElemData.found)
calculatedDropdownMaxHeight = std::max(windowSize.y() - dropdownElemData.boundingBox.y - 2.0f, 0.0f);
2025-07-06 20:43:53 +03:00
CLAY({
.id = clayID,
2025-07-06 20:43:53 +03:00
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(width), .height = CLAY_SIZING_FIT(0, calculatedDropdownMaxHeight)},
.childGap = 0
2025-07-06 20:43:53 +03:00
},
2025-11-01 20:47:34 +03:00
.backgroundColor = convert_vec4<Clay_Color>(io->theme->backColor1),
2025-07-06 20:43:53 +03:00
.cornerRadius = CLAY_CORNER_RADIUS(4),
.floating = {
.offset = {
.y = 4
},
.attachPoints = {
.element = CLAY_ATTACH_POINT_LEFT_TOP,
.parent = CLAY_ATTACH_POINT_LEFT_BOTTOM
},
.attachTo = CLAY_ATTACH_TO_PARENT
},
.border = {
2025-11-01 20:47:34 +03:00
.color = convert_vec4<Clay_Color>(io->theme->fillColor2),
.width = CLAY_BORDER_OUTSIDE(1)
2025-07-06 20:43:53 +03:00
}
}) {
obstructing_window();
scroll_bar_many_entries_area("dropdown scroll area", 18.0f, selections.size(), true, [&](size_t i, bool) {
bool selectedEntry = *val == i;
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(250), .height = CLAY_SIZING_FIXED(18)},
.padding = CLAY_PADDING_ALL(0),
.childGap = 0,
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER},
.layoutDirection = CLAY_TOP_TO_BOTTOM
}
}) {
SkColor4f entryColor;
if(selectedEntry)
entryColor = io->theme->backColor2;
else if(Clay_Hovered())
entryColor = io->theme->backColor2;
else
entryColor = io->theme->backColor1;
2025-07-06 20:43:53 +03:00
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
.padding = {.left = 2, .right = 2, .top = 0, .bottom = 0},
.childGap = 0,
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}
2025-07-06 20:43:53 +03:00
},
.backgroundColor = convert_vec4<Clay_Color>(entryColor)
2025-07-06 20:43:53 +03:00
}) {
text_label(selections[i]);
if(io->mouse.leftClick && Clay_Hovered()) {
*val = i;
dropdownData.isOpen = false;
2025-07-06 20:43:53 +03:00
}
}
}
});
2025-07-06 20:43:53 +03:00
if(hoverboxElemUpdate)
hoverboxElemUpdate();
}
}
if(click)
dropdownData.isOpen = !dropdownData.isOpen;
2025-07-06 20:43:53 +03:00
});
pop_id();
}
void GUIManager::scroll_bar_area(const std::string& id, bool clipHorizontal, const std::function<void(float, float, float&)>& elemUpdate) {
push_id(id);
2025-07-06 20:43:53 +03:00
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_LEFT_TO_RIGHT
}
}) {
struct ScrollAreaData {
float currentScrollPos = 0.0f;
bool isMoving = false;
2025-11-09 12:55:35 +03:00
float scrollerStartPos;
float mouseStartPos;
2025-07-06 20:43:53 +03:00
float contentDimensions = 100.0f;
float containerDimensions = 100.0f;
2025-11-08 20:50:23 +03:00
std::string uniqueID;
2025-07-06 20:43:53 +03:00
};
2025-11-08 20:50:23 +03:00
ScrollAreaData& sD = insert_any_with_id_with_function<ScrollAreaData>(0, [&](){
return ScrollAreaData{
.uniqueID = get_clay_unique_id()
};
2025-11-08 20:50:23 +03:00
});
2025-07-06 20:43:53 +03:00
2025-11-08 20:50:23 +03:00
Clay_ElementId clayID = Clay_GetElementId(strArena.std_str_to_clay_str(sD.uniqueID));
2025-07-06 20:43:53 +03:00
CLAY({
.id = clayID,
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.clip = {.horizontal = clipHorizontal, .vertical = true, .childOffset = {.x = 0, .y = Clay_GetScrollOffset().y}}
2025-07-06 20:43:53 +03:00
}) {
push_id(1);
2025-07-06 20:43:53 +03:00
elemUpdate(sD.contentDimensions, sD.containerDimensions, sD.currentScrollPos);
pop_id();
2025-07-06 20:43:53 +03:00
}
// Make sure you are accessing this data after the element has been created
Clay_ScrollContainerData scrollData = Clay_GetScrollContainerData(clayID);
Clay_BoundingBox scrollAreaBB = Clay_GetElementData(clayID).boundingBox;
sD.contentDimensions = scrollData.contentDimensions.height;
sD.containerDimensions = scrollData.scrollContainerDimensions.height;
if(scrollData.contentDimensions.height > scrollData.scrollContainerDimensions.height) {
float sAreaDim = scrollData.scrollContainerDimensions.height;
float contDim = scrollData.contentDimensions.height;
float scrollerSize = (sAreaDim / contDim) * sAreaDim;
float scrollPosMax = (contDim - sAreaDim) + 3.0f; // NOTE: Adding + 3.0f to prevent bug with scroller that pushes everything under it when its at the bottom of the scroll area
float scrollerPos = std::fabs(scrollData.scrollPosition->y / scrollPosMax);
float areaAboveScrollerSize = scrollerPos * (sAreaDim - scrollerSize);
if(Clay_Hovered())
sD.currentScrollPos += io->mouse.scroll.y() * io->deltaTime * 3000.0f;
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(12), .height = CLAY_SIZING_GROW(0)},
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
2025-11-09 12:55:35 +03:00
.backgroundColor = convert_vec4<Clay_Color>(io->theme->backColor2)
2025-07-06 20:43:53 +03:00
}) {
SkColor4f scrollerColor;
if(sD.isMoving)
scrollerColor = io->theme->fillColor1;
else if(Clay_Hovered())
2025-11-09 12:55:35 +03:00
scrollerColor = io->theme->fillColor1;
2025-07-06 20:43:53 +03:00
else
2025-11-09 12:55:35 +03:00
scrollerColor = io->theme->fillColor2;
2025-07-06 20:43:53 +03:00
2025-11-09 12:55:35 +03:00
bool isHoveringOverScroller = false;
2025-07-06 20:43:53 +03:00
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(areaAboveScrollerSize)}
}
}) {}
CLAY({
.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(scrollerSize)}},
.backgroundColor = convert_vec4<Clay_Color>(scrollerColor),
.cornerRadius = CLAY_CORNER_RADIUS(3),
}) {
2025-11-09 12:55:35 +03:00
if(Clay_Hovered())
isHoveringOverScroller = true;
2025-07-06 20:43:53 +03:00
}
CLAY({ .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}}) {}
2025-11-09 12:55:35 +03:00
if(Clay_Hovered() && io->mouse.leftClick) {
2025-07-06 20:43:53 +03:00
sD.isMoving = true;
if(isHoveringOverScroller)
2025-11-09 12:55:35 +03:00
sD.scrollerStartPos = scrollAreaBB.y + areaAboveScrollerSize + scrollerSize * 0.5f;
else
sD.scrollerStartPos = std::clamp(io->mouse.pos.y(), scrollAreaBB.y + scrollerSize * 0.5f, scrollAreaBB.y + scrollAreaBB.height - scrollerSize * 0.5f);
sD.mouseStartPos = io->mouse.pos.y();
2025-11-09 12:55:35 +03:00
}
2025-07-06 20:43:53 +03:00
if(!io->mouse.leftHeld)
sD.isMoving = false;
2025-11-09 12:55:35 +03:00
if(sD.isMoving) {
float newScrollPosFrac;
newScrollPosFrac = std::clamp((sD.scrollerStartPos - (sD.mouseStartPos - io->mouse.pos.y()) - scrollAreaBB.y - scrollerSize * 0.5f) / (scrollAreaBB.height - scrollerSize), 0.0f, 1.0f);
2025-11-09 12:55:35 +03:00
sD.currentScrollPos = newScrollPosFrac * (-scrollPosMax);
}
2025-07-06 20:43:53 +03:00
sD.currentScrollPos = std::clamp(sD.currentScrollPos, -scrollPosMax, 0.0f);
scrollData.scrollPosition->y = sD.currentScrollPos;
}
}
else
sD.currentScrollPos = 0.0f;
}
pop_id();
2025-07-06 20:43:53 +03:00
}
void GUIManager::scroll_bar_many_entries_area(const std::string& id, float entryHeight, size_t entryCount, bool clipHorizontal, const std::function<void(size_t, bool)>& entryUpdate, const std::function<void(float, float, float&)>& elemUpdate) {
2025-11-08 20:50:23 +03:00
push_id(id);
scroll_bar_area(id, clipHorizontal, [&](float scrollContentHeight, float containerHeight, float& scrollAmount) {
2025-07-06 20:43:53 +03:00
if(elemUpdate)
elemUpdate(scrollContentHeight, containerHeight, scrollAmount);
bool listHovered = Clay_Hovered();
2025-11-08 20:50:23 +03:00
float absScrollAmount = std::fabs(scrollAmount);
size_t startPoint = absScrollAmount / entryHeight;
2025-11-04 10:58:26 +03:00
size_t elementsContainable = (containerHeight / entryHeight) + 2; // Displaying 2 more elements ensures that there isn't any empty space in the container
2025-07-06 20:43:53 +03:00
size_t endPoint = std::min(entryCount, startPoint + elementsContainable);
if(startPoint != 0) {
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(startPoint * entryHeight)},
}
}) { }
}
for(size_t i = startPoint; i < endPoint; i++) {
push_id(i);
entryUpdate(i, listHovered);
pop_id();
}
if(endPoint != entryCount) {
CLAY({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED((entryCount - endPoint) * entryHeight)},
}
}) { }
}
});
pop_id();
}
2025-09-28 16:32:57 +03:00
bool GUIManager::rotate_wheel(const std::string& id, double* angle, float size, const std::function<void()>& elemUpdate) {
bool toRet = false;
push_id(id);
CLAY ({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(size), .height = CLAY_SIZING_FIXED(size) } } }) {
RotateWheel* e = insert_element<RotateWheel>();
e->update(*io, angle, elemUpdate);
toRet = e->selection.clicked;
}
pop_id();
return toRet;
}
void GUIManager::text_paragraph(const std::string& id, std::unique_ptr<skia::textlayout::Paragraph> paragraph, float width, const std::function<void()>& elemUpdate) {
push_id(id);
TextParagraph* e = insert_element<TextParagraph>();
e->update(*io, std::move(paragraph), width, elemUpdate);
pop_id();
}
2025-07-06 20:43:53 +03:00
void GUIManager::paint_circle_popup_menu(const std::string& id, const Vector2f& centerPos, const PaintCircleMenu::Data& val, const std::function<void()>& elemUpdate) {
float size = 300.0f;
push_id(id);
CLAY({.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(size), .height = CLAY_SIZING_FIXED(size)}
},
.floating = {
.offset = {centerPos.x() - size * 0.5f, centerPos.y() - size * 0.5f},
.zIndex = -1,
.attachTo = CLAY_ATTACH_TO_ROOT
}
}) {
PaintCircleMenu* e = insert_element<PaintCircleMenu>();
e->update(*io, val, elemUpdate);
}
pop_id();
}
2025-12-01 17:33:14 +03:00
bool GUIManager::action_list_popup_menu(const std::string& id, Vector2f popupPos, const std::vector<std::pair<std::string, std::function<void()>>>& actions) {
push_id(id);
std::string& uniqueID = insert_any_with_id_with_function<std::string>(0, [&]() {
return get_clay_unique_id();
});
Clay_ElementId clayID = Clay_GetElementId(strArena.std_str_to_clay_str(uniqueID));
Clay_ElementData actionListElemData = Clay_GetElementData(clayID);
if(actionListElemData.found) {
if((popupPos.y() + actionListElemData.boundingBox.height) > windowSize.y()) {
popupPos.y() -= actionListElemData.boundingBox.height;
}
}
bool toRet = false;
CLAY({
.id = clayID,
.layout = {
.sizing = {.width = CLAY_SIZING_FIT(100), .height = CLAY_SIZING_FIT(0)},
.padding = CLAY_PADDING_ALL(io->theme->padding1),
.childGap = 1,
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = convert_vec4<Clay_Color>(io->theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io->theme->windowCorners1),
.floating = {
.offset = {popupPos.x(), popupPos.y()},
.zIndex = -1,
.attachTo = CLAY_ATTACH_TO_ROOT
}
}) {
obstructing_window();
push_id(1);
for(auto& [actionName, actionFunc] : actions) {
if(text_button_left_transparent(actionName, actionName)) {
actionFunc();
toRet = true;
}
}
pop_id();
}
pop_id();
return toRet;
}
2025-07-06 20:43:53 +03:00
void GUIManager::obstructing_window() {
if(Clay_Hovered())
io->hoverObstructed = true;
}
void GUIManager::clay_error_handler(Clay_ErrorData errorData) {
std::cout << "[Clay Error] " << errorData.errorText.chars << std::endl;
}
void GUIManager::push_id(int64_t id) {
idStack.emplace_back(id);
}
void GUIManager::push_id(const std::string& id) {
idStack.emplace_back(id);
}
void GUIManager::pop_id() {
idStack.pop_back();
}
}