Add XCUI runtime screen layer and demo textarea

This commit is contained in:
2026-04-05 05:14:16 +08:00
parent 67a28bdd4a
commit ade5be31d6
18 changed files with 2042 additions and 37 deletions

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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