From ffbecbbcde3cdf2c20f425258a804dcea47276e0 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sat, 28 Feb 2026 22:21:08 +0300 Subject: [PATCH] Working on ui --- resources/config/main_menu.json | 35 +---- src/UiManager.cpp | 242 +++++++++++++++++++++++++++----- src/UiManager.h | 57 +++++++- 3 files changed, 259 insertions(+), 75 deletions(-) diff --git a/resources/config/main_menu.json b/resources/config/main_menu.json index 1807e74..f0f8c8b 100644 --- a/resources/config/main_menu.json +++ b/resources/config/main_menu.json @@ -10,30 +10,17 @@ "type": "LinearLayout", "name": "settingsButtons", "orientation": "vertical", + "vertical_align" : "center", + "horizontal_align" : "center", "spacing": 10, "x": 0, "y": 0, - "width": 300, - "height": 300, + "width": 1280, + "height": 720, "children": [ - { - "type": "Button", - "name": "langButton", - "x": 1100, - "y": 580, - "width": 142, - "height": 96, - "textures": { - "normal": "resources/main_menu/lang.png", - "hover": "resources/main_menu/lang.png", - "pressed": "resources/main_menu/lang.png" - } - }, { "type": "Button", "name": "titleBtn", - "x": 473, - "y": 500, "width": 254, "height": 35, "textures": { @@ -45,8 +32,6 @@ { "type": "Button", "name": "underlineBtn", - "x": 516, - "y": 465, "width": 168, "height": 44, "textures": { @@ -58,8 +43,6 @@ { "type": "Button", "name": "subtitleBtn", - "x": 528, - "y": 455, "width": 144, "height": 11, "textures": { @@ -71,8 +54,6 @@ { "type": "Button", "name": "singleButton", - "x": 409, - "y": 360, "width": 382, "height": 56, "textures": { @@ -84,8 +65,6 @@ { "type": "Button", "name": "multiplayerButton", - "x": 409, - "y": 289, "width": 382, "height": 56, "textures": { @@ -97,8 +76,6 @@ { "type": "Button", "name": "multiplayerButton2", - "x": 409, - "y": 218, "width": 382, "height": 56, "textures": { @@ -110,8 +87,6 @@ { "type": "Button", "name": "exitButton", - "x": 409, - "y": 147, "width": 382, "height": 56, "textures": { @@ -123,8 +98,6 @@ { "type": "Button", "name": "versionLabel", - "x": 559.5, - "y": 99, "width": 81, "height": 9, "textures": { diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 6b3dc95..80f5f95 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -184,21 +184,54 @@ namespace ZL { std::shared_ptr parseNode(const json& j, Renderer& renderer, const std::string& zipFile) { auto node = std::make_shared(); - if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get(); - if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get(); - if (j.contains("x")) node->rect.x = j["x"].get(); - if (j.contains("y")) node->rect.y = j["y"].get(); - if (j.contains("width")) node->rect.w = j["width"].get(); - if (j.contains("height")) node->rect.h = j["height"].get(); + // 1. Определяем тип контейнера и ориентацию + std::string typeStr = j.value("type", "FrameLayout"); // По умолчанию FrameLayout + if (typeStr == "LinearLayout") { + node->layoutType = LayoutType::Linear; + } + else { + node->layoutType = LayoutType::Frame; + } - if (j.contains("orientation") && j["orientation"].is_string()) node->orientation = j["orientation"].get(); - if (j.contains("spacing")) node->spacing = j["spacing"].get(); + if (j.contains("name")) node->name = j["name"].get(); - if (node->type == "Button") { + // 2. Читаем размеры во временные "локальные" поля + // Это критически важно: мы не пишем сразу в screenRect, + // так как LinearLayout их пересчитает. + node->localX = j.value("x", 0.0f); + node->localY = j.value("y", 0.0f); + node->width = j.value("width", 0.0f); + node->height = j.value("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 == "top") node->layoutSettings.vAlign = VerticalAlign::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 = node->rect; + btn->rect = initialRect; if (!j.contains("textures") || !j["textures"].is_object()) { std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl; @@ -225,10 +258,10 @@ namespace ZL { node->button = btn; } - else if (node->type == "Slider") { + else if (typeStr == "Slider") { auto s = std::make_shared(); s->name = node->name; - s->rect = node->rect; + s->rect = initialRect; if (!j.contains("textures") || !j["textures"].is_object()) { std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl; @@ -261,10 +294,10 @@ namespace ZL { node->slider = s; } - else if (node->type == "TextField") { + else if (typeStr == "TextField") { auto tf = std::make_shared(); tf->name = node->name; - tf->rect = node->rect; + tf->rect = initialRect; if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get(); if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get(); @@ -331,11 +364,11 @@ namespace ZL { } } - if (node->type == "TextView") { + if (typeStr == "TextView") { auto tv = std::make_shared(); - tv->name = node->name; - tv->rect = node->rect; + 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(); @@ -400,6 +433,7 @@ namespace ZL { throw std::runtime_error("Failed to load UI file: " + path); } + root = parseNode(j["root"], renderer, zipFile); return root; @@ -407,7 +441,7 @@ namespace ZL { void UiManager::replaceRoot(std::shared_ptr newRoot) { root = newRoot; - layoutNode(root); + layoutNode(root, 0, 0); buttons.clear(); sliders.clear(); textViews.clear(); @@ -433,37 +467,167 @@ namespace ZL { - void UiManager::layoutNode(const std::shared_ptr& node) { + void UiManager::updateLayout(const std::shared_ptr& node, float parentX, float parentY) { + // 1. Сначала определяем позицию самого контейнера + // Если это корень, он обычно занимает весь экран или заданные координаты + node->screenRect.x = parentX + node->localX; + node->screenRect.y = parentY + node->localY; + node->screenRect.w = node->width; + node->screenRect.h = node->height; + + float currentX = node->screenRect.x; + // В UI обычно Y растет сверху вниз, но в нашем OpenGL (Renderer.cpp) + // используется орто-матрица с Y-вверх. Учтем это. + float currentY = node->screenRect.y + node->screenRect.h; + for (auto& child : node->children) { - child->rect.x += node->rect.x; - child->rect.y += node->rect.y; - } - - if (node->type == "LinearLayout") { - std::string orient = node->orientation; - std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower); - - float cursorX = node->rect.x; - float cursorY = node->rect.y; - for (auto& child : node->children) { - if (orient == "horizontal") { - child->rect.x = cursorX; - child->rect.y = node->rect.y; - cursorX += child->rect.w + node->spacing; + if (node->layoutType == LayoutType::Linear) { + if (node->orientation == Orientation::Vertical) { + // Игнорируем child->localX/Y для LinearVertical + currentY -= child->height; // Сдвигаемся вниз на высоту элемента + updateLayout(child, node->screenRect.x, currentY - node->screenRect.y); + currentY -= node->spacing; // Добавляем отступ } else { - child->rect.x = node->rect.x; - child->rect.y = cursorY; - cursorY += child->rect.h + node->spacing; + // Логика для Horizontal... + updateLayout(child, currentX, 0); // упрощенно + currentX += child->width + node->spacing; } - layoutNode(child); + } + else { + // FrameLayout: используем честные localX и localY + updateLayout(child, node->screenRect.x, node->screenRect.y); + } + } + + // После вычисления позиций, нужно обновить меши кнопок (buildMesh) + // на основе вычисленных screenRect. + if (node->button) { + node->button->rect = node->screenRect; + node->button->buildMesh(); + } + } + + void UiManager::layoutNode(const std::shared_ptr& node, float parentX, float parentY) { + // 1. Базовый расчет позиции текущего узла относительно родителя + node->screenRect.x = parentX + node->localX; + node->screenRect.y = parentY + node->localY; + node->screenRect.w = node->width; + node->screenRect.h = node->height; + + 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; + } + } + + // Вычисляем начальные смещения (Offsets) + float startX = 0; + float startY = node->height; // Начинаем сверху (Y растет вверх) + + // Вертикальное выравнивание всего блока внутри контейнера + if (node->orientation == Orientation::Vertical) { + if (node->layoutSettings.vAlign == VerticalAlign::Center) { + startY = (node->height + totalContentHeight) / 2.0f; + } + else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) { + startY = totalContentHeight; + } + } + + // Горизонтальное выравнивание всего блока + if (node->orientation == Orientation::Horizontal) { + if (node->layoutSettings.hAlign == HorizontalAlign::Center) { + startX = (node->width - totalContentWidth) / 2.0f; + } + else if (node->layoutSettings.hAlign == HorizontalAlign::Right) { + startX = node->width - totalContentWidth; + } + } + + float cursorX = startX; + float cursorY = startY; + + for (auto& child : node->children) { + if (node->orientation == Orientation::Vertical) { + cursorY -= child->height; + + // Горизонтальное выравнивание внутри строки (Cross-axis alignment) + float childX = 0; + float freeSpaceX = node->width - child->width; + 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 { + // Вертикальное выравнивание внутри колонки + float childY = 0; + float freeSpaceY = node->height - child->height; + if (node->layoutSettings.vAlign == VerticalAlign::Center) childY = freeSpaceY / 2.0f; + else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) childY = 0; // Внизу + else childY = freeSpaceY; // Вверху + + child->localX = cursorX; + child->localY = childY; + cursorX += child->width + node->spacing; + } + layoutNode(child, node->screenRect.x, node->screenRect.y); } } else { + // Если Frame, просто идем по детям с их фиксированными координатами for (auto& child : node->children) { - layoutNode(child); + layoutNode(child, node->screenRect.x, node->screenRect.y); } } + + // Обновляем меши визуальных компонентов + 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; + // Аналогично для курсора и фонового меша + } } void UiManager::collectButtonsAndSliders(const std::shared_ptr& node) { diff --git a/src/UiManager.h b/src/UiManager.h index 05af33c..88a7082 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -31,6 +31,34 @@ namespace ZL { Pressed }; + enum class LayoutType { + Frame, // Позиционирование по X, Y + Linear // Автоматическое позиционирование + }; + + enum class Orientation { + Vertical, + Horizontal + }; + + enum class HorizontalAlign { + Left, + Center, + Right + }; + + enum class VerticalAlign { + Top, + Center, + Bottom + }; + + // В структуру или класс, отвечающий за LinearLayout (вероятно, это свойства UiNode) + struct LayoutSettings { + HorizontalAlign hAlign = HorizontalAlign::Left; + VerticalAlign vAlign = VerticalAlign::Top; + }; + struct UiButton { std::string name; UiRect rect; @@ -111,21 +139,38 @@ namespace ZL { }; struct UiNode { - std::string type; - UiRect rect; std::string name; + LayoutType layoutType = LayoutType::Frame; + Orientation orientation = Orientation::Vertical; + float spacing = 0.0f; + + LayoutSettings layoutSettings; + + // Внутренние вычисленные координаты для OpenGL + // Именно их мы передаем в Vertex Buffer при buildMesh() + UiRect screenRect; + + // Данные из JSON (желаемые размеры и смещения) + float localX = 0; + float localY = 0; + float width = 0; + float height = 0; + + // Иерархия std::vector> children; + + // Компоненты (только один из них обычно активен для ноды) std::shared_ptr button; std::shared_ptr slider; std::shared_ptr textView; std::shared_ptr textField; - std::string orientation = "vertical"; - float spacing = 0.0f; + // Анимации struct AnimStep { std::string type; float toX = 0.0f; float toY = 0.0f; + float toScale = 1.0f; // Полезно добавить для UI float durationMs = 0.0f; std::string easing = "linear"; }; @@ -202,7 +247,9 @@ namespace ZL { bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function cb); private: - void layoutNode(const std::shared_ptr& node); + void updateLayout(const std::shared_ptr& node, float parentX, float parentY); + void layoutNode(const std::shared_ptr& node, float parentX, float parentY); + void syncComponentRects(const std::shared_ptr& node); void collectButtonsAndSliders(const std::shared_ptr& node); struct ActiveAnim {