#pragma once #include "render/Renderer.h" #include "render/TextureManager.h" #include "render/TextRenderer.h" #include "Environment.h" #include "external/nlohmann/json.hpp" #include #include #include #include #include #include namespace ZL { using json = nlohmann::json; struct UiRect { float x = 0; float y = 0; float w = 0; float h = 0; bool contains(float px, float py) const { return px >= x && px <= x + w && py >= y && py <= y + h; } bool containsConsideringBorder(float px, float py, float border) const { return px >= x+border && px <= x + w-border && py >= y+border && py <= y + h-border; } }; enum class ButtonState { Normal, Hover, Pressed, Disabled }; enum class LayoutType { Frame, // Позиционирование по X, Y Linear // Автоматическое позиционирование }; enum class Orientation { Vertical, Horizontal }; enum class HorizontalAlign { Left, Center, Right }; enum class VerticalAlign { Top, Center, Bottom }; enum class HorizontalGravity { Left, Center, Right }; enum class VerticalGravity { Bottom, // Обычно в OpenGL Y растет вверх, так что низ - это 0 Center, Top }; // В структуру или класс, отвечающий за LinearLayout (вероятно, это свойства UiNode) struct LayoutSettings { HorizontalAlign hAlign = HorizontalAlign::Left; VerticalAlign vAlign = VerticalAlign::Top; HorizontalGravity hGravity = HorizontalGravity::Left; VerticalGravity vGravity = VerticalGravity::Top; }; struct UiButton { std::string name; UiRect rect; float border = 0; std::shared_ptr texNormal; std::shared_ptr texHover; std::shared_ptr texPressed; std::shared_ptr texDisabled; ButtonState state = ButtonState::Normal; VertexRenderStruct mesh; std::function onClick; std::function onPress; // fires on touch/mouse down // animation runtime float animOffsetX = 0.0f; float animOffsetY = 0.0f; float animScaleX = 1.0f; float animScaleY = 1.0f; void buildMesh(); void draw(Renderer& renderer) const; }; struct UiSlider { std::string name; UiRect rect; std::shared_ptr texTrack; std::shared_ptr texKnob; VertexRenderStruct trackMesh; VertexRenderStruct knobMesh; float value = 0.0f; bool vertical = true; std::function onValueChanged; void buildTrackMesh(); void buildKnobMesh(); void draw(Renderer& renderer) const; }; struct UiTextButton { std::string name; UiRect rect; float border = 0; // Textures are optional — button can be text-only std::shared_ptr texNormal; std::shared_ptr texHover; std::shared_ptr texPressed; std::shared_ptr texDisabled; ButtonState state = ButtonState::Normal; VertexRenderStruct mesh; // Text drawn on top of the button std::string text; std::string fontPath = "resources/fonts/DroidSans.ttf"; int fontSize = 32; std::array color = { 1.f, 1.f, 1.f, 1.f }; bool textCentered = true; std::unique_ptr textRenderer; std::function onClick; std::function onPress; // Animation runtime float animOffsetX = 0.0f; float animOffsetY = 0.0f; float animScaleX = 1.0f; float animScaleY = 1.0f; void buildMesh(); void draw(Renderer& renderer) const; }; struct UiTextView { std::string name; UiRect rect; std::string text = ""; std::string fontPath = "resources/fonts/DroidSans.ttf"; int fontSize = 32; std::array color = { 1.f, 1.f, 1.f, 1.f }; // rgba bool centered = true; std::unique_ptr textRenderer; void draw(Renderer& renderer) const { if (textRenderer) { textRenderer->drawText(text, rect.x + rect.w / 2, rect.y + rect.h / 2, 1.0f, centered, color); } } }; struct UiTextField { std::string name; UiRect rect; std::string text = ""; std::string placeholder = ""; std::string fontPath = "resources/fonts/DroidSans.ttf"; int fontSize = 32; std::array color = { 1.f, 1.f, 1.f, 1.f }; std::array placeholderColor = { 0.5f, 0.5f, 0.5f, 1.f }; std::array backgroundColor = { 0.2f, 0.2f, 0.2f, 1.f }; std::array borderColor = { 0.5f, 0.5f, 0.5f, 1.f }; int maxLength = 256; bool focused = false; std::unique_ptr textRenderer; std::function onTextChanged; void draw(Renderer& renderer) const; }; struct UiStaticImage { std::string name; UiRect rect; std::shared_ptr texture; VertexRenderStruct mesh; void buildMesh(); void draw(Renderer& renderer) const; }; struct UiNode { 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 textButton; std::shared_ptr slider; std::shared_ptr textView; std::shared_ptr textField; std::shared_ptr staticImage; // Анимации 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"; }; struct AnimSequence { std::vector steps; bool repeat = false; }; std::map animations; }; std::shared_ptr parseNode(const json& j, Renderer& renderer, const std::string& zipFile); std::shared_ptr loadUiFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); class UiManager { public: UiManager() = default; // Sentinel finger ID used for mouse events on desktop/web static constexpr int64_t MOUSE_FINGER_ID = -1LL; void replaceRoot(std::shared_ptr newRoot); void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); void draw(Renderer& renderer); // Multi-touch methods (used directly for touch events with per-finger IDs) void onTouchDown(int64_t fingerId, int x, int y); void onTouchUp(int64_t fingerId, int x, int y); void onTouchMove(int64_t fingerId, int x, int y); // Mouse convenience wrappers (delegate to touch with MOUSE_FINGER_ID) void onMouseMove(int x, int y) { onTouchMove(MOUSE_FINGER_ID, x, y); } void onMouseDown(int x, int y) { onTouchDown(MOUSE_FINGER_ID, x, y); } void onMouseUp(int x, int y) { onTouchUp(MOUSE_FINGER_ID, x, y); } void onKeyPress(unsigned char key); void onKeyBackspace(); // Returns true if any finger is currently interacting with UI bool isUiInteraction() const { return !pressedButtons.empty() || !pressedTextButtons.empty() || !pressedSliders.empty() || focusedTextField != nullptr; } // Returns true if this specific finger is currently interacting with UI bool isUiInteractionForFinger(int64_t fingerId) const { return pressedButtons.count(fingerId) > 0 || pressedTextButtons.count(fingerId) > 0 || pressedSliders.count(fingerId) > 0 || focusedTextField != nullptr; } void stopAllAnimations() { nodeActiveAnims.clear(); for (auto& b : buttons) { if (b) { b->animOffsetX = 0.0f; b->animOffsetY = 0.0f; b->animScaleX = 1.0f; b->animScaleY = 1.0f; } } for (auto& tb : textButtons) { if (tb) { tb->animOffsetX = 0.0f; tb->animOffsetY = 0.0f; tb->animScaleX = 1.0f; tb->animScaleY = 1.0f; } } } std::shared_ptr findButton(const std::string& name); bool setButtonCallback(const std::string& name, std::function cb); bool setButtonPressCallback(const std::string& name, std::function cb); std::shared_ptr findTextButton(const std::string& name); bool setTextButtonCallback(const std::string& name, std::function cb); bool setTextButtonPressCallback(const std::string& name, std::function cb); bool setTextButtonText(const std::string& name, const std::string& newText); bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile, const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true); std::shared_ptr findSlider(const std::string& name); bool setSliderCallback(const std::string& name, std::function cb); bool setSliderValue(const std::string& name, float value); // programmatic set (clamped 0..1) std::shared_ptr findTextView(const std::string& name); bool setText(const std::string& name, const std::string& newText); std::shared_ptr findTextField(const std::string& name); bool setTextFieldCallback(const std::string& name, std::function cb); std::string getTextFieldValue(const std::string& name); std::shared_ptr findStaticImage(const std::string& name); bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); bool pushMenuFromSavedRoot(std::shared_ptr newRoot); bool popMenu(); void clearMenuStack(); void update(float deltaMs); void startAnimation(const std::string& animName); bool startAnimationOnNode(const std::string& nodeName, const std::string& animName); bool stopAnimationOnNode(const std::string& nodeName, const std::string& animName); bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function cb); void updateAllLayouts(); private: void layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY); void syncComponentRects(const std::shared_ptr& node); void collectButtonsAndSliders(const std::shared_ptr& node); struct ActiveAnim { std::string name; const UiNode::AnimSequence* seq = nullptr; size_t stepIndex = 0; float elapsedMs = 0.0f; bool repeat = false; float startOffsetX = 0.0f; float startOffsetY = 0.0f; float endOffsetX = 0.0f; float endOffsetY = 0.0f; float startScaleX = 1.0f; float startScaleY = 1.0f; float endScaleX = 1.0f; float endScaleY = 1.0f; std::function onComplete; float origOffsetX = 0.0f; float origOffsetY = 0.0f; float origScaleX = 1.0f; float origScaleY = 1.0f; bool stepStarted = false; }; std::shared_ptr root; std::vector> buttons; std::vector> textButtons; std::vector> sliders; std::vector> textViews; std::vector> textFields; std::vector> staticImages; std::map, std::vector> nodeActiveAnims; std::map, std::function> animCallbacks; // key: (nodeName, animName) // Per-finger tracking for multi-touch support std::map> pressedButtons; std::map> pressedTextButtons; std::map> pressedSliders; std::shared_ptr focusedTextField; struct MenuState { std::shared_ptr root; std::vector> buttons; std::vector> textButtons; std::vector> sliders; std::vector> textViews; std::vector> textFields; std::vector> staticImages; std::map> pressedButtons; std::map> pressedTextButtons; std::map> pressedSliders; std::shared_ptr focusedTextField; std::string path; std::map, std::function> animCallbacks; }; std::vector menuStack; }; } // namespace ZL