Expand XCUI layout lab editor widgets

This commit is contained in:
2026-04-05 05:44:07 +08:00
parent 01c54d017f
commit 6dcf881967
12 changed files with 608 additions and 389 deletions

View File

@@ -60,6 +60,7 @@ struct LayoutNode {
std::string gapAttr = {};
std::string paddingAttr = {};
std::string scrollYAttr = {};
std::string indentAttr = {};
std::size_t parentIndex = kInvalidIndex;
std::vector<std::size_t> children = {};
UIRect rect = {};
@@ -287,6 +288,30 @@ bool IsScrollViewTag(const std::string& tagName) {
return tagName == "ScrollView";
}
bool IsTreeViewTag(const std::string& tagName) {
return tagName == "TreeView";
}
bool IsTreeItemTag(const std::string& tagName) {
return tagName == "TreeItem";
}
bool IsListViewTag(const std::string& tagName) {
return tagName == "ListView";
}
bool IsListItemTag(const std::string& tagName) {
return tagName == "ListItem";
}
bool IsPropertySectionTag(const std::string& tagName) {
return tagName == "PropertySection";
}
bool IsFieldRowTag(const std::string& tagName) {
return tagName == "FieldRow";
}
float ResolveScalar(
const std::string& text,
float referenceValue,
@@ -417,6 +442,7 @@ void BuildNodesRecursive(
layoutNode.gapAttr = GetAttributeValue(node, "gap");
layoutNode.paddingAttr = GetAttributeValue(node, "padding");
layoutNode.scrollYAttr = GetAttributeValue(node, "scrollY");
layoutNode.indentAttr = GetAttributeValue(node, "indent");
layoutNode.parentIndex = parentIndex;
layoutNode.depth = depth;
@@ -458,6 +484,12 @@ float ResolvePadding(const LayoutNode& node, const Style::UITheme& theme) {
ResolveFloatToken(theme, "space.outer", 18.0f));
}
if (IsTreeViewTag(node.tagName) ||
IsListViewTag(node.tagName) ||
IsPropertySectionTag(node.tagName)) {
return ResolveFloatToken(theme, "space.cardInset", 12.0f);
}
return node.tagName == "View"
? ResolveFloatToken(theme, "space.outer", 18.0f)
: 0.0f;
@@ -480,10 +512,44 @@ float ResolveListItemHeight(const Style::UITheme& theme) {
return ResolveFloatToken(theme, "size.listItemHeight", 60.0f);
}
float ResolveTreeIndent(const LayoutNode& node, const Style::UITheme& theme) {
float indentLevel = 0.0f;
if (!node.indentAttr.empty()) {
TryParseFloat(node.indentAttr, indentLevel);
}
return (std::max)(0.0f, indentLevel) *
ResolveFloatToken(theme, "size.treeIndent", 18.0f);
}
float ResolveFieldRowHeight(const Style::UITheme& theme) {
return ResolveFloatToken(theme, "size.fieldRowHeight", 32.0f);
}
UIRect GetContentRect(const LayoutNode& node, const Style::UITheme& theme) {
return InsetRect(node.rect, ResolvePadding(node, theme));
}
float ResolveDefaultHeight(const LayoutNode& node, const Style::UITheme& theme) {
if (IsTreeItemTag(node.tagName)) {
return ResolveFloatToken(theme, "size.treeItemHeight", 28.0f);
}
if (IsListItemTag(node.tagName)) {
return ResolveListItemHeight(theme);
}
if (IsFieldRowTag(node.tagName)) {
return ResolveFieldRowHeight(theme);
}
if (IsPropertySectionTag(node.tagName)) {
return ResolveFloatToken(theme, "size.propertySectionHeight", 148.0f);
}
return 0.0f;
}
void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex);
void LayoutColumnChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
@@ -505,7 +571,10 @@ void LayoutColumnChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
continue;
}
resolvedHeights[childOffset] = ResolveScalar(child.heightAttr, contentRect.height, 0.0f);
resolvedHeights[childOffset] = ResolveScalar(
child.heightAttr,
contentRect.height,
ResolveDefaultHeight(child, state.theme));
fixedHeight += resolvedHeights[childOffset];
}
@@ -608,10 +677,15 @@ void LayoutScrollViewChildren(RuntimeBuildContext& state, std::size_t nodeIndex)
float cursorY = contentRect.y - scrollOffset;
for (std::size_t childIndex : node.children) {
LayoutNode& child = state.nodes[childIndex];
const float childHeight =
!IsStretch(child.heightAttr)
? ResolveScalar(child.heightAttr, contentRect.height, defaultItemHeight)
: defaultItemHeight;
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 childWidth =
!IsStretch(child.widthAttr)
? (std::min)(
@@ -628,7 +702,11 @@ void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex) {
const LayoutNode& node = state.nodes[nodeIndex];
state.rectsById[node.id] = node.rect;
if (node.tagName == "View" || node.tagName == "Column") {
if (node.tagName == "View" ||
node.tagName == "Column" ||
IsTreeViewTag(node.tagName) ||
IsListViewTag(node.tagName) ||
IsPropertySectionTag(node.tagName)) {
LayoutColumnChildren(state, nodeIndex);
} else if (node.tagName == "Row") {
LayoutRowChildren(state, nodeIndex);
@@ -652,7 +730,9 @@ void DrawNode(
"color.panel",
Color(0.07f, 0.10f, 0.14f, 1.0f));
drawList.AddFilledRect(node.rect, ToUIColor(panelColor), 0.0f);
} else if (IsScrollViewTag(node.tagName)) {
} else if (IsScrollViewTag(node.tagName) ||
IsTreeViewTag(node.tagName) ||
IsListViewTag(node.tagName)) {
const Color surfaceColor = ResolveColorToken(
state.theme,
"color.scroll.surface",
@@ -664,6 +744,44 @@ void DrawNode(
const float rounding = ResolveFloatToken(state.theme, "radius.card", 10.0f);
drawList.AddFilledRect(node.rect, ToUIColor(surfaceColor), rounding);
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
} else if (IsPropertySectionTag(node.tagName)) {
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 textColor = ResolveColorToken(
state.theme,
"color.text",
Color(0.95f, 0.97f, 1.0f, 1.0f));
const Color mutedColor = ResolveColorToken(
state.theme,
"color.text.muted",
Color(0.72f, 0.79f, 0.86f, 1.0f));
const float rounding = ResolveFloatToken(state.theme, "radius.card", 10.0f);
const float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f);
const float titleFont = ResolveFloatToken(state.theme, "font.title", 16.0f);
const float bodyFont = ResolveFloatToken(state.theme, "font.body", 13.0f);
drawList.AddFilledRect(node.rect, ToUIColor(sectionColor), rounding);
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
if (!node.title.empty()) {
drawList.AddText(
UIPoint(node.rect.x + inset, node.rect.y + inset),
node.title,
ToUIColor(textColor),
titleFont);
}
if (!node.subtitle.empty()) {
drawList.AddText(
UIPoint(node.rect.x + inset, node.rect.y + inset + titleFont + 6.0f),
node.subtitle,
ToUIColor(mutedColor),
bodyFont);
}
} else if (node.tagName == "Card") {
const Color cardColor = ResolveColorToken(
state.theme,
@@ -713,9 +831,82 @@ void DrawNode(
2.0f,
rounding);
}
} else if (IsTreeItemTag(node.tagName) || IsListItemTag(node.tagName)) {
const bool hovered = node.id == hoveredId;
const Color rowColor = hovered
? ResolveColorToken(state.theme, "color.card.alt", Color(0.20f, 0.27f, 0.34f, 1.0f))
: 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 textColor = ResolveColorToken(
state.theme,
"color.text",
Color(0.95f, 0.97f, 1.0f, 1.0f));
const Color mutedColor = ResolveColorToken(
state.theme,
"color.text.muted",
Color(0.72f, 0.79f, 0.86f, 1.0f));
const float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f);
const float titleFont = ResolveFloatToken(state.theme, "font.body", 13.0f);
const float bodyFont = ResolveFloatToken(state.theme, "font.body", 13.0f);
const float rounding = ResolveFloatToken(state.theme, "radius.card", 10.0f);
const float indent = IsTreeItemTag(node.tagName) ? ResolveTreeIndent(node, state.theme) : 0.0f;
drawList.AddFilledRect(node.rect, ToUIColor(rowColor), rounding);
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
if (IsTreeItemTag(node.tagName)) {
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),
node.title,
ToUIColor(textColor),
titleFont);
if (!node.subtitle.empty()) {
drawList.AddText(
UIPoint(node.rect.x + inset + indent, node.rect.y + 8.0f + titleFont + 4.0f),
node.subtitle,
ToUIColor(mutedColor),
bodyFont);
}
} else if (IsFieldRowTag(node.tagName)) {
const bool hovered = node.id == hoveredId;
const Color textColor = ResolveColorToken(
state.theme,
"color.text",
Color(0.95f, 0.97f, 1.0f, 1.0f));
const Color mutedColor = ResolveColorToken(
state.theme,
"color.text.muted",
Color(0.72f, 0.79f, 0.86f, 1.0f));
const Color lineColor = hovered
? 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 float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f);
const float fontSize = ResolveFloatToken(state.theme, "font.body", 13.0f);
drawList.AddRectOutline(node.rect, ToUIColor(lineColor), 1.0f, 6.0f);
drawList.AddText(
UIPoint(node.rect.x + inset, node.rect.y + 8.0f),
node.title,
ToUIColor(textColor),
fontSize);
drawList.AddText(
UIPoint(node.rect.x + node.rect.width * 0.54f, node.rect.y + 8.0f),
node.subtitle,
ToUIColor(mutedColor),
fontSize);
}
const bool clipsChildren = IsScrollViewTag(node.tagName);
const bool clipsChildren =
IsScrollViewTag(node.tagName) ||
IsTreeViewTag(node.tagName) ||
IsListViewTag(node.tagName);
if (clipsChildren) {
drawList.PushClipRect(GetContentRect(node, state.theme));
}
@@ -734,7 +925,9 @@ bool IsPointInsideNodeClipping(
std::size_t currentIndex = nodeIndex;
while (currentIndex != kInvalidIndex) {
const LayoutNode& currentNode = state.nodes[currentIndex];
if (IsScrollViewTag(currentNode.tagName) &&
if ((IsScrollViewTag(currentNode.tagName) ||
IsTreeViewTag(currentNode.tagName) ||
IsListViewTag(currentNode.tagName)) &&
!ContainsPoint(GetContentRect(currentNode, state.theme), point)) {
return false;
}
@@ -751,7 +944,12 @@ std::size_t HitTest(
int bestDepth = -1;
for (std::size_t index = 0; index < state.nodes.size(); ++index) {
const LayoutNode& node = state.nodes[index];
if (node.tagName != "Card" ||
const bool hoverable =
node.tagName == "Card" ||
IsTreeItemTag(node.tagName) ||
IsListItemTag(node.tagName) ||
IsFieldRowTag(node.tagName);
if (!hoverable ||
!ContainsPoint(node.rect, point) ||
!IsPointInsideNodeClipping(state, index, point)) {
continue;
@@ -890,6 +1088,18 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
++state.frameResult.stats.overlayCount;
} else if (IsScrollViewTag(node.tagName)) {
++state.frameResult.stats.scrollViewCount;
} else if (IsTreeViewTag(node.tagName)) {
++state.frameResult.stats.treeViewCount;
} else if (IsTreeItemTag(node.tagName)) {
++state.frameResult.stats.treeItemCount;
} else if (IsListViewTag(node.tagName)) {
++state.frameResult.stats.listViewCount;
} else if (IsListItemTag(node.tagName)) {
++state.frameResult.stats.listItemCount;
} else if (IsPropertySectionTag(node.tagName)) {
++state.frameResult.stats.propertySectionCount;
} else if (IsFieldRowTag(node.tagName)) {
++state.frameResult.stats.fieldRowCount;
}
}

View File

@@ -40,6 +40,12 @@ struct XCUILayoutLabFrameStats {
std::size_t columnCount = 0;
std::size_t overlayCount = 0;
std::size_t scrollViewCount = 0;
std::size_t treeViewCount = 0;
std::size_t treeItemCount = 0;
std::size_t listViewCount = 0;
std::size_t listItemCount = 0;
std::size_t propertySectionCount = 0;
std::size_t fieldRowCount = 0;
std::string hoveredElementId = {};
};