Archive XCUI subplan 04

This commit is contained in:
2026-04-04 19:04:28 +08:00
parent 611ca705c8
commit c2cb2e5914
11 changed files with 866 additions and 21 deletions

View File

@@ -0,0 +1,116 @@
#include <XCEngine/UI/Core/UIBuildContext.h>
namespace XCEngine {
namespace UI {
void UIBuildList::Reset() {
m_elements.clear();
}
void UIBuildList::Reserve(std::size_t elementCount) {
m_elements.reserve(elementCount);
}
void UIBuildList::AddElement(const UIBuildElement& element) {
m_elements.push_back(element);
}
void UIBuildContext::ElementScope::Close() {
if (m_context == nullptr || !m_valid) {
return;
}
m_context->EndElement();
m_valid = false;
m_context = nullptr;
}
UIBuildContext::UIBuildContext(UIBuildList& buildList)
: m_buildList(&buildList) {
}
bool UIBuildContext::BeginElement(const UIBuildElementDesc& desc) {
return AppendElement(desc, true);
}
bool UIBuildContext::AddLeaf(const UIBuildElementDesc& desc) {
return AppendElement(desc, false);
}
bool UIBuildContext::EndElement() {
if (!IsValid()) {
return false;
}
if (m_stack.empty()) {
SetError("UIBuildContext::EndElement called with an empty stack.");
return false;
}
m_stack.pop_back();
return true;
}
UIBuildContext::ElementScope UIBuildContext::PushElement(const UIBuildElementDesc& desc) {
return ElementScope(this, BeginElement(desc));
}
bool UIBuildContext::AppendElement(const UIBuildElementDesc& desc, bool keepOpen) {
if (!IsValid()) {
return false;
}
if (m_buildList == nullptr) {
SetError("UIBuildContext does not have an output build list.");
return false;
}
if (desc.id == kInvalidUIElementId) {
SetError("UIBuildContext requires a non-zero UIElementId.");
return false;
}
if (desc.typeName.empty()) {
SetError("UIBuildContext requires a non-empty element type name.");
return false;
}
if (m_seenIds.find(desc.id) != m_seenIds.end()) {
SetError("UIBuildContext received a duplicate UIElementId.");
return false;
}
const UIElementId parentId = m_stack.empty()
? kInvalidUIElementId
: m_stack.back();
if (parentId == kInvalidUIElementId && !m_buildList->Empty()) {
SetError("UIBuildContext only supports a single root element.");
return false;
}
UIBuildElement element = {};
element.id = desc.id;
element.parentId = parentId;
element.typeName = desc.typeName;
element.structuralRevision = desc.structuralRevision;
element.localStateRevision = desc.localStateRevision;
element.viewModelRevision = desc.viewModel != nullptr
? desc.viewModel->GetViewModelRevision()
: 0;
m_buildList->AddElement(element);
m_seenIds.insert(desc.id);
if (keepOpen) {
m_stack.push_back(desc.id);
}
return true;
}
void UIBuildContext::SetError(const std::string& errorMessage) {
if (m_lastError.empty()) {
m_lastError = errorMessage;
}
}
} // namespace UI
} // namespace XCEngine

View File

