#include "UiManager.h" #include "utils/Utils.h" #include "render/TextRenderer.h" #include #include #include #include "GameConstants.h" 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* 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; renderer.PushMatrix(); renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f }); renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f }); renderer.RenderUniform1i(textureUniformName, 0); glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID()); renderer.DrawVertexRenderStruct(mesh); 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 { renderer.RenderUniform1i(textureUniformName, 0); if (texTrack) { glBindTexture(GL_TEXTURE_2D, texTrack->getTexID()); renderer.DrawVertexRenderStruct(trackMesh); } if (texKnob) { glBindTexture(GL_TEXTURE_2D, texKnob->getTexID()); renderer.DrawVertexRenderStruct(knobMesh); } } void UiStaticImage::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 UiStaticImage::draw(Renderer& renderer) const { if (!texture) return; renderer.RenderUniform1i(textureUniformName, 0); glBindTexture(GL_TEXTURE_2D, texture->getTexID()); renderer.DrawVertexRenderStruct(mesh); } 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 parseNode(const json& j, Renderer& renderer, const std::string& zipFile) { auto node = std::make_shared(); // 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(); // 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(); } } 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(); } } else { node->height = 0.0f; } // 3. Параметры компоновки if (j.contains("orientation")) { std::string orient = j["orientation"].get(); 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(); if (hg == "right") node->layoutSettings.hGravity = HorizontalGravity::Right; else if (hg == "center") node->layoutSettings.hGravity = HorizontalGravity::Center; else node->layoutSettings.hGravity = HorizontalGravity::Left; } // Читаем Vertical Gravity if (j.contains("vertical_gravity")) { std::string vg = j["vertical_gravity"].get(); if (vg == "bottom") node->layoutSettings.vGravity = VerticalGravity::Bottom; else if (vg == "center") node->layoutSettings.vGravity = VerticalGravity::Center; 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(); 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 { if (!t.contains(key) || !t[key].is_string()) return nullptr; std::string path = t[key].get(); 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(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(); 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 { if (!t.contains(key) || !t[key].is_string()) return nullptr; std::string path = t[key].get(); 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(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(); if (j.contains("orientation")) { std::string orient = j["orientation"].get(); 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(); tf->name = node->name; tf->rect = initialRect; if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get(); if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get(); if (j.contains("fontSize")) tf->fontSize = j["fontSize"].get(); if (j.contains("maxLength")) tf->maxLength = j["maxLength"].get(); 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(); } } 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(); } } 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(); } } 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(); } } tf->textRenderer = std::make_unique(); 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(); 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::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(); s.toY = step["to"][1].get(); } if (step.contains("duration")) { s.durationMs = step["duration"].get() * 1000.0f; } if (step.contains("easing") && step["easing"].is_string()) { s.easing = step["easing"].get(); 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 == "StaticImage") { auto img = std::make_shared(); img->name = node->name; img->rect = initialRect; std::string texPath; if (j.contains("texture") && j["texture"].is_string()) { texPath = j["texture"].get(); } else if (j.contains("textures") && j["textures"].is_object() && j["textures"].contains("normal")) { texPath = j["textures"]["normal"].get(); } if (!texPath.empty()) { try { auto data = CreateTextureDataFromPng(texPath.c_str(), zipFile.c_str()); img->texture = std::make_shared(data); } catch (const std::exception& e) { std::cerr << "UiManager: failed load texture for StaticImage '" << img->name << "' : " << e.what() << std::endl; } } node->staticImage = img; } if (typeStr == "TextView") { auto tv = std::make_shared(); tv->name = node->name; tv->rect = initialRect; if (j.contains("text")) tv->text = j["text"].get(); if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get(); if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get(); 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(); } } if (j.contains("centered")) tv->centered = j["centered"].get(); tv->textRenderer = std::make_unique(); 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 loadUiFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) { std::shared_ptr 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 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(); staticImages.clear(); collectButtonsAndSliders(root); nodeActiveAnims.clear(); for (auto& b : buttons) { b->buildMesh(); } for (auto& s : sliders) { s->buildTrackMesh(); s->buildKnobMesh(); } for (auto& img : staticImages) { img->buildMesh(); } } void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) { std::shared_ptr newRoot = loadUiFromFile(path, renderer, zipFile); replaceRoot(newRoot); } void UiManager::layoutNode(const std::shared_ptr& 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; } else if (child->layoutSettings.hGravity == HorizontalGravity::Center) { fLX = (currentW - childW) / 2.0f + child->localX; } if (child->layoutSettings.vGravity == VerticalGravity::Top) { fLY = currentH - childH - child->localY; } else if (child->layoutSettings.vGravity == VerticalGravity::Center) { fLY = (currentH - childH) / 2.0f + 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& 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; // Аналогично для курсора и фонового меша } // 5. Обновляем статическое изображение if (node->staticImage) { node->staticImage->rect = node->screenRect; node->staticImage->buildMesh(); } } 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& 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); } if (node->staticImage) { staticImages.push_back(node->staticImage); } for (auto& c : node->children) collectButtonsAndSliders(c); } bool UiManager::setButtonCallback(const std::string& name, std::function 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::setButtonPressCallback(const std::string& name, std::function cb) { auto b = findButton(name); if (!b) { std::cerr << "UiManager: setButtonPressCallback failed, button not found: " << name << std::endl; return false; } b->onPress = 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(); 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(data); } if (!knobPath.empty()) { auto data = CreateTextureDataFromPng(knobPath.c_str(), zipFile.c_str()); s->texKnob = std::make_shared(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 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 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 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 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 newRoot) { MenuState prev; prev.root = root; prev.buttons = buttons; prev.sliders = sliders; prev.textFields = textFields; prev.staticImages = staticImages; prev.pressedButtons = pressedButtons; prev.pressedSliders = pressedSliders; 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; staticImages = s.staticImages; pressedButtons = s.pressedButtons; pressedSliders = s.pressedSliders; 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& img : staticImages) { img->draw(renderer); } 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 findNodeByName(const std::shared_ptr& 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, size_t>> animationsToRemove; std::vector> 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::onTouchMove(int64_t fingerId, int x, int y) { // Hover state updates only make sense for mouse (single pointer) if (fingerId == MOUSE_FINGER_ID) { 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; } } } } auto it = pressedSliders.find(fingerId); if (it != pressedSliders.end()) { auto s = it->second; 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::onTouchDown(int64_t fingerId, 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; pressedButtons[fingerId] = b; if (b->onPress) b->onPress(b->name); break; // a single finger can only press one button } } } for (auto& s : sliders) { if (s->rect.contains((float)x, (float)y)) { pressedSliders[fingerId] = 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::onTouchUp(int64_t fingerId, int x, int y) { std::vector> clicked; auto btnIt = pressedButtons.find(fingerId); if (btnIt != pressedButtons.end()) { auto b = btnIt->second; if (b) { bool contains = b->rect.contains((float)x, (float)y); if (b->state == ButtonState::Pressed) { if (contains) { clicked.push_back(b); } // On mouse: leave Hover if still over button. On touch: always Normal. b->state = (contains && fingerId == MOUSE_FINGER_ID) ? ButtonState::Hover : ButtonState::Normal; } } pressedButtons.erase(btnIt); } pressedSliders.erase(fingerId); for (auto& b : clicked) { if (b->onClick) { b->onClick(b->name); } } } 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 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&)> traverse = [&](const std::shared_ptr& 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 cb) { animCallbacks[{nodeName, animName}] = std::move(cb); return true; } std::shared_ptr UiManager::findStaticImage(const std::string& name) { for (auto& img : staticImages) { if (img->name == name) return img; } return nullptr; } std::shared_ptr 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