Add XCUI runtime screen layer and demo textarea
This commit is contained in:
@@ -516,6 +516,13 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputRouter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIShortcutRegistry.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputDispatcher.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenPlayer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UISystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UIScreenDocumentHost.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UIScreenPlayer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Runtime/UISystem.cpp
|
||||
|
||||
# Input
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Input/InputTypes.h
|
||||
|
||||
29
engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h
Normal file
29
engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Runtime {
|
||||
|
||||
class IUIScreenDocumentHost {
|
||||
public:
|
||||
virtual ~IUIScreenDocumentHost() = default;
|
||||
|
||||
virtual UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) = 0;
|
||||
virtual UIScreenFrameResult BuildFrame(
|
||||
const UIScreenDocument& document,
|
||||
const UIScreenFrameInput& input) = 0;
|
||||
};
|
||||
|
||||
class UIDocumentScreenHost final : public IUIScreenDocumentHost {
|
||||
public:
|
||||
UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override;
|
||||
UIScreenFrameResult BuildFrame(
|
||||
const UIScreenDocument& document,
|
||||
const UIScreenFrameInput& input) override;
|
||||
};
|
||||
|
||||
} // namespace Runtime
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
39
engine/include/XCEngine/UI/Runtime/UIScreenPlayer.h
Normal file
39
engine/include/XCEngine/UI/Runtime/UIScreenPlayer.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Runtime {
|
||||
|
||||
class UIScreenPlayer {
|
||||
public:
|
||||
explicit UIScreenPlayer(IUIScreenDocumentHost& documentHost);
|
||||
|
||||
bool Load(const UIScreenAsset& asset);
|
||||
void Unload();
|
||||
|
||||
bool IsLoaded() const;
|
||||
const UIScreenAsset* GetAsset() const;
|
||||
const UIScreenDocument* GetDocument() const;
|
||||
const UIScreenFrameResult& GetLastFrame() const;
|
||||
const std::string& GetLastError() const;
|
||||
std::uint64_t GetPresentedFrameCount() const;
|
||||
|
||||
const UIScreenFrameResult& Update(const UIScreenFrameInput& input);
|
||||
|
||||
private:
|
||||
IUIScreenDocumentHost* m_documentHost = nullptr;
|
||||
UIScreenAsset m_asset = {};
|
||||
UIScreenDocument m_document = {};
|
||||
UIScreenFrameResult m_lastFrame = {};
|
||||
std::string m_lastError = {};
|
||||
std::uint64_t m_presentedFrameCount = 0;
|
||||
};
|
||||
|
||||
} // namespace Runtime
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
100
engine/include/XCEngine/UI/Runtime/UIScreenTypes.h
Normal file
100
engine/include/XCEngine/UI/Runtime/UIScreenTypes.h
Normal file
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Runtime {
|
||||
|
||||
using UIScreenLayerId = std::uint64_t;
|
||||
|
||||
struct UIScreenAsset {
|
||||
std::string screenId = {};
|
||||
std::string documentPath = {};
|
||||
std::string themePath = {};
|
||||
|
||||
bool IsValid() const {
|
||||
return !documentPath.empty();
|
||||
}
|
||||
};
|
||||
|
||||
struct UIScreenDocument {
|
||||
std::string sourcePath = {};
|
||||
std::string displayName = {};
|
||||
std::vector<std::string> dependencies = {};
|
||||
Resources::UIDocumentModel viewDocument = {};
|
||||
Resources::UIDocumentModel themeDocument = {};
|
||||
bool hasThemeDocument = false;
|
||||
|
||||
bool IsValid() const {
|
||||
return !sourcePath.empty();
|
||||
}
|
||||
|
||||
const Resources::UIDocumentModel* GetThemeDocument() const {
|
||||
return hasThemeDocument ? &themeDocument : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct UIScreenLoadResult {
|
||||
bool succeeded = false;
|
||||
std::string errorMessage = {};
|
||||
UIScreenDocument document = {};
|
||||
};
|
||||
|
||||
struct UIScreenFrameInput {
|
||||
UIRect viewportRect = {};
|
||||
std::vector<UIInputEvent> events = {};
|
||||
double deltaTimeSeconds = 0.0;
|
||||
std::uint64_t frameIndex = 0;
|
||||
bool focused = false;
|
||||
};
|
||||
|
||||
struct UIScreenFrameStats {
|
||||
bool documentLoaded = false;
|
||||
std::size_t drawListCount = 0;
|
||||
std::size_t commandCount = 0;
|
||||
std::size_t inputEventCount = 0;
|
||||
std::size_t nodeCount = 0;
|
||||
std::size_t filledRectCommandCount = 0;
|
||||
std::size_t textCommandCount = 0;
|
||||
std::uint64_t presentedFrameIndex = 0;
|
||||
};
|
||||
|
||||
struct UIScreenFrameResult {
|
||||
UIDrawData drawData = {};
|
||||
UIScreenFrameStats stats = {};
|
||||
std::string errorMessage = {};
|
||||
};
|
||||
|
||||
struct UIScreenLayerOptions {
|
||||
std::string debugName = {};
|
||||
bool visible = true;
|
||||
bool acceptsInput = true;
|
||||
bool blocksLayersBelow = false;
|
||||
};
|
||||
|
||||
struct UISystemPresentedLayer {
|
||||
UIScreenLayerId layerId = 0;
|
||||
UIScreenAsset asset = {};
|
||||
UIScreenLayerOptions options = {};
|
||||
UIScreenFrameStats stats = {};
|
||||
};
|
||||
|
||||
struct UISystemFrameResult {
|
||||
UIDrawData drawData = {};
|
||||
std::vector<UISystemPresentedLayer> layers = {};
|
||||
std::size_t presentedLayerCount = 0;
|
||||
std::size_t skippedLayerCount = 0;
|
||||
std::uint64_t frameIndex = 0;
|
||||
std::string errorMessage = {};
|
||||
};
|
||||
|
||||
} // namespace Runtime
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
50
engine/include/XCEngine/UI/Runtime/UISystem.h
Normal file
50
engine/include/XCEngine/UI/Runtime/UISystem.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Runtime {
|
||||
|
||||
class UISystem {
|
||||
public:
|
||||
explicit UISystem(IUIScreenDocumentHost& documentHost);
|
||||
|
||||
UIScreenPlayer& CreatePlayer(const UIScreenLayerOptions& options = UIScreenLayerOptions());
|
||||
UIScreenLayerId PushScreen(
|
||||
const UIScreenAsset& asset,
|
||||
const UIScreenLayerOptions& options = UIScreenLayerOptions());
|
||||
bool RemoveLayer(UIScreenLayerId layerId);
|
||||
bool SetLayerVisibility(UIScreenLayerId layerId, bool visible);
|
||||
bool SetLayerOptions(UIScreenLayerId layerId, const UIScreenLayerOptions& options);
|
||||
const UIScreenLayerOptions* FindLayerOptions(UIScreenLayerId layerId) const;
|
||||
UIScreenLayerId GetLayerId(std::size_t index) const;
|
||||
void DestroyAllPlayers();
|
||||
|
||||
std::size_t GetPlayerCount() const;
|
||||
std::size_t GetLayerCount() const;
|
||||
|
||||
const UISystemFrameResult& Update(const UIScreenFrameInput& input);
|
||||
void Tick(const UIScreenFrameInput& input);
|
||||
const UISystemFrameResult& GetLastFrame() const;
|
||||
|
||||
const std::vector<std::unique_ptr<UIScreenPlayer>>& GetPlayers() const;
|
||||
|
||||
private:
|
||||
std::size_t FindLayerIndex(UIScreenLayerId layerId) const;
|
||||
|
||||
IUIScreenDocumentHost* m_documentHost = nullptr;
|
||||
std::vector<std::unique_ptr<UIScreenPlayer>> m_players = {};
|
||||
std::vector<UIScreenLayerId> m_layerIds = {};
|
||||
std::vector<UIScreenLayerOptions> m_layerOptions = {};
|
||||
UISystemFrameResult m_lastFrame = {};
|
||||
UIScreenLayerId m_nextLayerId = 1;
|
||||
};
|
||||
|
||||
} // namespace Runtime
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
531
engine/src/UI/Runtime/UIScreenDocumentHost.cpp
Normal file
531
engine/src/UI/Runtime/UIScreenDocumentHost.cpp
Normal file
@@ -0,0 +1,531 @@
|
||||
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
#include <XCEngine/UI/Layout/LayoutEngine.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Runtime {
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Math::Color;
|
||||
using XCEngine::Resources::CompileUIDocument;
|
||||
using XCEngine::Resources::GetUIDocumentDefaultRootTag;
|
||||
using XCEngine::Resources::UIDocumentAttribute;
|
||||
using XCEngine::Resources::UIDocumentCompileRequest;
|
||||
using XCEngine::Resources::UIDocumentCompileResult;
|
||||
using XCEngine::Resources::UIDocumentKind;
|
||||
using XCEngine::Resources::UIDocumentNode;
|
||||
namespace Layout = XCEngine::UI::Layout;
|
||||
|
||||
constexpr float kDefaultFontSize = 16.0f;
|
||||
constexpr float kSmallFontSize = 13.0f;
|
||||
constexpr float kApproximateGlyphWidth = 0.56f;
|
||||
|
||||
struct RuntimeLayoutNode {
|
||||
const UIDocumentNode* source = nullptr;
|
||||
std::vector<RuntimeLayoutNode> children = {};
|
||||
UISize desiredSize = {};
|
||||
UIRect rect = {};
|
||||
};
|
||||
|
||||
UIColor ToUIColor(const Color& color) {
|
||||
return UIColor(color.r, color.g, color.b, color.a);
|
||||
}
|
||||
|
||||
std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
bool IsUtf8ContinuationByte(unsigned char value) {
|
||||
return (value & 0xC0u) == 0x80u;
|
||||
}
|
||||
|
||||
std::size_t CountUtf8Codepoints(const std::string& text) {
|
||||
std::size_t count = 0u;
|
||||
for (unsigned char ch : text) {
|
||||
if (!IsUtf8ContinuationByte(ch)) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
float MeasureTextWidth(const std::string& text, float fontSize) {
|
||||
return fontSize * kApproximateGlyphWidth * static_cast<float>(CountUtf8Codepoints(text));
|
||||
}
|
||||
|
||||
float MeasureTextHeight(float fontSize) {
|
||||
return fontSize + 6.0f;
|
||||
}
|
||||
|
||||
const UIDocumentAttribute* FindAttribute(const UIDocumentNode& node, const char* name) {
|
||||
for (const UIDocumentAttribute& attribute : node.attributes) {
|
||||
if (attribute.name == name) {
|
||||
return &attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string GetAttribute(
|
||||
const UIDocumentNode& node,
|
||||
const char* name,
|
||||
const std::string& fallback = {}) {
|
||||
const UIDocumentAttribute* attribute = FindAttribute(node, name);
|
||||
return attribute != nullptr ? ToStdString(attribute->value) : fallback;
|
||||
}
|
||||
|
||||
bool TryParseFloat(const std::string& text, float& outValue) {
|
||||
if (text.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char* end = nullptr;
|
||||
const float parsed = std::strtof(text.c_str(), &end);
|
||||
if (end == text.c_str()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (*end != '\0') {
|
||||
if (!std::isspace(static_cast<unsigned char>(*end))) {
|
||||
return false;
|
||||
}
|
||||
++end;
|
||||
}
|
||||
|
||||
outValue = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
float ParseFloatAttribute(
|
||||
const UIDocumentNode& node,
|
||||
const char* name,
|
||||
float fallback) {
|
||||
float value = fallback;
|
||||
return TryParseFloat(GetAttribute(node, name), value) ? value : fallback;
|
||||
}
|
||||
|
||||
Layout::UILayoutLength ParseLengthAttribute(
|
||||
const UIDocumentNode& node,
|
||||
const char* name) {
|
||||
const std::string value = GetAttribute(node, name);
|
||||
if (value == "stretch" || value == "fill") {
|
||||
return Layout::UILayoutLength::Stretch();
|
||||
}
|
||||
|
||||
float pixels = 0.0f;
|
||||
return TryParseFloat(value, pixels)
|
||||
? Layout::UILayoutLength::Pixels(pixels)
|
||||
: Layout::UILayoutLength::Auto();
|
||||
}
|
||||
|
||||
Layout::UILayoutThickness ParsePadding(
|
||||
const UIDocumentNode& node,
|
||||
float fallback) {
|
||||
return Layout::UILayoutThickness::Uniform(ParseFloatAttribute(node, "padding", fallback));
|
||||
}
|
||||
|
||||
std::string ResolveNodeText(const UIDocumentNode& node) {
|
||||
const std::string text = GetAttribute(node, "text");
|
||||
if (!text.empty()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const std::string title = GetAttribute(node, "title");
|
||||
if (!title.empty()) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return ToStdString(node.tagName);
|
||||
}
|
||||
|
||||
bool IsHorizontalTag(const std::string& tagName) {
|
||||
return tagName == "Row";
|
||||
}
|
||||
|
||||
bool IsContainerTag(const UIDocumentNode& node) {
|
||||
if (node.children.Size() > 0u) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string tagName = ToStdString(node.tagName);
|
||||
return tagName == "View" ||
|
||||
tagName == "Column" ||
|
||||
tagName == "Row" ||
|
||||
tagName == "Card" ||
|
||||
tagName == "Button";
|
||||
}
|
||||
|
||||
Color ResolveBackgroundColor(const UIDocumentNode& node) {
|
||||
const std::string tone = GetAttribute(node, "tone");
|
||||
const std::string tagName = ToStdString(node.tagName);
|
||||
|
||||
if (tagName == "View") {
|
||||
return Color(0.08f, 0.09f, 0.11f, 1.0f);
|
||||
}
|
||||
if (tone == "accent") {
|
||||
return Color(0.19f, 0.31f, 0.52f, 1.0f);
|
||||
}
|
||||
if (tone == "accent-alt") {
|
||||
return Color(0.24f, 0.26f, 0.39f, 1.0f);
|
||||
}
|
||||
if (tagName == "Button") {
|
||||
return Color(0.20f, 0.23f, 0.29f, 1.0f);
|
||||
}
|
||||
|
||||
return Color(0.13f, 0.15f, 0.18f, 1.0f);
|
||||
}
|
||||
|
||||
Color ResolveBorderColor(const UIDocumentNode& node) {
|
||||
const std::string tone = GetAttribute(node, "tone");
|
||||
if (tone == "accent") {
|
||||
return Color(0.31f, 0.56f, 0.90f, 1.0f);
|
||||
}
|
||||
if (tone == "accent-alt") {
|
||||
return Color(0.47f, 0.51f, 0.76f, 1.0f);
|
||||
}
|
||||
|
||||
return Color(0.25f, 0.28f, 0.33f, 1.0f);
|
||||
}
|
||||
|
||||
RuntimeLayoutNode BuildLayoutTree(const UIDocumentNode& source) {
|
||||
RuntimeLayoutNode node = {};
|
||||
node.source = &source;
|
||||
node.children.reserve(source.children.Size());
|
||||
for (const UIDocumentNode& child : source.children) {
|
||||
node.children.push_back(BuildLayoutTree(child));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
UISize MeasureNode(RuntimeLayoutNode& node) {
|
||||
const UIDocumentNode& source = *node.source;
|
||||
const std::string tagName = ToStdString(source.tagName);
|
||||
|
||||
if (tagName == "Text") {
|
||||
const std::string text = ResolveNodeText(source);
|
||||
node.desiredSize = UISize(
|
||||
MeasureTextWidth(text, kDefaultFontSize),
|
||||
MeasureTextHeight(kDefaultFontSize));
|
||||
return node.desiredSize;
|
||||
}
|
||||
|
||||
if (!IsContainerTag(source)) {
|
||||
node.desiredSize = UISize(
|
||||
(std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + 24.0f),
|
||||
44.0f);
|
||||
return node.desiredSize;
|
||||
}
|
||||
|
||||
Layout::UIStackLayoutOptions options = {};
|
||||
options.axis = IsHorizontalTag(tagName)
|
||||
? Layout::UILayoutAxis::Horizontal
|
||||
: Layout::UILayoutAxis::Vertical;
|
||||
options.spacing = ParseFloatAttribute(
|
||||
source,
|
||||
"gap",
|
||||
options.axis == Layout::UILayoutAxis::Horizontal ? 10.0f : 8.0f);
|
||||
options.padding = ParsePadding(
|
||||
source,
|
||||
tagName == "View" ? 16.0f : 12.0f);
|
||||
|
||||
std::vector<Layout::UILayoutItem> items = {};
|
||||
items.reserve(node.children.size());
|
||||
for (RuntimeLayoutNode& child : node.children) {
|
||||
Layout::UILayoutItem item = {};
|
||||
item.desiredContentSize = MeasureNode(child);
|
||||
item.width = ParseLengthAttribute(*child.source, "width");
|
||||
item.height = ParseLengthAttribute(*child.source, "height");
|
||||
items.push_back(item);
|
||||
}
|
||||
|
||||
const auto measured = Layout::MeasureStackLayout(options, items);
|
||||
node.desiredSize = measured.desiredSize;
|
||||
|
||||
const std::string title = GetAttribute(source, "title");
|
||||
const std::string subtitle = GetAttribute(source, "subtitle");
|
||||
float headerHeight = 0.0f;
|
||||
if (!title.empty()) {
|
||||
headerHeight += MeasureTextHeight(kDefaultFontSize);
|
||||
}
|
||||
if (!subtitle.empty()) {
|
||||
if (headerHeight > 0.0f) {
|
||||
headerHeight += 4.0f;
|
||||
}
|
||||
headerHeight += MeasureTextHeight(kSmallFontSize);
|
||||
}
|
||||
|
||||
node.desiredSize.width = (std::max)(
|
||||
node.desiredSize.width,
|
||||
MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
||||
node.desiredSize.height += headerHeight;
|
||||
|
||||
float explicitWidth = 0.0f;
|
||||
if (TryParseFloat(GetAttribute(source, "width"), explicitWidth)) {
|
||||
node.desiredSize.width = explicitWidth;
|
||||
}
|
||||
|
||||
float explicitHeight = 0.0f;
|
||||
if (TryParseFloat(GetAttribute(source, "height"), explicitHeight)) {
|
||||
node.desiredSize.height = explicitHeight;
|
||||
}
|
||||
|
||||
return node.desiredSize;
|
||||
}
|
||||
|
||||
void ArrangeNode(RuntimeLayoutNode& node, const UIRect& rect) {
|
||||
node.rect = rect;
|
||||
|
||||
const UIDocumentNode& source = *node.source;
|
||||
if (!IsContainerTag(source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string tagName = ToStdString(source.tagName);
|
||||
Layout::UIStackLayoutOptions options = {};
|
||||
options.axis = IsHorizontalTag(tagName)
|
||||
? Layout::UILayoutAxis::Horizontal
|
||||
: Layout::UILayoutAxis::Vertical;
|
||||
options.spacing = ParseFloatAttribute(
|
||||
source,
|
||||
"gap",
|
||||
options.axis == Layout::UILayoutAxis::Horizontal ? 10.0f : 8.0f);
|
||||
options.padding = ParsePadding(
|
||||
source,
|
||||
tagName == "View" ? 16.0f : 12.0f);
|
||||
|
||||
float headerHeight = 0.0f;
|
||||
if (!GetAttribute(source, "title").empty()) {
|
||||
headerHeight += MeasureTextHeight(kDefaultFontSize);
|
||||
}
|
||||
if (!GetAttribute(source, "subtitle").empty()) {
|
||||
if (headerHeight > 0.0f) {
|
||||
headerHeight += 4.0f;
|
||||
}
|
||||
headerHeight += MeasureTextHeight(kSmallFontSize);
|
||||
}
|
||||
|
||||
UIRect contentRect = rect;
|
||||
contentRect.y += headerHeight;
|
||||
contentRect.height = (std::max)(0.0f, rect.height - headerHeight);
|
||||
|
||||
std::vector<Layout::UILayoutItem> items = {};
|
||||
items.reserve(node.children.size());
|
||||
for (RuntimeLayoutNode& child : node.children) {
|
||||
Layout::UILayoutItem item = {};
|
||||
item.desiredContentSize = child.desiredSize;
|
||||
item.width = ParseLengthAttribute(*child.source, "width");
|
||||
item.height = ParseLengthAttribute(*child.source, "height");
|
||||
items.push_back(item);
|
||||
}
|
||||
|
||||
const auto arranged = Layout::ArrangeStackLayout(options, items, contentRect);
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
ArrangeNode(node.children[index], arranged.children[index].arrangedRect);
|
||||
}
|
||||
}
|
||||
|
||||
void EmitNode(
|
||||
const RuntimeLayoutNode& node,
|
||||
UIDrawList& drawList,
|
||||
UIScreenFrameStats& stats) {
|
||||
const UIDocumentNode& source = *node.source;
|
||||
const std::string tagName = ToStdString(source.tagName);
|
||||
|
||||
++stats.nodeCount;
|
||||
|
||||
if (tagName == "View" || tagName == "Card" || tagName == "Button") {
|
||||
drawList.AddFilledRect(node.rect, ToUIColor(ResolveBackgroundColor(source)), 10.0f);
|
||||
++stats.filledRectCommandCount;
|
||||
|
||||
if (tagName != "View") {
|
||||
drawList.AddRectOutline(node.rect, ToUIColor(ResolveBorderColor(source)), 1.0f, 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
const std::string title = GetAttribute(source, "title");
|
||||
const std::string subtitle = GetAttribute(source, "subtitle");
|
||||
float textY = node.rect.y + 12.0f;
|
||||
if (!title.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(node.rect.x + 12.0f, textY),
|
||||
title,
|
||||
ToUIColor(Color(0.94f, 0.95f, 0.97f, 1.0f)),
|
||||
kDefaultFontSize);
|
||||
++stats.textCommandCount;
|
||||
textY += MeasureTextHeight(kDefaultFontSize) + 2.0f;
|
||||
}
|
||||
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(node.rect.x + 12.0f, textY),
|
||||
subtitle,
|
||||
ToUIColor(Color(0.67f, 0.70f, 0.76f, 1.0f)),
|
||||
kSmallFontSize);
|
||||
++stats.textCommandCount;
|
||||
}
|
||||
|
||||
if (tagName == "Text") {
|
||||
drawList.AddText(
|
||||
UIPoint(node.rect.x, node.rect.y),
|
||||
ResolveNodeText(source),
|
||||
ToUIColor(Color(0.92f, 0.94f, 0.97f, 1.0f)),
|
||||
kDefaultFontSize);
|
||||
++stats.textCommandCount;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
EmitNode(child, drawList, stats);
|
||||
}
|
||||
}
|
||||
|
||||
std::string ResolveDisplayName(
|
||||
const UIScreenAsset& asset,
|
||||
const UIDocumentCompileResult& viewResult) {
|
||||
if (!asset.screenId.empty()) {
|
||||
return asset.screenId;
|
||||
}
|
||||
|
||||
if (!viewResult.document.displayName.Empty()) {
|
||||
return ToStdString(viewResult.document.displayName);
|
||||
}
|
||||
|
||||
if (!viewResult.document.sourcePath.Empty()) {
|
||||
return std::filesystem::path(viewResult.document.sourcePath.CStr()).stem().string();
|
||||
}
|
||||
|
||||
return "UIScreen";
|
||||
}
|
||||
|
||||
void AppendDependencies(
|
||||
const Containers::Array<Containers::String>& dependencies,
|
||||
std::unordered_set<std::string>& seenDependencies,
|
||||
std::vector<std::string>& outDependencies) {
|
||||
for (const Containers::String& dependency : dependencies) {
|
||||
const std::string value = ToStdString(dependency);
|
||||
if (value.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seenDependencies.insert(value).second) {
|
||||
outDependencies.push_back(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIScreenLoadResult UIDocumentScreenHost::LoadScreen(const UIScreenAsset& asset) {
|
||||
UIScreenLoadResult result = {};
|
||||
if (!asset.IsValid()) {
|
||||
result.errorMessage = "UIScreenAsset is invalid.";
|
||||
return result;
|
||||
}
|
||||
|
||||
UIDocumentCompileRequest viewRequest = {};
|
||||
viewRequest.kind = UIDocumentKind::View;
|
||||
viewRequest.path = Containers::String(asset.documentPath.c_str());
|
||||
viewRequest.expectedRootTag = GetUIDocumentDefaultRootTag(viewRequest.kind);
|
||||
|
||||
UIDocumentCompileResult viewResult = {};
|
||||
if (!CompileUIDocument(viewRequest, viewResult) ||
|
||||
!viewResult.succeeded ||
|
||||
!viewResult.document.valid) {
|
||||
result.errorMessage = viewResult.errorMessage.Empty()
|
||||
? "Failed to compile UI screen view document."
|
||||
: ToStdString(viewResult.errorMessage);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.succeeded = true;
|
||||
result.document.sourcePath = asset.documentPath;
|
||||
result.document.displayName = ResolveDisplayName(asset, viewResult);
|
||||
result.document.viewDocument = viewResult.document;
|
||||
|
||||
std::unordered_set<std::string> seenDependencies = {};
|
||||
AppendDependencies(
|
||||
viewResult.document.dependencies,
|
||||
seenDependencies,
|
||||
result.document.dependencies);
|
||||
|
||||
if (!asset.themePath.empty()) {
|
||||
UIDocumentCompileRequest themeRequest = {};
|
||||
themeRequest.kind = UIDocumentKind::Theme;
|
||||
themeRequest.path = Containers::String(asset.themePath.c_str());
|
||||
themeRequest.expectedRootTag = GetUIDocumentDefaultRootTag(themeRequest.kind);
|
||||
|
||||
UIDocumentCompileResult themeResult = {};
|
||||
if (!CompileUIDocument(themeRequest, themeResult) ||
|
||||
!themeResult.succeeded ||
|
||||
!themeResult.document.valid) {
|
||||
result = {};
|
||||
result.errorMessage = themeResult.errorMessage.Empty()
|
||||
? "Failed to compile UI screen theme document."
|
||||
: ToStdString(themeResult.errorMessage);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.document.themeDocument = themeResult.document;
|
||||
result.document.hasThemeDocument = true;
|
||||
if (seenDependencies.insert(asset.themePath).second) {
|
||||
result.document.dependencies.push_back(asset.themePath);
|
||||
}
|
||||
AppendDependencies(
|
||||
themeResult.document.dependencies,
|
||||
seenDependencies,
|
||||
result.document.dependencies);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
|
||||
const UIScreenDocument& document,
|
||||
const UIScreenFrameInput& input) {
|
||||
UIScreenFrameResult result = {};
|
||||
if (!document.viewDocument.valid || document.viewDocument.rootNode.tagName.Empty()) {
|
||||
result.errorMessage = "UIScreenDocument does not contain a compiled view document.";
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode);
|
||||
MeasureNode(root);
|
||||
|
||||
UIRect viewportRect = input.viewportRect;
|
||||
if (viewportRect.width <= 0.0f) {
|
||||
viewportRect.width = (std::max)(640.0f, root.desiredSize.width);
|
||||
}
|
||||
if (viewportRect.height <= 0.0f) {
|
||||
viewportRect.height = (std::max)(360.0f, root.desiredSize.height);
|
||||
}
|
||||
|
||||
ArrangeNode(root, viewportRect);
|
||||
|
||||
UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
|
||||
EmitNode(root, drawList, result.stats);
|
||||
|
||||
result.stats.documentLoaded = true;
|
||||
result.stats.drawListCount = result.drawData.GetDrawListCount();
|
||||
result.stats.commandCount = result.drawData.GetTotalCommandCount();
|
||||
result.stats.inputEventCount = input.events.size();
|
||||
result.stats.presentedFrameIndex = input.frameIndex;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Runtime
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
106
engine/src/UI/Runtime/UIScreenPlayer.cpp
Normal file
106
engine/src/UI/Runtime/UIScreenPlayer.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Runtime {
|
||||
|
||||
namespace {
|
||||
|
||||
UIScreenFrameResult MakeNotLoadedFrame(const UIScreenFrameInput& input) {
|
||||
UIScreenFrameResult frame = {};
|
||||
frame.errorMessage = "UIScreenPlayer has no loaded screen document.";
|
||||
frame.stats.documentLoaded = false;
|
||||
frame.stats.inputEventCount = input.events.size();
|
||||
frame.stats.presentedFrameIndex = input.frameIndex;
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIScreenPlayer::UIScreenPlayer(IUIScreenDocumentHost& documentHost)
|
||||
: m_documentHost(&documentHost) {
|
||||
}
|
||||
|
||||
bool UIScreenPlayer::Load(const UIScreenAsset& asset) {
|
||||
if (m_documentHost == nullptr) {
|
||||
m_lastError = "UIScreenPlayer has no document host.";
|
||||
m_document = {};
|
||||
m_asset = {};
|
||||
m_lastFrame = {};
|
||||
m_presentedFrameCount = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIScreenLoadResult loadResult = m_documentHost->LoadScreen(asset);
|
||||
if (!loadResult.succeeded || !loadResult.document.IsValid()) {
|
||||
m_asset = {};
|
||||
m_document = {};
|
||||
m_lastFrame = {};
|
||||
m_presentedFrameCount = 0;
|
||||
m_lastError = loadResult.errorMessage.empty()
|
||||
? "UIScreenPlayer failed to load screen document."
|
||||
: loadResult.errorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_asset = asset;
|
||||
m_document = loadResult.document;
|
||||
m_lastFrame = {};
|
||||
m_lastError.clear();
|
||||
m_presentedFrameCount = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void UIScreenPlayer::Unload() {
|
||||
m_asset = {};
|
||||
m_document = {};
|
||||
m_lastFrame = {};
|
||||
m_lastError.clear();
|
||||
m_presentedFrameCount = 0;
|
||||
}
|
||||
|
||||
bool UIScreenPlayer::IsLoaded() const {
|
||||
return m_document.IsValid();
|
||||
}
|
||||
|
||||
const UIScreenAsset* UIScreenPlayer::GetAsset() const {
|
||||
return m_asset.IsValid() ? &m_asset : nullptr;
|
||||
}
|
||||
|
||||
const UIScreenDocument* UIScreenPlayer::GetDocument() const {
|
||||
return m_document.IsValid() ? &m_document : nullptr;
|
||||
}
|
||||
|
||||
const UIScreenFrameResult& UIScreenPlayer::GetLastFrame() const {
|
||||
return m_lastFrame;
|
||||
}
|
||||
|
||||
const std::string& UIScreenPlayer::GetLastError() const {
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
std::uint64_t UIScreenPlayer::GetPresentedFrameCount() const {
|
||||
return m_presentedFrameCount;
|
||||
}
|
||||
|
||||
const UIScreenFrameResult& UIScreenPlayer::Update(const UIScreenFrameInput& input) {
|
||||
if (!IsLoaded() || m_documentHost == nullptr) {
|
||||
m_lastFrame = MakeNotLoadedFrame(input);
|
||||
m_lastError = m_lastFrame.errorMessage;
|
||||
return m_lastFrame;
|
||||
}
|
||||
|
||||
m_lastFrame = m_documentHost->BuildFrame(m_document, input);
|
||||
m_lastFrame.stats.documentLoaded = true;
|
||||
m_lastFrame.stats.drawListCount = m_lastFrame.drawData.GetDrawListCount();
|
||||
m_lastFrame.stats.commandCount = m_lastFrame.drawData.GetTotalCommandCount();
|
||||
m_lastFrame.stats.inputEventCount = input.events.size();
|
||||
m_lastFrame.stats.presentedFrameIndex = input.frameIndex;
|
||||
m_lastError = m_lastFrame.errorMessage;
|
||||
++m_presentedFrameCount;
|
||||
return m_lastFrame;
|
||||
}
|
||||
|
||||
} // namespace Runtime
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
184
engine/src/UI/Runtime/UISystem.cpp
Normal file
184
engine/src/UI/Runtime/UISystem.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include <XCEngine/UI/Runtime/UISystem.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Runtime {
|
||||
|
||||
UISystem::UISystem(IUIScreenDocumentHost& documentHost)
|
||||
: m_documentHost(&documentHost) {
|
||||
}
|
||||
|
||||
UIScreenPlayer& UISystem::CreatePlayer(const UIScreenLayerOptions& options) {
|
||||
m_players.push_back(std::make_unique<UIScreenPlayer>(*m_documentHost));
|
||||
m_layerIds.push_back(m_nextLayerId++);
|
||||
m_layerOptions.push_back(options);
|
||||
return *m_players.back();
|
||||
}
|
||||
|
||||
UIScreenLayerId UISystem::PushScreen(
|
||||
const UIScreenAsset& asset,
|
||||
const UIScreenLayerOptions& options) {
|
||||
UIScreenPlayer& player = CreatePlayer(options);
|
||||
if (!player.Load(asset)) {
|
||||
m_players.pop_back();
|
||||
m_layerIds.pop_back();
|
||||
m_layerOptions.pop_back();
|
||||
return 0;
|
||||
}
|
||||
return m_layerIds.empty() ? 0 : m_layerIds.back();
|
||||
}
|
||||
|
||||
bool UISystem::RemoveLayer(UIScreenLayerId layerId) {
|
||||
const std::size_t index = FindLayerIndex(layerId);
|
||||
if (index >= m_players.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_players.erase(m_players.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
m_layerIds.erase(m_layerIds.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
m_layerOptions.erase(m_layerOptions.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UISystem::SetLayerVisibility(UIScreenLayerId layerId, bool visible) {
|
||||
const std::size_t index = FindLayerIndex(layerId);
|
||||
if (index >= m_layerOptions.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_layerOptions[index].visible = visible;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UISystem::SetLayerOptions(
|
||||
UIScreenLayerId layerId,
|
||||
const UIScreenLayerOptions& options) {
|
||||
const std::size_t index = FindLayerIndex(layerId);
|
||||
if (index >= m_layerOptions.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_layerOptions[index] = options;
|
||||
return true;
|
||||
}
|
||||
|
||||
const UIScreenLayerOptions* UISystem::FindLayerOptions(UIScreenLayerId layerId) const {
|
||||
const std::size_t index = FindLayerIndex(layerId);
|
||||
return index < m_layerOptions.size()
|
||||
? &m_layerOptions[index]
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
UIScreenLayerId UISystem::GetLayerId(std::size_t index) const {
|
||||
return index < m_layerIds.size() ? m_layerIds[index] : 0;
|
||||
}
|
||||
|
||||
void UISystem::DestroyAllPlayers() {
|
||||
m_players.clear();
|
||||
m_layerIds.clear();
|
||||
m_layerOptions.clear();
|
||||
m_lastFrame = {};
|
||||
}
|
||||
|
||||
std::size_t UISystem::GetPlayerCount() const {
|
||||
return m_players.size();
|
||||
}
|
||||
|
||||
std::size_t UISystem::GetLayerCount() const {
|
||||
return m_layerIds.size();
|
||||
}
|
||||
|
||||
const UISystemFrameResult& UISystem::Update(const UIScreenFrameInput& input) {
|
||||
m_lastFrame = {};
|
||||
m_lastFrame.frameIndex = input.frameIndex;
|
||||
|
||||
std::vector<std::size_t> presentedIndices;
|
||||
presentedIndices.reserve(m_players.size());
|
||||
for (std::size_t index = m_players.size(); index > 0; --index) {
|
||||
const std::size_t layerIndex = index - 1;
|
||||
if (!m_layerOptions[layerIndex].visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
presentedIndices.push_back(layerIndex);
|
||||
if (m_layerOptions[layerIndex].blocksLayersBelow) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::reverse(presentedIndices.begin(), presentedIndices.end());
|
||||
|
||||
std::size_t interactiveLayerIndex = m_players.size();
|
||||
for (std::size_t index = presentedIndices.size(); index > 0; --index) {
|
||||
const std::size_t layerIndex = presentedIndices[index - 1];
|
||||
if (m_layerOptions[layerIndex].acceptsInput) {
|
||||
interactiveLayerIndex = layerIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::size_t layerIndex : presentedIndices) {
|
||||
UIScreenFrameInput layerInput = input;
|
||||
if (layerIndex != interactiveLayerIndex) {
|
||||
layerInput.events.clear();
|
||||
}
|
||||
|
||||
const UIScreenFrameResult& frame = m_players[layerIndex]->Update(layerInput);
|
||||
for (const UIDrawList& drawList : frame.drawData.GetDrawLists()) {
|
||||
m_lastFrame.drawData.AddDrawList(drawList);
|
||||
}
|
||||
|
||||
UISystemPresentedLayer presentedLayer = {};
|
||||
presentedLayer.layerId = m_layerIds[layerIndex];
|
||||
if (const UIScreenAsset* asset = m_players[layerIndex]->GetAsset();
|
||||
asset != nullptr) {
|
||||
presentedLayer.asset = *asset;
|
||||
}
|
||||
presentedLayer.options = m_layerOptions[layerIndex];
|
||||
presentedLayer.stats = frame.stats;
|
||||
m_lastFrame.layers.push_back(std::move(presentedLayer));
|
||||
|
||||
if (m_lastFrame.errorMessage.empty() && !frame.errorMessage.empty()) {
|
||||
m_lastFrame.errorMessage = frame.errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
m_lastFrame.presentedLayerCount = m_lastFrame.layers.size();
|
||||
m_lastFrame.skippedLayerCount =
|
||||
m_players.size() > m_lastFrame.presentedLayerCount
|
||||
? m_players.size() - m_lastFrame.presentedLayerCount
|
||||
: 0;
|
||||
return m_lastFrame;
|
||||
}
|
||||
|
||||
void UISystem::Tick(const UIScreenFrameInput& input) {
|
||||
for (const std::unique_ptr<UIScreenPlayer>& player : m_players) {
|
||||
if (player) {
|
||||
player->Update(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const UISystemFrameResult& UISystem::GetLastFrame() const {
|
||||
return m_lastFrame;
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<UIScreenPlayer>>& UISystem::GetPlayers() const {
|
||||
return m_players;
|
||||
}
|
||||
|
||||
std::size_t UISystem::FindLayerIndex(UIScreenLayerId layerId) const {
|
||||
for (std::size_t index = 0; index < m_layerIds.size(); ++index) {
|
||||
if (m_layerIds[index] == layerId) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return m_layerIds.size();
|
||||
}
|
||||
|
||||
} // namespace Runtime
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
Reference in New Issue
Block a user