#include "UiManager.h" #include "utils/Utils.h" #include #include #include namespace ZL { using json = nlohmann::json; 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; } if (!(*tex)) return; 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); glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID()); renderer.DrawVertexRenderStruct(mesh); renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vTexCoordName); } 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 UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) { 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); layoutNode(root); buttons.clear(); sliders.clear(); collectButtonsAndSliders(root); for (auto& b : buttons) { b->buildMesh(); } for (auto& s : sliders) { s->buildTrackMesh(); s->buildKnobMesh(); } } std::shared_ptr UiManager::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(); if (j.contains("orientation") && j["orientation"].is_string()) node->orientation = j["orientation"].get(); if (j.contains("spacing")) node->spacing = j["spacing"].get(); if (node->type == "Button") { auto btn = std::make_shared(); btn->name = node->name; btn->rect = node->rect; 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 { 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"); node->button = btn; } else if (node->type == "Slider") { auto s = std::make_shared(); s->name = node->name; s->rect = node->rect; 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 { 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; } if (j.contains("children") && j["children"].is_array()) { for (const auto& ch : j["children"]) { node->children.push_back(parseNode(ch, renderer, zipFile)); } } return node; } void UiManager::layoutNode(const std::shared_ptr& node) { 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; } else { child->rect.x = node->rect.x; child->rect.y = cursorY; cursorY += child->rect.h + node->spacing; } layoutNode(child); } } else { for (auto& child : node->children) { layoutNode(child); } } } void UiManager::collectButtonsAndSliders(const std::shared_ptr& node) { if (node->button) { buttons.push_back(node->button); } if (node->slider) { sliders.push_back(node->slider); } 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::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; } bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) { MenuState prev; prev.root = root; prev.buttons = buttons; prev.sliders = sliders; prev.pressedButton = pressedButton; prev.pressedSlider = pressedSlider; prev.path = ""; try { loadFromFile(path, renderer, zipFile); menuStack.push_back(std::move(prev)); return true; } catch (const std::exception& e) { std::cerr << "UiManager: pushMenuFromFile failed to load " << path << " : " << e.what() << std::endl; return false; } } 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(); root = s.root; buttons = s.buttons; sliders = s.sliders; pressedButton = s.pressedButton; pressedSlider = s.pressedSlider; for (auto& b : buttons) { if (b) 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::width, Environment::height, -1, 1); renderer.PushMatrix(); renderer.LoadIdentity(); for (const auto& b : buttons) { b->draw(renderer); } for (const auto& s : sliders) { s->draw(renderer); } renderer.PopMatrix(); renderer.PopProjectionMatrix(); } void UiManager::onMouseMove(int x, int y) { for (auto& b : buttons) { if (b->rect.contains((float)x, (float)y)) { 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->rect.contains((float)x, (float)y)) { 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; } } } void UiManager::onMouseUp(int x, int y) { for (auto& b : buttons) { bool contains = b->rect.contains((float)x, (float)y); if (b->state == ButtonState::Pressed) { if (contains && pressedButton == b) { if (b->onClick) { b->onClick(b->name); } } b->state = contains ? ButtonState::Hover : ButtonState::Normal; } } pressedButton.reset(); if (pressedSlider) { pressedSlider.reset(); } } std::shared_ptr UiManager::findButton(const std::string& name) { for (auto& b : buttons) if (b->name == name) return b; return nullptr; } } // namespace ZL