space-game001/src/UiManager.h
2026-06-01 22:03:05 +03:00

499 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 <variant>
#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;
// Optional explicit hit-test zone, centred on rect.
// 0 means "use rect.w / rect.h" (default behaviour).
float clickZoneWidth = 0.0f;
float clickZoneHeight = 0.0f;
UiRect getClickZoneRect() const {
float cw = (clickZoneWidth > 0.0f) ? clickZoneWidth : rect.w;
float ch = (clickZoneHeight > 0.0f) ? clickZoneHeight : rect.h;
return { rect.x + (rect.w - cw) * 0.5f, rect.y + (rect.h - ch) * 0.5f, cw, ch };
}
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;
bool fillMode = false; // track width = value * rect.w (display-only bar)
bool interactive = true; // false = ignore touch events
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;
// Optional explicit hit-test zone, centred on rect.
// 0 means "use rect.w / rect.h" (default behaviour).
float clickZoneWidth = 0.0f;
float clickZoneHeight = 0.0f;
UiRect getClickZoneRect() const {
float cw = (clickZoneWidth > 0.0f) ? clickZoneWidth : rect.w;
float ch = (clickZoneHeight > 0.0f) ? clickZoneHeight : rect.h;
return { rect.x + (rect.w - cw) * 0.5f, rect.y + (rect.h - ch) * 0.5f, cw, ch };
}
// 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;
float textPaddingX = 12.0f;
float textPaddingY = 0.0f;
bool wrap = false;
bool topAligned = false;
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 textCentered = true;
bool wrap = false;
bool topAligned = true;
float paddingX = 0.0f;
float paddingY = 0.0f;
int maxLines = 0; // 0 = no line limit
std::unique_ptr<TextRenderer> textRenderer;
void draw(Renderer& renderer) const;
};
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;
// Fade-in on first display (triggers each time the containing UI is shown)
bool fadeInEnabled = false;
float fadeInDurationMs = 1000.0f;
float fadeInElapsedMs = 0.0f; // runtime, reset by collectButtonsAndSliders
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;
float scaleX = 1.0f;
float scaleY = 1.0f;
bool visible = true;
// Pulse-scale animation (StaticImage only; auto-starts when pulseEnabled == true)
bool pulseEnabled = false;
float pulseMinScale = 1.0f;
float pulseMaxScale = 1.1f;
float pulsePeriodMs = 1000.0f;
float pulseElapsedMs = 0.0f; // runtime, not persisted in JSON
// Pop-in scale animation (scale 0→1 on first reveal)
bool popInActive = false;
float popInProgress = 0.0f; // 0..1
float popInDurationMs = 300.0f;
// Иерархия
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 appendFromFile(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;
}
}
for (auto& n : pulsingNodes) {
if (n) {
n->scaleX = 1.0f;
n->scaleY = 1.0f;
n->pulseElapsedMs = 0.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 setTextButtonColor(const std::string& name, const std::array<float, 4>& color);
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);
bool setTextColor(const std::string& name, const std::array<float, 4>& color);
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();
void startPopIn(const std::string& nodeName, float durationMs = 300.0f);
std::shared_ptr<UiNode> findNode(const std::string& name);
bool setNodeVisible(const std::string& nodeName, bool visible);
bool getNodeVisible(const std::string& nodeName);
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;
};
using AnyButton = std::variant<std::shared_ptr<UiButton>, std::shared_ptr<UiTextButton>>;
std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiTextButton>> textButtons;
// All buttons and textButtons in DFS declaration order.
// onTouchDown iterates this in reverse so later-declared elements have higher priority.
std::vector<AnyButton> allInteractives;
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::vector<std::shared_ptr<UiNode>> pulsingNodes;
std::vector<std::shared_ptr<UiNode>> popInNodes;
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<AnyButton> allInteractives;
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::vector<std::shared_ptr<UiNode>> pulsingNodes;
std::vector<std::shared_ptr<UiNode>> popInNodes;
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