1280 lines
38 KiB
C++
1280 lines
38 KiB
C++
#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
|