@@ -0,0 +1,342 @@
#include <XCEngine/UI/Core/UIContext.h>
#include <algorithm>
namespace XCEngine {
namespace UI {
namespace {
void MergeChange(
std::vector<UIElementChange>& changes,
UIElementId id,
UIElementChangeKind kind,
UIDirtyFlags dirtyFlags) {
for (UIElementChange& change : changes) {
if (change.id != id) {
continue;
}
if (change.kind == UIElementChangeKind::Created || kind == UIElementChangeKind::Created) {
change.kind = UIElementChangeKind::Created;
} else if (change.kind == UIElementChangeKind::Removed || kind == UIElementChangeKind::Removed) {
change.kind = UIElementChangeKind::Removed;
} else {
change.kind = UIElementChangeKind::Updated;
}
change.dirtyFlags |= dirtyFlags;
return;
}
UIElementChange change = {};
change.id = id;
change.kind = kind;
change.dirtyFlags = dirtyFlags;
changes.push_back(change);
}
bool IsStructureAffectingChange(UIDirtyFlags flags) {
return HasAnyDirtyFlags(flags, UIDirtyFlags::Structure | UIDirtyFlags::Layout);
}
} // namespace
const UIElementChange* UIElementTreeRebuildResult::FindChange(UIElementId id) const {
for (const UIElementChange& change : changes) {
if (change.id == id) {
return &change;
}
}
return nullptr;
}
bool UIElementTreeRebuildResult::HasChange(UIElementId id) const {
return FindChange(id) != nullptr;
}
UIElementTreeRebuildResult UIElementTree::Rebuild(const UIBuildList& buildList) {
UIElementTreeRebuildResult result = {};
result.generation = m_generation + 1;
if (buildList.Empty()) {
if (!m_nodes.empty()) {
result.treeChanged = true;
for (const auto& entry : m_nodes) {
MergeChange(
result.changes,
entry.first,
UIElementChangeKind::Removed,
UIDirtyFlags::Structure | UIDirtyFlags::Layout | UIDirtyFlags::Paint);
}
}
m_nodes.clear();
m_rootId = kInvalidUIElementId;
m_generation = result.generation;
return result;
}
std::unordered_map<UIElementId, UIBuildElement> newElementsById;
newElementsById.reserve(buildList.GetCount());
for (const UIBuildElement& element : buildList.GetElements()) {
if (element.id == kInvalidUIElementId) {
result.succeeded = false;
result.errorMessage = "UIElementTree::Rebuild received an invalid element id.";
return result;
}
const auto insertResult = newElementsById.emplace(element.id, element);
if (!insertResult.second) {
result.succeeded = false;
result.errorMessage = "UIElementTree::Rebuild received duplicate element ids.";
return result;
}
}
const UIElementId newRootId = buildList.GetElements().front().id;
if (buildList.GetElements().front().parentId != kInvalidUIElementId) {
result.succeeded = false;
result.errorMessage = "UIElementTree::Rebuild requires the first element to be the root.";
return result;
}
std::unordered_map<UIElementId, UIElementNode> newNodes;
newNodes.reserve(buildList.GetCount());
for (const UIBuildElement& element : buildList.GetElements()) {
if (element.parentId != kInvalidUIElementId &&
newElementsById.find(element.parentId) == newElementsById.end()) {
result.succeeded = false;
result.errorMessage = "UIElementTree::Rebuild found an element whose parent does not exist.";
return result;
}
UIElementNode node = {};
node.id = element.id;
node.parentId = element.parentId;
node.typeName = element.typeName;
node.structuralRevision = element.structuralRevision;
node.localStateRevision = element.localStateRevision;
node.viewModelRevision = element.viewModelRevision;
node.lastBuildGeneration = result.generation;
const auto existingNode = m_nodes.find(element.id);
if (existingNode != m_nodes.end()) {
node.dirtyFlags = existingNode->second.dirtyFlags;
}
if (node.parentId != kInvalidUIElementId) {
const auto parentNode = newNodes.find(node.parentId);
if (parentNode == newNodes.end()) {
result.succeeded = false;
result.errorMessage = "UIElementTree::Rebuild requires parents to appear before children.";
return result;
}
node.depth = parentNode->second.depth + 1u;
parentNode->second.childIds.push_back(node.id);
}
newNodes.emplace(node.id, std::move(node));
}
for (const auto& oldEntry : m_nodes) {
if (newNodes.find(oldEntry.first) != newNodes.end()) {
continue;
}
result.treeChanged = true;
MergeChange(
result.changes,
oldEntry.first,
UIElementChangeKind::Removed,
UIDirtyFlags::Structure | UIDirtyFlags::Layout | UIDirtyFlags::Paint);
const UIElementId parentId = oldEntry.second.parentId;
if (parentId != kInvalidUIElementId && newNodes.find(parentId) != newNodes.end()) {
MarkDirtyInternal(newNodes, parentId, UIDirtyFlags::Structure | UIDirtyFlags::Layout | UIDirtyFlags::Paint);
}
}
for (auto& newEntry : newNodes) {
const UIElementId id = newEntry.first;
UIElementNode& newNode = newEntry.second;
const auto oldEntry = m_nodes.find(id);
if (oldEntry == m_nodes.end()) {
result.treeChanged = true;
MarkDirtyInternal(newNodes, id, UIDirtyFlags::Structure | UIDirtyFlags::Layout | UIDirtyFlags::Paint);
MergeChange(
result.changes,
id,
UIElementChangeKind::Created,
UIDirtyFlags::Structure | UIDirtyFlags::Layout | UIDirtyFlags::Paint);
continue;
}
UIDirtyFlags dirtyFlags = UIDirtyFlags::None;
const UIElementNode& oldNode = oldEntry->second;
if (oldNode.parentId != newNode.parentId ||
oldNode.typeName != newNode.typeName ||
oldNode.structuralRevision != newNode.structuralRevision) {
dirtyFlags |= UIDirtyFlags::Structure | UIDirtyFlags::Layout | UIDirtyFlags::Paint;
}
if (oldNode.localStateRevision != newNode.localStateRevision) {
dirtyFlags |= UIDirtyFlags::LocalState | UIDirtyFlags::Paint;
}
if (oldNode.viewModelRevision != newNode.viewModelRevision) {
dirtyFlags |= UIDirtyFlags::ViewModel | UIDirtyFlags::Paint;
}
if (oldNode.childIds != newNode.childIds) {
dirtyFlags |= UIDirtyFlags::Structure | UIDirtyFlags::Layout | UIDirtyFlags::Paint;
}
if (!IsDirty(dirtyFlags)) {
continue;
}
result.treeChanged = true;
MarkDirtyInternal(newNodes, id, dirtyFlags);
MergeChange(result.changes, id, UIElementChangeKind::Updated, dirtyFlags);
}
m_nodes = std::move(newNodes);
m_rootId = newRootId;
m_generation = result.generation;
result.dirtyRootIds = CollectDirtyRootIds();
return result;
}
bool UIElementTree::HasNode(UIElementId id) const {
return m_nodes.find(id) != m_nodes.end();
}
const UIElementNode* UIElementTree::FindNode(UIElementId id) const {
const auto entry = m_nodes.find(id);
return entry != m_nodes.end()
? &entry->second
: nullptr;
}
void UIElementTree::MarkDirty(UIElementId id, UIDirtyFlags flags) {
MarkDirtyInternal(m_nodes, id, flags);
}
std::vector<UIElementId> UIElementTree::CollectDirtyRootIds() const {
std::vector<UIElementId> dirtyRoots;
dirtyRoots.reserve(m_nodes.size());
for (const auto& entry : m_nodes) {
const UIElementNode& node = entry.second;
if (!node.IsDirty()) {
continue;
}
const auto parentEntry = m_nodes.find(node.parentId);
if (parentEntry != m_nodes.end() && parentEntry->second.IsDirty()) {
continue;
}
dirtyRoots.push_back(node.id);
}
std::sort(
dirtyRoots.begin(),
dirtyRoots.end(),
[this](UIElementId left, UIElementId right) {
const UIElementNode* leftNode = FindNode(left);
const UIElementNode* rightNode = FindNode(right);
const std::uint32_t leftDepth = leftNode != nullptr ? leftNode->depth : 0u;
const std::uint32_t rightDepth = rightNode != nullptr ? rightNode->depth : 0u;
return leftDepth != rightDepth
? leftDepth < rightDepth
: left < right;
});
return dirtyRoots;
}
void UIElementTree::ClearDirtyFlags(UIElementId id, bool includeSubtree) {
if (!includeSubtree) {
const auto entry = m_nodes.find(id);
if (entry != m_nodes.end()) {
entry->second.dirtyFlags = UIDirtyFlags::None;
}
return;
}
ClearDirtyFlagsRecursive(id);
}
void UIElementTree::ClearAllDirtyFlags() {
for (auto& entry : m_nodes) {
entry.second.dirtyFlags = UIDirtyFlags::None;
}
}
void UIElementTree::MarkDirtyInternal(
std::unordered_map<UIElementId, UIElementNode>& nodes,
UIElementId id,
UIDirtyFlags flags) {
const auto entry = nodes.find(id);
if (entry == nodes.end()) {
return;
}
entry->second.dirtyFlags |= flags;
if (!IsStructureAffectingChange(flags)) {
return;
}
UIElementId parentId = entry->second.parentId;
while (parentId != kInvalidUIElementId) {
const auto parentEntry = nodes.find(parentId);
if (parentEntry == nodes.end()) {
break;
}
parentEntry->second.dirtyFlags |= UIDirtyFlags::Layout;
parentId = parentEntry->second.parentId;
}
}
void UIElementTree::ClearDirtyFlagsRecursive(UIElementId id) {
const auto entry = m_nodes.find(id);
if (entry == m_nodes.end()) {
return;
}
entry->second.dirtyFlags = UIDirtyFlags::None;
const std::vector<UIElementId> childIds = entry->second.childIds;
for (UIElementId childId : childIds) {
ClearDirtyFlagsRecursive(childId);
}
}
UIElementTreeRebuildResult UIContext::Rebuild(const BuildCallback& buildCallback) {
m_buildList.Reset();
UIBuildContext buildContext(m_buildList);
if (buildCallback) {
buildCallback(buildContext);
}
UIElementTreeRebuildResult result = {};
result.generation = m_tree.GetGeneration() + 1;
if (!buildContext.IsValid()) {
result.succeeded = false;
result.errorMessage = buildContext.GetLastError();
return result;
}
if (buildContext.HasOpenElements()) {
result.succeeded = false;
result.errorMessage = "UIContext::Rebuild finished with unclosed UI elements.";
return result;
}
return m_tree.Rebuild(m_buildList);
}
} // namespace UI
} // namespace XCEngine