421 lines
13 KiB
C++
421 lines
13 KiB
C++
#pragma once
|
||
|
||
#include "render/Renderer.h"
|
||
#include "render/TextureManager.h"
|
||
#include "render/TextRenderer.h"
|
||
#include "Environment.h"
|
||
#include "external/nlohmann/json.hpp"
|
||
#include <string>
|
||
#include <vector>
|
||
#include <memory>
|
||
#include <functional>
|
||
#include <map>
|
||
#include <cstdint>
|
||
|
||
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<Texture> texNormal;
|
||
std::shared_ptr<Texture> texHover;
|
||
std::shared_ptr<Texture> texPressed;
|
||
std::shared_ptr<Texture> texDisabled;
|
||
ButtonState state = ButtonState::Normal;
|
||
|
||
VertexRenderStruct mesh;
|
||
|
||
std::function<void(const std::string&)> onClick;
|
||
std::function<void(const std::string&)> 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<Texture> texTrack;
|
||
std::shared_ptr<Texture> texKnob;
|
||
|
||
VertexRenderStruct trackMesh;
|
||
VertexRenderStruct knobMesh;
|
||
|
||
float value = 0.0f;
|
||
bool vertical = true;
|
||
|
||
std::function<void(const std::string&, float)> 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<Texture> texNormal;
|
||
std::shared_ptr<Texture> texHover;
|
||
std::shared_ptr<Texture> texPressed;
|
||
std::shared_ptr<Texture> 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<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
|
||
bool textCentered = true;
|
||
|
||
std::unique_ptr<TextRenderer> textRenderer;
|
||
|
||
std::function<void(const std::string&)> onClick;
|
||
std::function<void(const std::string&)> 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<float, 4> color = { 1.f, 1.f, 1.f, 1.f }; // rgba
|
||
bool centered = true;
|
||
|
||
std::unique_ptr<TextRenderer> 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<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
|
||
std::array<float, 4> placeholderColor = { 0.5f, 0.5f, 0.5f, 1.f };
|
||
std::array<float, 4> backgroundColor = { 0.2f, 0.2f, 0.2f, 1.f };
|
||
std::array<float, 4> borderColor = { 0.5f, 0.5f, 0.5f, 1.f };
|
||
int maxLength = 256;
|
||
bool focused = false;
|
||
|
||
std::unique_ptr<TextRenderer> textRenderer;
|
||
std::function<void(const std::string&, const std::string&)> onTextChanged;
|
||
|
||
void draw(Renderer& renderer) const;
|
||
};
|
||
|
||
struct UiStaticImage {
|
||
std::string name;
|
||
UiRect rect;
|
||
std::shared_ptr<Texture> 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<std::shared_ptr<UiNode>> children;
|
||
|
||
// Компоненты (только один из них обычно активен для ноды)
|
||
std::shared_ptr<UiButton> button;
|
||
std::shared_ptr<UiTextButton> textButton;
|
||
std::shared_ptr<UiSlider> slider;
|
||
std::shared_ptr<UiTextView> textView;
|
||
std::shared_ptr<UiTextField> textField;
|
||
std::shared_ptr<UiStaticImage> 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<AnimStep> steps;
|
||
bool repeat = false;
|
||
};
|
||
std::map<std::string, AnimSequence> animations;
|
||
};
|
||
|
||
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile);
|
||
std::shared_ptr<UiNode> 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<UiNode> 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<UiButton> findButton(const std::string& name);
|
||
|
||
bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
|
||
bool setButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb);
|
||
|
||
std::shared_ptr<UiTextButton> findTextButton(const std::string& name);
|
||
bool setTextButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
|
||
bool setTextButtonPressCallback(const std::string& name, std::function<void(const std::string&)> 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<UiSlider> findSlider(const std::string& name);
|
||
bool setSliderCallback(const std::string& name, std::function<void(const std::string&, float)> cb);
|
||
bool setSliderValue(const std::string& name, float value); // programmatic set (clamped 0..1)
|
||
|
||
std::shared_ptr<UiTextView> findTextView(const std::string& name);
|
||
bool setText(const std::string& name, const std::string& newText);
|
||
|
||
std::shared_ptr<UiTextField> findTextField(const std::string& name);
|
||
bool setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb);
|
||
std::string getTextFieldValue(const std::string& name);
|
||
|
||
std::shared_ptr<UiStaticImage> findStaticImage(const std::string& name);
|
||
|
||
bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
||
bool pushMenuFromSavedRoot(std::shared_ptr<UiNode> 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<void()> cb);
|
||
void updateAllLayouts();
|
||
|
||
private:
|
||
void layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY);
|
||
void syncComponentRects(const std::shared_ptr<UiNode>& node);
|
||
void collectButtonsAndSliders(const std::shared_ptr<UiNode>& 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<void()> onComplete;
|
||
|
||
float origOffsetX = 0.0f;
|
||
float origOffsetY = 0.0f;
|
||
float origScaleX = 1.0f;
|
||
float origScaleY = 1.0f;
|
||
bool stepStarted = false;
|
||
};
|
||
|
||
std::shared_ptr<UiNode> root;
|
||
std::vector<std::shared_ptr<UiButton>> buttons;
|
||
std::vector<std::shared_ptr<UiTextButton>> textButtons;
|
||
std::vector<std::shared_ptr<UiSlider>> sliders;
|
||
std::vector<std::shared_ptr<UiTextView>> textViews;
|
||
std::vector<std::shared_ptr<UiTextField>> textFields;
|
||
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
|
||
|
||
std::map<std::shared_ptr<UiNode>, std::vector<ActiveAnim>> nodeActiveAnims;
|
||
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName)
|
||
|
||
// Per-finger tracking for multi-touch support
|
||
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
|
||
std::map<int64_t, std::shared_ptr<UiTextButton>> pressedTextButtons;
|
||
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
|
||
std::shared_ptr<UiTextField> focusedTextField;
|
||
|
||
struct MenuState {
|
||
std::shared_ptr<UiNode> root;
|
||
std::vector<std::shared_ptr<UiButton>> buttons;
|
||
std::vector<std::shared_ptr<UiTextButton>> textButtons;
|
||
std::vector<std::shared_ptr<UiSlider>> sliders;
|
||
std::vector<std::shared_ptr<UiTextView>> textViews;
|
||
std::vector<std::shared_ptr<UiTextField>> textFields;
|
||
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
|
||
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
|
||
std::map<int64_t, std::shared_ptr<UiTextButton>> pressedTextButtons;
|
||
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
|
||
std::shared_ptr<UiTextField> focusedTextField;
|
||
std::string path;
|
||
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks;
|
||
};
|
||
|
||
std::vector<MenuState> menuStack;
|
||
|
||
};
|
||
|
||
} // namespace ZL
|