Add XCUI expansion state and coverage tests

This commit is contained in:
2026-04-05 07:29:27 +08:00
parent 646e5855ce
commit 511e94fd30
18 changed files with 1213 additions and 53 deletions

View File

@@ -9,6 +9,7 @@
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Types.h>
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <algorithm>
@@ -78,6 +79,7 @@ struct RuntimeBuildContext {
std::vector<LayoutNode> nodes = {};
std::unordered_map<std::string, std::size_t> nodeIndexById = {};
std::unordered_map<std::string, UIRect> rectsById = {};
UIWidgets::UIExpansionModel expansionModel = {};
UIWidgets::UISelectionModel selectionModel = {};
bool documentsReady = false;
std::string statusMessage = {};
@@ -491,16 +493,20 @@ float ResolveListItemHeight(const Style::UITheme& theme) {
theme);
}
float ResolveTreeIndent(const LayoutNode& node, const Style::UITheme& theme) {
float ResolveTreeIndentLevel(const LayoutNode& node) {
float indentLevel = 0.0f;
if (!node.indentAttr.empty()) {
TryParseFloat(node.indentAttr, indentLevel);
}
return (std::max)(0.0f, indentLevel);
}
float ResolveTreeIndent(const LayoutNode& node, const Style::UITheme& theme) {
return UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(
UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem,
theme,
(std::max)(0.0f, indentLevel));
ResolveTreeIndentLevel(node));
}
float ResolveFieldRowHeight(const Style::UITheme& theme) {
@@ -519,23 +525,195 @@ float ResolveDefaultHeight(const LayoutNode& node, const Style::UITheme& theme)
theme);
}
float ResolvePropertySectionHeaderHeight(
const RuntimeBuildContext& state,
const LayoutNode& node) {
const float titleFont = ResolveFloatToken(state.theme, "font.title", 16.0f);
const float bodyFont = ResolveFloatToken(state.theme, "font.body", 13.0f);
return titleFont + 8.0f + (node.subtitle.empty() ? 0.0f : bodyFont + 8.0f);
}
bool HasTreeItemChildren(const RuntimeBuildContext& state, std::size_t nodeIndex) {
const LayoutNode& node = state.nodes[nodeIndex];
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName) !=
UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem ||
node.parentIndex == kInvalidIndex) {
return false;
}
const LayoutNode& parent = state.nodes[node.parentIndex];
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(parent.tagName) !=
UIWidgets::UIEditorCollectionPrimitiveKind::TreeView) {
return false;
}
const float indentLevel = ResolveTreeIndentLevel(node);
const auto siblingIt = std::find(parent.children.begin(), parent.children.end(), nodeIndex);
if (siblingIt == parent.children.end()) {
return false;
}
for (auto it = siblingIt + 1; it != parent.children.end(); ++it) {
const LayoutNode& sibling = state.nodes[*it];
const float siblingIndentLevel = ResolveTreeIndentLevel(sibling);
if (siblingIndentLevel <= indentLevel) {
break;
}
return true;
}
return false;
}
bool IsNodeCollapsible(const RuntimeBuildContext& state, std::size_t nodeIndex) {
const LayoutNode& node = state.nodes[nodeIndex];
const UIWidgets::UIEditorCollectionPrimitiveKind primitiveKind =
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName);
if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
return !node.children.empty();
}
return HasTreeItemChildren(state, nodeIndex);
}
bool IsNodeExpanded(const RuntimeBuildContext& state, std::size_t nodeIndex) {
return !IsNodeCollapsible(state, nodeIndex) ||
state.expansionModel.IsExpanded(state.nodes[nodeIndex].id);
}
float ResolvePropertySectionCollapsedHeight(
const RuntimeBuildContext& state,
const LayoutNode& node) {
const float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f);
return inset * 2.0f + ResolvePropertySectionHeaderHeight(state, node);
}
bool IsNodeVisible(const RuntimeBuildContext& state, std::size_t nodeIndex) {
const LayoutNode& node = state.nodes[nodeIndex];
const UIWidgets::UIEditorCollectionPrimitiveKind primitiveKind =
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName);
std::size_t ancestorIndex = node.parentIndex;
while (ancestorIndex != kInvalidIndex) {
const LayoutNode& ancestor = state.nodes[ancestorIndex];
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(ancestor.tagName) ==
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection &&
!IsNodeExpanded(state, ancestorIndex)) {
return false;
}
ancestorIndex = ancestor.parentIndex;
}
if (primitiveKind != UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem ||
node.parentIndex == kInvalidIndex) {
return true;
}
const LayoutNode& parent = state.nodes[node.parentIndex];
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(parent.tagName) !=
UIWidgets::UIEditorCollectionPrimitiveKind::TreeView) {
return true;
}
float requiredAncestorIndent = ResolveTreeIndentLevel(node);
if (requiredAncestorIndent <= 0.0f) {
return true;
}
const auto siblingIt = std::find(parent.children.begin(), parent.children.end(), nodeIndex);
if (siblingIt == parent.children.end()) {
return true;
}
const std::size_t siblingOffset =
static_cast<std::size_t>(siblingIt - parent.children.begin());
for (std::size_t offset = siblingOffset; offset > 0u && requiredAncestorIndent > 0.0f; --offset) {
const std::size_t siblingIndex = parent.children[offset - 1u];
const LayoutNode& sibling = state.nodes[siblingIndex];
const float siblingIndent = ResolveTreeIndentLevel(sibling);
if (siblingIndent < requiredAncestorIndent) {
if (!IsNodeExpanded(state, siblingIndex)) {
return false;
}
requiredAncestorIndent = siblingIndent;
}
}
return true;
}
std::vector<std::size_t> CollectVisibleChildren(
const RuntimeBuildContext& state,
std::size_t nodeIndex) {
std::vector<std::size_t> visibleChildren = {};
const LayoutNode& node = state.nodes[nodeIndex];
visibleChildren.reserve(node.children.size());
for (const std::size_t childIndex : node.children) {
if (IsNodeVisible(state, childIndex)) {
visibleChildren.push_back(childIndex);
}
}
return visibleChildren;
}
void SeedDefaultExpansionState(RuntimeBuildContext& state) {
state.expansionModel.Clear();
for (std::size_t nodeIndex = 0; nodeIndex < state.nodes.size(); ++nodeIndex) {
const LayoutNode& node = state.nodes[nodeIndex];
const UIWidgets::UIEditorCollectionPrimitiveKind primitiveKind =
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName);
if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection ||
HasTreeItemChildren(state, nodeIndex)) {
state.expansionModel.Expand(node.id);
}
}
}
void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex);
void LayoutColumnChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
LayoutNode& node = state.nodes[nodeIndex];
const float padding = ResolvePadding(node, state.theme);
const float gap = ResolveGap(node, state.theme);
const UIRect contentRect = InsetRect(node.rect, padding);
if (node.children.empty()) {
UIRect contentRect = InsetRect(node.rect, padding);
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName) ==
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
const float headerHeight = ResolvePropertySectionHeaderHeight(state, node);
contentRect.y += headerHeight;
contentRect.height = (std::max)(0.0f, contentRect.height - headerHeight);
}
const std::vector<std::size_t> visibleChildren = CollectVisibleChildren(state, nodeIndex);
if (visibleChildren.empty()) {
return;
}
float fixedHeight = 0.0f;
std::size_t stretchCount = 0;
std::vector<float> resolvedHeights(node.children.size(), 0.0f);
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
const LayoutNode& child = state.nodes[node.children[childOffset]];
std::vector<float> resolvedHeights(visibleChildren.size(), 0.0f);
std::vector<bool> childUsesStretchHeight(visibleChildren.size(), false);
for (std::size_t childOffset = 0; childOffset < visibleChildren.size(); ++childOffset) {
const LayoutNode& child = state.nodes[visibleChildren[childOffset]];
const float defaultHeight = ResolveDefaultHeight(child, state.theme);
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(child.tagName) ==
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection &&
!IsNodeExpanded(state, visibleChildren[childOffset])) {
resolvedHeights[childOffset] = ResolvePropertySectionCollapsedHeight(state, child);
fixedHeight += resolvedHeights[childOffset];
continue;
}
if (child.heightAttr.empty() && defaultHeight > 0.0f) {
resolvedHeights[childOffset] = defaultHeight;
fixedHeight += resolvedHeights[childOffset];
continue;
}
if (IsStretch(child.heightAttr)) {
childUsesStretchHeight[childOffset] = true;
++stretchCount;
continue;
}
@@ -543,27 +721,28 @@ void LayoutColumnChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
resolvedHeights[childOffset] = ResolveScalar(
child.heightAttr,
contentRect.height,
ResolveDefaultHeight(child, state.theme));
defaultHeight);
fixedHeight += resolvedHeights[childOffset];
}
const float totalGap = gap * static_cast<float>((std::max<std::size_t>)(1u, node.children.size()) - 1u);
const float totalGap =
gap * static_cast<float>((std::max<std::size_t>)(1u, visibleChildren.size()) - 1u);
const float remainingHeight = (std::max)(0.0f, contentRect.height - fixedHeight - totalGap);
const float stretchHeight =
stretchCount > 0u ? remainingHeight / static_cast<float>(stretchCount) : 0.0f;
float cursorY = contentRect.y;
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
LayoutNode& child = state.nodes[node.children[childOffset]];
for (std::size_t childOffset = 0; childOffset < visibleChildren.size(); ++childOffset) {
LayoutNode& child = state.nodes[visibleChildren[childOffset]];
const float childHeight =
!IsStretch(child.heightAttr) ? resolvedHeights[childOffset] : stretchHeight;
childUsesStretchHeight[childOffset] ? stretchHeight : resolvedHeights[childOffset];
const float childWidth =
!IsStretch(child.widthAttr)
? ResolveScalar(child.widthAttr, contentRect.width, contentRect.width)
: contentRect.width;
child.rect = UIRect(contentRect.x, cursorY, childWidth, childHeight);
cursorY += childHeight + gap;
LayoutNodeTree(state, node.children[childOffset]);
LayoutNodeTree(state, visibleChildren[childOffset]);
}
}
@@ -572,15 +751,16 @@ void LayoutRowChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
const float padding = ResolvePadding(node, state.theme);
const float gap = ResolveGap(node, state.theme);
const UIRect contentRect = InsetRect(node.rect, padding);
if (node.children.empty()) {
const std::vector<std::size_t> visibleChildren = CollectVisibleChildren(state, nodeIndex);
if (visibleChildren.empty()) {
return;
}
float fixedWidth = 0.0f;
std::size_t stretchCount = 0;
std::vector<float> resolvedWidths(node.children.size(), 0.0f);
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
const LayoutNode& child = state.nodes[node.children[childOffset]];
std::vector<float> resolvedWidths(visibleChildren.size(), 0.0f);
for (std::size_t childOffset = 0; childOffset < visibleChildren.size(); ++childOffset) {
const LayoutNode& child = state.nodes[visibleChildren[childOffset]];
if (IsStretch(child.widthAttr)) {
++stretchCount;
continue;
@@ -590,14 +770,15 @@ void LayoutRowChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
fixedWidth += resolvedWidths[childOffset];
}
const float totalGap = gap * static_cast<float>((std::max<std::size_t>)(1u, node.children.size()) - 1u);
const float totalGap =
gap * static_cast<float>((std::max<std::size_t>)(1u, visibleChildren.size()) - 1u);
const float remainingWidth = (std::max)(0.0f, contentRect.width - fixedWidth - totalGap);
const float stretchWidth =
stretchCount > 0u ? remainingWidth / static_cast<float>(stretchCount) : 0.0f;
float cursorX = contentRect.x;
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
LayoutNode& child = state.nodes[node.children[childOffset]];
for (std::size_t childOffset = 0; childOffset < visibleChildren.size(); ++childOffset) {
LayoutNode& child = state.nodes[visibleChildren[childOffset]];
const float childWidth =
!IsStretch(child.widthAttr) ? resolvedWidths[childOffset] : stretchWidth;
const float childHeight =
@@ -606,7 +787,7 @@ void LayoutRowChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
: contentRect.height;
child.rect = UIRect(cursorX, contentRect.y, childWidth, childHeight);
cursorX += childWidth + gap;
LayoutNodeTree(state, node.children[childOffset]);
LayoutNodeTree(state, visibleChildren[childOffset]);
}
}
@@ -614,7 +795,7 @@ void LayoutOverlayChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
LayoutNode& node = state.nodes[nodeIndex];
const UIRect contentRect = GetContentRect(node, state.theme);
for (std::size_t childIndex : node.children) {
for (const std::size_t childIndex : CollectVisibleChildren(state, nodeIndex)) {
LayoutNode& child = state.nodes[childIndex];
const float offsetX = ResolveScalar(child.xAttr, contentRect.width, 0.0f);
const float offsetY = ResolveScalar(child.yAttr, contentRect.height, 0.0f);
@@ -644,17 +825,22 @@ void LayoutScrollViewChildren(RuntimeBuildContext& state, std::size_t nodeIndex)
const UIRect contentRect = GetContentRect(node, state.theme);
float cursorY = contentRect.y - scrollOffset;
for (std::size_t childIndex : node.children) {
for (const std::size_t childIndex : CollectVisibleChildren(state, nodeIndex)) {
LayoutNode& child = state.nodes[childIndex];
const float defaultHeight = ResolveDefaultHeight(child, state.theme);
const float childHeight = child.heightAttr.empty()
? (defaultHeight > 0.0f ? defaultHeight : defaultItemHeight)
: (!IsStretch(child.heightAttr)
? ResolveScalar(
child.heightAttr,
contentRect.height,
defaultHeight > 0.0f ? defaultHeight : defaultItemHeight)
: defaultItemHeight);
const float childHeight =
UIWidgets::ClassifyUIEditorCollectionPrimitive(child.tagName) ==
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection &&
!IsNodeExpanded(state, childIndex)
? ResolvePropertySectionCollapsedHeight(state, child)
: (child.heightAttr.empty()
? (defaultHeight > 0.0f ? defaultHeight : defaultItemHeight)
: (!IsStretch(child.heightAttr)
? ResolveScalar(
child.heightAttr,
contentRect.height,
defaultHeight > 0.0f ? defaultHeight : defaultItemHeight)
: defaultItemHeight));
const float childWidth =
!IsStretch(child.widthAttr)
? (std::min)(
@@ -686,6 +872,23 @@ void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex) {
}
}
void DrawDisclosureGlyph(
UIDrawList& drawList,
const UIRect& rect,
const UIColor& color,
bool expanded) {
drawList.AddFilledRect(
UIRect(rect.x, rect.y + rect.height * 0.5f - 1.0f, rect.width, 2.0f),
color,
1.0f);
if (!expanded) {
drawList.AddFilledRect(
UIRect(rect.x + rect.width * 0.5f - 1.0f, rect.y, 2.0f, rect.height),
color,
1.0f);
}
}
void DrawNode(
RuntimeBuildContext& state,
std::size_t nodeIndex,
@@ -716,14 +919,15 @@ void DrawNode(
drawList.AddFilledRect(node.rect, ToUIColor(surfaceColor), rounding);
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
const bool selected = state.selectionModel.IsSelected(node.id);
const bool expanded = IsNodeExpanded(state, nodeIndex);
const Color sectionColor = ResolveColorToken(
state.theme,
"color.card",
Color(0.12f, 0.17f, 0.23f, 1.0f));
const Color borderColor = ResolveColorToken(
state.theme,
"color.border",
Color(0.24f, 0.34f, 0.43f, 1.0f));
const Color borderColor = selected
? ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))
: ResolveColorToken(state.theme, "color.border", Color(0.24f, 0.34f, 0.43f, 1.0f));
const Color textColor = ResolveColorToken(
state.theme,
"color.text",
@@ -753,6 +957,17 @@ void DrawNode(
ToUIColor(mutedColor),
bodyFont);
}
if (IsNodeCollapsible(state, nodeIndex)) {
DrawDisclosureGlyph(
drawList,
UIRect(
node.rect.x + node.rect.width - inset - 10.0f,
node.rect.y + inset + 3.0f,
10.0f,
10.0f),
ToUIColor(textColor),
expanded);
}
} else if (node.tagName == "Card") {
const bool selected = state.selectionModel.IsSelected(node.id);
const Color cardColor = ResolveColorToken(
@@ -813,6 +1028,8 @@ void DrawNode(
primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::ListItem) {
const bool hovered = node.id == hoveredId;
const bool selected = state.selectionModel.IsSelected(node.id);
const bool hasTreeChildren = primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem &&
HasTreeItemChildren(state, nodeIndex);
const Color rowColor = selected
? ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))
: (hovered
@@ -840,10 +1057,18 @@ void DrawNode(
drawList.AddFilledRect(node.rect, ToUIColor(rowColor), rounding);
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem) {
drawList.AddFilledRect(
UIRect(node.rect.x + inset + indent - 8.0f, node.rect.y + 11.0f, 4.0f, 4.0f),
ToUIColor(textColor),
2.0f);
if (hasTreeChildren) {
DrawDisclosureGlyph(
drawList,
UIRect(node.rect.x + inset + indent - 10.0f, node.rect.y + 10.0f, 8.0f, 8.0f),
ToUIColor(textColor),
IsNodeExpanded(state, nodeIndex));
} else {
drawList.AddFilledRect(
UIRect(node.rect.x + inset + indent - 8.0f, node.rect.y + 11.0f, 4.0f, 4.0f),
ToUIColor(textColor),
2.0f);
}
}
drawList.AddText(
UIPoint(node.rect.x + inset + indent, node.rect.y + 8.0f),
@@ -891,7 +1116,10 @@ void DrawNode(
if (clipsChildren) {
drawList.PushClipRect(GetContentRect(node, state.theme));
}
for (std::size_t childIndex : node.children) {
for (const std::size_t childIndex : node.children) {
if (!IsNodeVisible(state, childIndex)) {
continue;
}
DrawNode(state, childIndex, drawList, hoveredId);
}
if (clipsChildren) {
@@ -928,6 +1156,7 @@ std::size_t HitTest(
UIWidgets::IsUIEditorCollectionPrimitiveHoverable(
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName));
if (!hoverable ||
!IsNodeVisible(state, index) ||
!ContainsPoint(node.rect, point) ||
!IsPointInsideNodeClipping(state, index, point)) {
continue;
@@ -967,6 +1196,7 @@ bool XCUILayoutLabRuntime::ReloadDocuments() {
state.nodes.clear();
state.nodeIndexById.clear();
state.rectsById.clear();
state.expansionModel.Clear();
state.selectionModel.ClearSelection();
state.documentSource.SetPathSet(XCUIAssetDocumentSource::MakeLayoutLabPathSet());
@@ -1001,6 +1231,7 @@ bool XCUILayoutLabRuntime::ReloadDocuments() {
std::string(),
0u,
0);
SeedDefaultExpansionState(state);
state.documentsReady = !state.nodes.empty();
state.statusMessage = state.documentsReady
? (loadState.usedLegacyFallback
@@ -1050,6 +1281,9 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
if (input.pointerPressed) {
if (hoveredIndex != kInvalidIndex) {
if (IsNodeCollapsible(state, hoveredIndex)) {
state.expansionModel.ToggleExpanded(state.nodes[hoveredIndex].id);
}
state.selectionModel.SetSelection(state.nodes[hoveredIndex].id);
} else {
state.selectionModel.ClearSelection();
@@ -1069,7 +1303,12 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
state.frameResult.stats.drawListCount = state.frameResult.drawData.GetDrawListCount();
state.frameResult.stats.commandCount = state.frameResult.drawData.GetTotalCommandCount();
AnalyzeNativeOverlayCompatibility(state.frameResult.drawData, state.frameResult.stats);
for (const LayoutNode& node : state.nodes) {
for (std::size_t nodeIndex = 0; nodeIndex < state.nodes.size(); ++nodeIndex) {
const LayoutNode& node = state.nodes[nodeIndex];
if (!IsNodeVisible(state, nodeIndex)) {
continue;
}
const UIWidgets::UIEditorCollectionPrimitiveKind primitiveKind =
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName);
if (node.tagName == "Row") {
@@ -1084,12 +1323,18 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
++state.frameResult.stats.treeViewCount;
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem) {
++state.frameResult.stats.treeItemCount;
if (HasTreeItemChildren(state, nodeIndex) && IsNodeExpanded(state, nodeIndex)) {
++state.frameResult.stats.expandedTreeItemCount;
}
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::ListView) {
++state.frameResult.stats.listViewCount;
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::ListItem) {
++state.frameResult.stats.listItemCount;
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
++state.frameResult.stats.propertySectionCount;
if (IsNodeExpanded(state, nodeIndex)) {
++state.frameResult.stats.expandedPropertySectionCount;
}
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow) {
++state.frameResult.stats.fieldRowCount;
}

View File

@@ -47,6 +47,8 @@ struct XCUILayoutLabFrameStats {
std::size_t listItemCount = 0;
std::size_t propertySectionCount = 0;
std::size_t fieldRowCount = 0;
std::size_t expandedTreeItemCount = 0;
std::size_t expandedPropertySectionCount = 0;
std::string hoveredElementId = {};
std::string selectedElementId = {};
};

View File

@@ -317,6 +317,12 @@ void XCUILayoutLabPanel::Render() {
stats.columnCount,
stats.overlayCount,
stats.scrollViewCount);
ImGui::Text(
"Tree items: %zu (%zu expanded) | Property sections: %zu (%zu expanded)",
stats.treeItemCount,
stats.expandedTreeItemCount,
stats.propertySectionCount,
stats.expandedPropertySectionCount);
ImGui::Text(
"Draw lists: %zu | Draw commands: %zu",
stats.drawListCount,