space-game001/src/UiManager.cpp
2026-03-05 23:58:35 +03:00

1280 lines
38 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "UiManager.h"
#include "utils/Utils.h"
#include "render/TextRenderer.h"
#include <fstream>
#include <iostream>
#include <algorithm>
namespace ZL {
using json = nlohmann::json;
static float applyEasing(const std::string& easing, float t) {
if (easing == "easein") {
return t * t;
}
else if (easing == "easeout") {
float inv = 1.0f - t;
return 1.0f - inv * inv;
}
return t;
}
void UiButton::buildMesh() {
mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear();
float x0 = rect.x;
float y0 = rect.y;
float x1 = rect.x + rect.w;
float y1 = rect.y + rect.h;
mesh.data.PositionData.push_back({ x0, y0, 0 });
mesh.data.TexCoordData.push_back({ 0, 0 });
mesh.data.PositionData.push_back({ x0, y1, 0 });
mesh.data.TexCoordData.push_back({ 0, 1 });
mesh.data.PositionData.push_back({ x1, y1, 0 });
mesh.data.TexCoordData.push_back({ 1, 1 });
mesh.data.PositionData.push_back({ x0, y0, 0 });
mesh.data.TexCoordData.push_back({ 0, 0 });
mesh.data.PositionData.push_back({ x1, y1, 0 });
mesh.data.TexCoordData.push_back({ 1, 1 });
mesh.data.PositionData.push_back({ x1, y0, 0 });
mesh.data.TexCoordData.push_back({ 1, 0 });
mesh.RefreshVBO();
}
void UiButton::draw(Renderer& renderer) const {
if (!texNormal) return;
const std::shared_ptr<Texture>* tex = &texNormal;
switch (state) {
case ButtonState::Normal: tex = &texNormal; break;
case ButtonState::Hover: tex = &texHover; break;
case ButtonState::Pressed: tex = &texPressed; break;
case ButtonState::Disabled: tex = &texDisabled; break;
}
if (!(*tex)) return;
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.PushMatrix();
renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f });
renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f });
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID());
renderer.DrawVertexRenderStruct(mesh);
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.PopMatrix();
}
void UiSlider::buildTrackMesh() {
trackMesh.data.PositionData.clear();
trackMesh.data.TexCoordData.clear();
float x0 = rect.x;
float y0 = rect.y;
float x1 = rect.x + rect.w;
float y1 = rect.y + rect.h;
trackMesh.data.PositionData.push_back({ x0, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 0, 0 });
trackMesh.data.PositionData.push_back({ x0, y1, 0 });
trackMesh.data.TexCoordData.push_back({ 0, 1 });
trackMesh.data.PositionData.push_back({ x1, y1, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 1 });
trackMesh.data.PositionData.push_back({ x0, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 0, 0 });
trackMesh.data.PositionData.push_back({ x1, y1, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 1 });
trackMesh.data.PositionData.push_back({ x1, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 0 });
trackMesh.RefreshVBO();
}
void UiSlider::buildKnobMesh() {
knobMesh.data.PositionData.clear();
knobMesh.data.TexCoordData.clear();
float kw = vertical ? rect.w * 4.0f : rect.w * 0.5f;
float kh = vertical ? rect.w * 4.0f : rect.h * 0.5f;
float cx = rect.x + rect.w * 0.5f;
float cy = rect.y + (vertical ? (value * rect.h) : (rect.h * 0.5f));
float x0 = cx - kw * 0.5f;
float y0 = cy - kh * 0.5f;
float x1 = cx + kw * 0.5f;
float y1 = cy + kh * 0.5f;
knobMesh.data.PositionData.push_back({ x0, y0, 0 });
knobMesh.data.TexCoordData.push_back({ 0, 0 });
knobMesh.data.PositionData.push_back({ x0, y1, 0 });
knobMesh.data.TexCoordData.push_back({ 0, 1 });
knobMesh.data.PositionData.push_back({ x1, y1, 0 });
knobMesh.data.TexCoordData.push_back({ 1, 1 });
knobMesh.data.PositionData.push_back({ x0, y0, 0 });
knobMesh.data.TexCoordData.push_back({ 0, 0 });
knobMesh.data.PositionData.push_back({ x1, y1, 0 });
knobMesh.data.TexCoordData.push_back({ 1, 1 });
knobMesh.data.PositionData.push_back({ x1, y0, 0 });
knobMesh.data.TexCoordData.push_back({ 1, 0 });
knobMesh.RefreshVBO();
}
void UiSlider::draw(Renderer& renderer) const {
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
if (texTrack) {
glBindTexture(GL_TEXTURE_2D, texTrack->getTexID());
renderer.DrawVertexRenderStruct(trackMesh);
}
if (texKnob) {
glBindTexture(GL_TEXTURE_2D, texKnob->getTexID());
renderer.DrawVertexRenderStruct(knobMesh);
}
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
}
void UiTextField::draw(Renderer& renderer) const {
if (textRenderer) {
float textX = rect.x + 10.0f;
float textY = rect.y + rect.h / 2.0f;
if (text.empty()) {
textRenderer->drawText(placeholder, textX, textY, 1.0f, false, placeholderColor);
}
else {
textRenderer->drawText(text, textX, textY, 1.0f, false, color);
}
}
}
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
auto node = std::make_shared<UiNode>();
// 1. Определяем тип контейнера и ориентацию
std::string typeStr = j.value("type", "FrameLayout"); // По умолчанию FrameLayout
if (typeStr == "LinearLayout") {
node->layoutType = LayoutType::Linear;
}
else {
node->layoutType = LayoutType::Frame;
}
if (j.contains("name")) node->name = j["name"].get<std::string>();
// 2. Читаем размеры во временные "локальные" поля
// Это критически важно: мы не пишем сразу в screenRect,
// так как LinearLayout их пересчитает.
node->localX = j.value("x", 0.0f);
node->localY = j.value("y", 0.0f);
if (j.contains("width")) {
if (j["width"].is_string() && j["width"] == "match_parent") {
node->width = -1.0f; // Наш маркер для match_parent
}
else {
node->width = j["width"].get<float>();
}
}
else
{
node->width = 0.0f;
}
if (j.contains("height")) {
if (j["height"].is_string() && j["height"] == "match_parent") {
node->height = -1.0f; // Наш маркер для match_parent
}
else {
node->height = j["height"].get<float>();
}
}
else
{
node->height = 0.0f;
}
// 3. Параметры компоновки
if (j.contains("orientation")) {
std::string orient = j["orientation"].get<std::string>();
node->orientation = (orient == "horizontal") ? Orientation::Horizontal : Orientation::Vertical;
}
node->spacing = j.value("spacing", 0.0f);
if (j.contains("horizontal_align")) {
std::string halign = j["horizontal_align"];
if (halign == "center") node->layoutSettings.hAlign = HorizontalAlign::Center;
else if (halign == "right") node->layoutSettings.hAlign = HorizontalAlign::Right;
}
if (j.contains("vertical_align")) {
std::string valign = j["vertical_align"];
if (valign == "center") node->layoutSettings.vAlign = VerticalAlign::Center;
else if (valign == "bottom") node->layoutSettings.vAlign = VerticalAlign::Bottom;
}
if (j.contains("horizontal_gravity")) {
std::string hg = j["horizontal_gravity"].get<std::string>();
if (hg == "right") node->layoutSettings.hGravity = HorizontalGravity::Right;
else node->layoutSettings.hGravity = HorizontalGravity::Left;
}
// Читаем Vertical Gravity
if (j.contains("vertical_gravity")) {
std::string vg = j["vertical_gravity"].get<std::string>();
if (vg == "bottom") node->layoutSettings.vGravity = VerticalGravity::Bottom;
else node->layoutSettings.vGravity = VerticalGravity::Top;
}
// Подготавливаем базовый rect для компонентов (кнопок и т.д.)
// На этапе парсинга мы даем им "желаемый" размер
UiRect initialRect = { node->localX, node->localY, node->width, node->height };
if (typeStr == "Button") {
auto btn = std::make_shared<UiButton>();
btn->name = node->name;
btn->rect = initialRect;
if (!j.contains("textures") || !j["textures"].is_object()) {
std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl;
throw std::runtime_error("UI button textures missing");
}
auto t = j["textures"];
auto loadTex = [&](const std::string& key)->std::shared_ptr<Texture> {
if (!t.contains(key) || !t[key].is_string()) return nullptr;
std::string path = t[key].get<std::string>();
try {
std::cout << "UiManager: loading texture for button '" << btn->name << "' : " << path << " Zip file: " << zipFile << std::endl;
auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
return std::make_shared<Texture>(data);
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed load texture " << path << " : " << e.what() << std::endl;
throw std::runtime_error("UI texture load failed: " + path);
}
};
btn->texNormal = loadTex("normal");
btn->texHover = loadTex("hover");
btn->texPressed = loadTex("pressed");
btn->texDisabled = loadTex("disabled");
btn->border = j.value("border", 0.0f);
node->button = btn;
}
else if (typeStr == "Slider") {
auto s = std::make_shared<UiSlider>();
s->name = node->name;
s->rect = initialRect;
if (!j.contains("textures") || !j["textures"].is_object()) {
std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl;
throw std::runtime_error("UI slider textures missing");
}
auto t = j["textures"];
auto loadTex = [&](const std::string& key)->std::shared_ptr<Texture> {
if (!t.contains(key) || !t[key].is_string()) return nullptr;
std::string path = t[key].get<std::string>();
try {
std::cout << "UiManager: --loading texture for slider '" << s->name << "' : " << path << " Zip file: " << zipFile << std::endl;
auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
return std::make_shared<Texture>(data);
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed load texture " << path << " : " << e.what() << std::endl;
throw std::runtime_error("UI texture load failed: " + path);
}
};
s->texTrack = loadTex("track");
s->texKnob = loadTex("knob");
if (j.contains("value")) s->value = j["value"].get<float>();
if (j.contains("orientation")) {
std::string orient = j["orientation"].get<std::string>();
std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower);
s->vertical = (orient != "horizontal");
}
node->slider = s;
}
else if (typeStr == "TextField") {
auto tf = std::make_shared<UiTextField>();
tf->name = node->name;
tf->rect = initialRect;
if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get<std::string>();
if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get<std::string>();
if (j.contains("fontSize")) tf->fontSize = j["fontSize"].get<int>();
if (j.contains("maxLength")) tf->maxLength = j["maxLength"].get<int>();
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tf->color[i] = j["color"][i].get<float>();
}
}
if (j.contains("placeholderColor") && j["placeholderColor"].is_array() && j["placeholderColor"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tf->placeholderColor[i] = j["placeholderColor"][i].get<float>();
}
}
if (j.contains("backgroundColor") && j["backgroundColor"].is_array() && j["backgroundColor"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tf->backgroundColor[i] = j["backgroundColor"][i].get<float>();
}
}
if (j.contains("borderColor") && j["borderColor"].is_array() && j["borderColor"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tf->borderColor[i] = j["borderColor"][i].get<float>();
}
}
tf->textRenderer = std::make_unique<TextRenderer>();
if (!tf->textRenderer->init(renderer, tf->fontPath, tf->fontSize, zipFile)) {
std::cerr << "Failed to init TextRenderer for TextField: " << tf->name << std::endl;
}
node->textField = tf;
}
if (j.contains("animations") && j["animations"].is_object()) {
for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) {
std::string animName = it.key();
const auto& animDef = it.value();
UiNode::AnimSequence seq;
if (animDef.contains("repeat") && animDef["repeat"].is_boolean()) seq.repeat = animDef["repeat"].get<bool>();
if (animDef.contains("steps") && animDef["steps"].is_array()) {
for (const auto& step : animDef["steps"]) {
UiNode::AnimStep s;
if (step.contains("type") && step["type"].is_string()) {
s.type = step["type"].get<std::string>();
std::transform(s.type.begin(), s.type.end(), s.type.begin(), ::tolower);
}
if (step.contains("to") && step["to"].is_array() && step["to"].size() >= 2) {
s.toX = step["to"][0].get<float>();
s.toY = step["to"][1].get<float>();
}
if (step.contains("duration")) {
s.durationMs = step["duration"].get<float>() * 1000.0f;
}
if (step.contains("easing") && step["easing"].is_string()) {
s.easing = step["easing"].get<std::string>();
std::transform(s.easing.begin(), s.easing.end(), s.easing.begin(), ::tolower);
}
seq.steps.push_back(s);
}
}
node->animations[animName] = std::move(seq);
}
}
if (typeStr == "TextView") {
auto tv = std::make_shared<UiTextView>();
tv->name = node->name;
tv->rect = initialRect;
if (j.contains("text")) tv->text = j["text"].get<std::string>();
if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get<std::string>();
if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get<int>();
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
for (int i = 0; i < 4; ++i) {
tv->color[i] = j["color"][i].get<float>();
}
}
if (j.contains("centered")) tv->centered = j["centered"].get<bool>();
tv->textRenderer = std::make_unique<TextRenderer>();
if (!tv->textRenderer->init(renderer, tv->fontPath, tv->fontSize, zipFile)) {
std::cerr << "Failed to init TextRenderer for TextView: " << tv->name << std::endl;
}
node->textView = tv;
}
if (j.contains("children") && j["children"].is_array()) {
for (const auto& ch : j["children"]) {
node->children.push_back(parseNode(ch, renderer, zipFile));
}
}
return node;
}
std::shared_ptr<UiNode> loadUiFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile)
{
std::shared_ptr<UiNode> root;
std::string content;
try {
if (zipFile.empty()) {
content = readTextFile(path);
}
else {
auto buf = readFileFromZIP(path, zipFile);
if (buf.empty()) {
std::cerr << "UiManager: failed to read " << path << " from zip " << zipFile << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
content.assign(buf.begin(), buf.end());
}
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed to open " << path << " : " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
json j;
try {
j = json::parse(content);
}
catch (const std::exception& e) {
std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
if (!j.contains("root") || !j["root"].is_object()) {
std::cerr << "UiManager: root node missing or invalid" << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
root = parseNode(j["root"], renderer, zipFile);
return root;
}
void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) {
root = newRoot;
layoutNode(
root,
0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0)
Environment::projectionWidth, // parentW
Environment::projectionHeight, // parentH
root->localX, // finalLocalX
root->localY // finalLocalY
);
buttons.clear();
sliders.clear();
textViews.clear();
textFields.clear();
collectButtonsAndSliders(root);
nodeActiveAnims.clear();
for (auto& b : buttons) {
b->buildMesh();
}
for (auto& s : sliders) {
s->buildTrackMesh();
s->buildKnobMesh();
}
}
void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
std::shared_ptr<UiNode> newRoot = loadUiFromFile(path, renderer, zipFile);
replaceRoot(newRoot);
}
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) {
node->screenRect.w = (node->width < 0) ? parentW : node->width;
node->screenRect.h = (node->height < 0) ? parentH : node->height;
// ТЕПЕРЬ используем переданные координаты, а не node->localX напрямую
node->screenRect.x = parentX + finalLocalX;
node->screenRect.y = parentY + finalLocalY;
float currentW = node->screenRect.w;
float currentH = node->screenRect.h;
if (node->layoutType == LayoutType::Linear) {
float totalContentWidth = 0;
float totalContentHeight = 0;
// Предварительный расчет занимаемого места всеми детьми
for (size_t i = 0; i < node->children.size(); ++i) {
if (node->orientation == Orientation::Vertical) {
totalContentHeight += node->children[i]->height;
if (i < node->children.size() - 1) totalContentHeight += node->spacing;
}
else {
totalContentWidth += node->children[i]->width;
if (i < node->children.size() - 1) totalContentWidth += node->spacing;
}
}
float startX = 0;
float startY = currentH;
if (node->orientation == Orientation::Vertical) {
if (node->layoutSettings.vAlign == VerticalAlign::Center) {
startY = (currentH + totalContentHeight) / 2.0f;
}
else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
startY = totalContentHeight;
}
}
// Горизонтальное выравнивание всего блока
if (node->orientation == Orientation::Horizontal) {
if (node->layoutSettings.hAlign == HorizontalAlign::Center) {
startX = (currentW - totalContentWidth) / 2.0f;
}
else if (node->layoutSettings.hAlign == HorizontalAlign::Right) {
startX = currentW - totalContentWidth;
}
}
float cursorX = startX;
float cursorY = startY;
for (auto& child : node->children) {
float childW = (child->width < 0) ? currentW : child->width;
float childH = (child->height < 0) ? currentH : child->height;
if (node->orientation == Orientation::Vertical) {
cursorY -= childH; // используем вычисленный childH
float childX = 0;
float freeSpaceX = currentW - childW;
if (node->layoutSettings.hAlign == HorizontalAlign::Center) childX = freeSpaceX / 2.0f;
else if (node->layoutSettings.hAlign == HorizontalAlign::Right) childX = freeSpaceX;
child->localX = childX;
child->localY = cursorY;
cursorY -= node->spacing;
}
else {
child->localX = cursorX;
// Вертикальное выравнивание внутри "строки" (Cross-axis alignment)
float childY = 0;
float freeSpaceY = currentH - childH;
if (node->layoutSettings.vAlign == VerticalAlign::Center) {
childY = freeSpaceY / 2.0f;
}
else if (node->layoutSettings.vAlign == VerticalAlign::Top) {
childY = freeSpaceY; // Прижимаем к верхнему краю (т.к. Y растет вверх)
}
else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
childY = 0; // Прижимаем к нижнему краю
}
child->localY = childY;
// Сдвигаем курсор вправо для следующего элемента
cursorX += childW + node->spacing;
}
layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, child->localX, child->localY);
}
}
else {
for (auto& child : node->children) {
float childW = (child->width < 0) ? currentW : child->width;
float childH = (child->height < 0) ? currentH : child->height;
float fLX = child->localX;
float fLY = child->localY;
if (child->layoutSettings.hGravity == HorizontalGravity::Right) {
fLX = currentW - childW - child->localX;
}
if (child->layoutSettings.vGravity == VerticalGravity::Top) {
fLY = currentH - childH - child->localY;
}
// Передаем рассчитанные fLX, fLY в рекурсию
layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, fLX, fLY);
}
}
// Обновляем меши визуальных компонентов
syncComponentRects(node);
}
void UiManager::syncComponentRects(const std::shared_ptr<UiNode>& node) {
if (!node) return;
// 1. Обновляем кнопку
if (node->button) {
node->button->rect = node->screenRect;
// Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh
// или при рендеринге через Uniform-переменные матрицы модели.
node->button->buildMesh();
}
// 2. Обновляем слайдер
if (node->slider) {
node->slider->rect = node->screenRect;
node->slider->buildTrackMesh();
node->slider->buildKnobMesh();
}
// 3. Обновляем текстовое поле (TextView)
if (node->textView) {
node->textView->rect = node->screenRect;
// Если в TextView реализован кэш меша для текста, его нужно обновить здесь
// node->textView->rebuildText();
}
// 4. Обновляем поле ввода (TextField)
if (node->textField) {
node->textField->rect = node->screenRect;
// Аналогично для курсора и фонового меша
}
}
void UiManager::updateAllLayouts() {
if (!root) return;
// Запускаем расчет от корня, передавая размеры экрана как "родительские"
layoutNode(
root,
0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0)
Environment::projectionWidth, // parentW
Environment::projectionHeight, // parentH
root->localX, // finalLocalX
root->localY // finalLocalY
);
}
void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
if (node->button) {
buttons.push_back(node->button);
}
if (node->slider) {
sliders.push_back(node->slider);
}
if (node->textView) {
textViews.push_back(node->textView);
}
if (node->textField) {
textFields.push_back(node->textField);
}
for (auto& c : node->children) collectButtonsAndSliders(c);
}
bool UiManager::setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb) {
auto b = findButton(name);
if (!b) {
std::cerr << "UiManager: setButtonCallback failed, button not found: " << name << std::endl;
return false;
}
b->onClick = std::move(cb);
return true;
}
bool UiManager::addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile,
const std::string& trackPath, const std::string& knobPath, float initialValue, bool vertical) {
auto s = std::make_shared<UiSlider>();
s->name = name;
s->rect = rect;
s->value = std::clamp(initialValue, 0.0f, 1.0f);
s->vertical = vertical;
try {
if (!trackPath.empty()) {
auto data = CreateTextureDataFromPng(trackPath.c_str(), zipFile.c_str());
s->texTrack = std::make_shared<Texture>(data);
}
if (!knobPath.empty()) {
auto data = CreateTextureDataFromPng(knobPath.c_str(), zipFile.c_str());
s->texKnob = std::make_shared<Texture>(data);
}
}
catch (const std::exception& e) {
std::cerr << "UiManager: addSlider failed to load textures: " << e.what() << std::endl;
return false;
}
s->buildTrackMesh();
s->buildKnobMesh();
sliders.push_back(s);
return true;
}
std::shared_ptr<UiSlider> UiManager::findSlider(const std::string& name) {
for (auto& s : sliders) if (s->name == name) return s;
return nullptr;
}
bool UiManager::setSliderCallback(const std::string& name, std::function<void(const std::string&, float)> cb) {
auto s = findSlider(name);
if (!s) {
std::cerr << "UiManager: setSliderCallback failed, slider not found: " << name << std::endl;
return false;
}
s->onValueChanged = std::move(cb);
return true;
}
bool UiManager::setSliderValue(const std::string& name, float value) {
auto s = findSlider(name);
if (!s) return false;
value = std::clamp(value, 0.0f, 1.0f);
if (fabs(s->value - value) < 1e-6f) return true;
s->value = value;
s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
return true;
}
std::shared_ptr<UiTextField> UiManager::findTextField(const std::string& name) {
for (auto& tf : textFields) if (tf->name == name) return tf;
return nullptr;
}
bool UiManager::setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb) {
auto tf = findTextField(name);
if (!tf) {
std::cerr << "UiManager: setTextFieldCallback failed, textfield not found: " << name << std::endl;
return false;
}
tf->onTextChanged = std::move(cb);
return true;
}
std::string UiManager::getTextFieldValue(const std::string& name) {
auto tf = findTextField(name);
if (!tf) return "";
return tf->text;
}
bool UiManager::pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot)
{
MenuState prev;
prev.root = root;
prev.buttons = buttons;
prev.sliders = sliders;
prev.textFields = textFields;
prev.pressedButton = pressedButton;
prev.pressedSlider = pressedSlider;
prev.focusedTextField = focusedTextField;
prev.path = "";
prev.animCallbacks = animCallbacks;
try {
nodeActiveAnims.clear();
animCallbacks.clear();
focusedTextField = nullptr;
for (auto& b : buttons) {
if (b) {
b->animOffsetX = 0.0f;
b->animOffsetY = 0.0f;
b->animScaleX = 1.0f;
b->animScaleY = 1.0f;
}
}
replaceRoot(newRoot);
menuStack.push_back(std::move(prev));
return true;
}
catch (const std::exception& e) {
std::cerr << "UiManager: pushMenuFromFile failed to load from root : " << e.what() << std::endl;
animCallbacks = prev.animCallbacks;
return false;
}
}
bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
auto newRoot = loadUiFromFile(path, renderer, zipFile);
return pushMenuFromSavedRoot(newRoot);
}
bool UiManager::popMenu() {
if (menuStack.empty()) {
std::cerr << "UiManager: popMenu called but menu stack is empty" << std::endl;
return false;
}
auto s = menuStack.back();
menuStack.pop_back();
nodeActiveAnims.clear();
root = s.root;
buttons = s.buttons;
sliders = s.sliders;
textFields = s.textFields;
pressedButton = s.pressedButton;
pressedSlider = s.pressedSlider;
focusedTextField = s.focusedTextField;
animCallbacks = s.animCallbacks;
for (auto& b : buttons) {
if (b) {
b->animOffsetX = 0.0f;
b->animOffsetY = 0.0f;
b->animScaleX = 1.0f;
b->animScaleY = 1.0f;
b->buildMesh();
}
}
for (auto& sl : sliders) {
if (sl) {
sl->buildTrackMesh();
sl->buildKnobMesh();
}
}
return true;
}
void UiManager::clearMenuStack() {
menuStack.clear();
}
void UiManager::draw(Renderer& renderer) {
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1);
renderer.PushMatrix();
renderer.LoadIdentity();
for (const auto& b : buttons) {
b->draw(renderer);
}
for (const auto& s : sliders) {
s->draw(renderer);
}
for (const auto& tv : textViews) {
tv->draw(renderer);
}
for (const auto& tf : textFields) {
tf->draw(renderer);
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
}
static std::shared_ptr<UiNode> findNodeByName(const std::shared_ptr<UiNode>& node, const std::string& name) {
if (!node) return nullptr;
if (!name.empty() && node->name == name) return node;
for (auto& c : node->children) {
auto r = findNodeByName(c, name);
if (r) return r;
}
return nullptr;
}
void UiManager::update(float deltaMs) {
if (!root) return;
std::vector<std::pair<std::shared_ptr<UiNode>, size_t>> animationsToRemove;
std::vector<std::function<void()>> pendingCallbacks;
for (auto& kv : nodeActiveAnims) {
auto node = kv.first;
auto& activeList = kv.second;
for (size_t i = 0; i < activeList.size(); ++i) {
auto& act = activeList[i];
if (!act.seq) {
animationsToRemove.push_back({ node, i });
continue;
}
const auto& steps = act.seq->steps;
if (act.stepIndex >= steps.size()) {
if (act.repeat) {
if (node->button) {
node->button->animOffsetX = act.origOffsetX;
node->button->animOffsetY = act.origOffsetY;
node->button->animScaleX = act.origScaleX;
node->button->animScaleY = act.origScaleY;
}
act.stepIndex = 0;
act.elapsedMs = 0.0f;
act.stepStarted = false;
}
else {
if (act.onComplete) {
pendingCallbacks.push_back(act.onComplete);
}
animationsToRemove.push_back({ node, i });
}
continue;
}
const auto& step = steps[act.stepIndex];
if (step.durationMs <= 0.0f) {
if (step.type == "move") {
if (node->button) {
node->button->animOffsetX = step.toX;
node->button->animOffsetY = step.toY;
}
}
else if (step.type == "scale") {
if (node->button) {
node->button->animScaleX = step.toX;
node->button->animScaleY = step.toY;
}
}
act.stepIndex++;
act.elapsedMs = 0.0f;
act.stepStarted = false;
continue;
}
if (!act.stepStarted && act.stepIndex == 0 && act.elapsedMs == 0.0f) {
if (node->button) {
act.origOffsetX = node->button->animOffsetX;
act.origOffsetY = node->button->animOffsetY;
act.origScaleX = node->button->animScaleX;
act.origScaleY = node->button->animScaleY;
}
else {
act.origOffsetX = act.origOffsetY = 0.0f;
act.origScaleX = act.origScaleY = 1.0f;
}
}
float prevElapsed = act.elapsedMs;
act.elapsedMs += deltaMs;
if (!act.stepStarted && prevElapsed == 0.0f) {
if (node->button) {
act.startOffsetX = node->button->animOffsetX;
act.startOffsetY = node->button->animOffsetY;
act.startScaleX = node->button->animScaleX;
act.startScaleY = node->button->animScaleY;
}
else {
act.startOffsetX = act.startOffsetY = 0.0f;
act.startScaleX = act.startScaleY = 1.0f;
}
if (step.type == "move") {
act.endOffsetX = step.toX;
act.endOffsetY = step.toY;
}
else if (step.type == "scale") {
act.endScaleX = step.toX;
act.endScaleY = step.toY;
}
act.stepStarted = true;
}
float t = (step.durationMs > 0.0f) ? (act.elapsedMs / step.durationMs) : 1.0f;
if (t > 1.0f) t = 1.0f;
float te = applyEasing(step.easing, t);
if (step.type == "move") {
float nx = act.startOffsetX + (act.endOffsetX - act.startOffsetX) * te;
float ny = act.startOffsetY + (act.endOffsetY - act.startOffsetY) * te;
if (node->button) {
node->button->animOffsetX = nx;
node->button->animOffsetY = ny;
}
}
else if (step.type == "scale") {
float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te;
float sy = act.startScaleY + (act.endScaleY - act.startScaleY) * te;
if (node->button) {
node->button->animScaleX = sx;
node->button->animScaleY = sy;
}
}
else if (step.type == "wait") {
//wait
}
if (act.elapsedMs >= step.durationMs) {
act.stepIndex++;
act.elapsedMs = 0.0f;
act.stepStarted = false;
}
}
}
for (auto it = animationsToRemove.rbegin(); it != animationsToRemove.rend(); ++it) {
auto& [node, index] = *it;
if (nodeActiveAnims.find(node) != nodeActiveAnims.end()) {
auto& animList = nodeActiveAnims[node];
if (index < animList.size()) {
animList.erase(animList.begin() + index);
}
if (animList.empty()) {
nodeActiveAnims.erase(node);
}
}
}
for (auto& cb : pendingCallbacks) {
try {
cb();
}
catch (...) {
std::cerr << "UiManager: animation onComplete callback threw exception" << std::endl;
}
}
}
void UiManager::onMouseMove(int x, int y) {
for (auto& b : buttons) {
if (b->state != ButtonState::Disabled)
{
if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover;
}
else {
if (b->state != ButtonState::Pressed) b->state = ButtonState::Normal;
}
}
}
if (pressedSlider) {
auto s = pressedSlider;
float t;
if (s->vertical) {
t = (y - s->rect.y) / s->rect.h;
}
else {
t = (x - s->rect.x) / s->rect.w;
}
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
s->value = t;
s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
}
}
void UiManager::onMouseDown(int x, int y) {
for (auto& b : buttons) {
if (b->state != ButtonState::Disabled)
{
if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
b->state = ButtonState::Pressed;
pressedButton = b;
}
}
}
for (auto& s : sliders) {
if (s->rect.contains((float)x, (float)y)) {
pressedSlider = s;
float t;
if (s->vertical) {
t = (y - s->rect.y) / s->rect.h;
}
else {
t = (x - s->rect.x) / s->rect.w;
}
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
s->value = t;
s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
break;
}
}
for (auto& tf : textFields) {
if (tf->rect.contains((float)x, (float)y)) {
focusedTextField = tf;
tf->focused = true;
}
else {
tf->focused = false;
}
}
}
void UiManager::onMouseUp(int x, int y) {
std::vector<std::shared_ptr<UiButton>> clicked;
for (auto& b : buttons) {
if (!b) continue;
bool contains = b->rect.contains((float)x, (float)y);
if (b->state == ButtonState::Pressed) {
if (contains && pressedButton == b) {
clicked.push_back(b);
}
b->state = contains ? ButtonState::Hover : ButtonState::Normal;
}
}
for (auto& b : clicked) {
if (b->onClick) {
b->onClick(b->name);
}
}
pressedButton.reset();
if (pressedSlider) pressedSlider.reset();
}
void UiManager::onKeyPress(unsigned char key) {
if (!focusedTextField) return;
if (key >= 32 && key <= 126) {
if (focusedTextField->text.length() < (size_t)focusedTextField->maxLength) {
focusedTextField->text += key;
if (focusedTextField->onTextChanged) {
focusedTextField->onTextChanged(focusedTextField->name, focusedTextField->text);
}
}
}
}
void UiManager::onKeyBackspace() {
if (!focusedTextField) return;
if (!focusedTextField->text.empty()) {
focusedTextField->text.pop_back();
if (focusedTextField->onTextChanged) {
focusedTextField->onTextChanged(focusedTextField->name, focusedTextField->text);
}
}
}
std::shared_ptr<UiButton> UiManager::findButton(const std::string& name) {
for (auto& b : buttons) if (b->name == name) return b;
return nullptr;
}
bool UiManager::startAnimationOnNode(const std::string& nodeName, const std::string& animName) {
if (!root) return false;
auto node = findNodeByName(root, nodeName);
if (!node) return false;
auto it = node->animations.find(animName);
if (it == node->animations.end()) return false;
ActiveAnim aa;
aa.name = animName;
aa.seq = &it->second;
aa.stepIndex = 0;
aa.elapsedMs = 0.0f;
aa.repeat = it->second.repeat;
aa.stepStarted = false;
if (node->button) {
aa.origOffsetX = node->button->animOffsetX;
aa.origOffsetY = node->button->animOffsetY;
aa.origScaleX = node->button->animScaleX;
aa.origScaleY = node->button->animScaleY;
}
auto cbIt = animCallbacks.find({ nodeName, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[node].push_back(std::move(aa));
return true;
}
bool UiManager::stopAnimationOnNode(const std::string& nodeName, const std::string& animName) {
if (!root) return false;
auto node = findNodeByName(root, nodeName);
if (!node) return false;
auto it = nodeActiveAnims.find(node);
if (it != nodeActiveAnims.end()) {
auto& animList = it->second;
for (auto animIt = animList.begin(); animIt != animList.end(); ) {
if (animIt->name == animName) {
animIt = animList.erase(animIt);
}
else {
++animIt;
}
}
if (animList.empty()) {
nodeActiveAnims.erase(it);
}
return true;
}
return false;
}
void UiManager::startAnimation(const std::string& animName) {
if (!root) return;
std::function<void(const std::shared_ptr<UiNode>&)> traverse = [&](const std::shared_ptr<UiNode>& n) {
if (!n) return;
auto it = n->animations.find(animName);
if (it != n->animations.end()) {
ActiveAnim aa;
aa.name = animName;
aa.seq = &it->second;
aa.stepIndex = 0;
aa.elapsedMs = 0.0f;
aa.repeat = it->second.repeat;
aa.stepStarted = false;
if (n->button) {
aa.origOffsetX = n->button->animOffsetX;
aa.origOffsetY = n->button->animOffsetY;
aa.origScaleX = n->button->animScaleX;
aa.origScaleY = n->button->animScaleY;
}
auto cbIt = animCallbacks.find({ n->name, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[n].push_back(std::move(aa));
}
for (auto& c : n->children) traverse(c);
};
traverse(root);
}
bool UiManager::setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb) {
animCallbacks[{nodeName, animName}] = std::move(cb);
return true;
}
std::shared_ptr<UiTextView> UiManager::findTextView(const std::string& name) {
for (auto& tv : textViews) {
if (tv->name == name) return tv;
}
return nullptr;
}
bool UiManager::setText(const std::string& name, const std::string& newText) {
auto tv = findTextView(name);
if (!tv) {
return false;
}
tv->text = newText;
return true;
}
} // namespace ZL