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

@@ -11,6 +11,11 @@
<Token name="space.stack" type="float" value="12" />
<Token name="space.cardInset" type="float" value="12" />
<Token name="radius.card" type="float" value="10" />
<Token name="size.listItemHeight" type="float" value="60" />
<Token name="size.treeItemHeight" type="float" value="30" />
<Token name="size.treeIndent" type="float" value="18" />
<Token name="size.fieldRowHeight" type="float" value="32" />
<Token name="size.propertySectionHeight" type="float" value="156" />
<Token name="font.title" type="float" value="16" />
<Token name="font.body" type="float" value="13" />
</Theme>

View File

@@ -7,37 +7,89 @@
title="XCUI Layout Lab"
subtitle="Resource-driven row / column / overlay stress." />
<Row id="mainRow" height="stretch" gap="14">
<Column id="leftColumn" width="0.28" gap="12">
<Card id="leftTop" height="stretch" title="Left Column" subtitle="Stack item 1" />
<Card id="leftBottom" height="stretch" title="Left Column" subtitle="Stack item 2" />
<Column id="leftRail" width="272" gap="10">
<Card
id="toolShelf"
height="62"
tone="accent-alt"
title="Tool Shelf"
subtitle="Scene, asset, and play-mode actions." />
<ScrollView id="assetList" height="stretch" padding="10" gap="8" scrollY="132">
<Card
id="assetListHeader"
height="54"
tone="accent-alt"
title="Project Browser"
subtitle="Pinned filters and import shortcuts." />
<TreeView id="projectTree" height="156" padding="8" gap="6">
<TreeItem id="treeAssetsRoot" title="Assets" subtitle="Workspace root" />
<TreeItem id="treeScenes" title="Scenes" subtitle="4 authored scenes" indent="1" />
<TreeItem id="treeMaterials" title="Materials" subtitle="Shared lookdev library" indent="1" />
<TreeItem id="treeCharacters" title="Characters" subtitle="Prefab variants" indent="1" />
<TreeItem id="treeUi" title="UI" subtitle="Runtime atlas + themes" indent="1" />
</TreeView>
<ListView id="recentAssetList" height="360" padding="8" gap="6">
<ListItem id="assetMaterials" title="Materials_Master" subtitle="14 assets selected" />
<ListItem id="assetTerrain" title="Terrain_Cliffs_04" subtitle="Texture2D -4096 x 4096" />
<ListItem id="assetLighting" title="Lighting_GlobalRig" subtitle="Prefab -Directional setup" />
<ListItem id="assetCharacter" title="Hero_Character_Controller" subtitle="Prefab -Animation graph bound" />
<ListItem id="assetUiAtlas" title="UI_RuntimeAtlas" subtitle="SpriteAtlas -83 packed sprites" />
</ListView>
</ScrollView>
</Column>
<Overlay id="centerOverlay" width="0.42">
<Column id="centerColumn" width="stretch" gap="10">
<Card
id="overlayBase"
title="Center Overlay"
subtitle="Base layer filling the entire region." />
id="viewportToolbar"
height="62"
title="Viewport Toolbar"
subtitle="Gizmos, snap presets, camera bookmarks." />
<Overlay id="viewportOverlay" height="stretch">
<Card
id="viewportBase"
title="Scene Viewport"
subtitle="Primary preview surface with composition overlays." />
<Card
id="viewportBadge"
x="18"
y="18"
width="224"
height="68"
tone="accent-alt"
title="Selection Overlay"
subtitle="Bounds, pivots, nav markers." />
<Card
id="viewportInspectorBubble"
x="0.58"
y="0.54"
width="0.32"
height="88"
tone="accent-alt"
title="Context Bubble"
subtitle="Inline quick edit affordance." />
</Overlay>
</Column>
<Column id="inspectorColumn" width="320" gap="10">
<Card
id="overlayNorth"
x="18"
y="18"
width="0.42"
height="72"
tone="accent-alt"
title="Overlay A"
subtitle="Floating note" />
<Card
id="overlayCenter"
x="0.28"
y="0.45"
width="0.44"
height="86"
tone="accent-alt"
title="Overlay B"
subtitle="Centered overlay layer" />
</Overlay>
<Column id="rightColumn" width="stretch" gap="12">
<Card id="rightTop" height="stretch" title="Right Column" subtitle="Another stacked column" />
<Card id="rightBottom" height="stretch" title="Right Column" subtitle="Pairs with the overlay stage" />
id="inspectorSummary"
height="88"
title="Inspector Summary"
subtitle="Transform, renderer, and prefab overrides." />
<ScrollView id="inspectorSections" height="stretch" padding="10" gap="8" scrollY="52">
<PropertySection id="inspectorTransform" height="156" title="Transform" subtitle="Position / rotation / scale">
<FieldRow id="fieldPosition" title="Position" subtitle="0.0, 1.5, 0.0" />
<FieldRow id="fieldRotation" title="Rotation" subtitle="0.0, 42.0, 0.0" />
<FieldRow id="fieldScale" title="Scale" subtitle="1.0, 1.0, 1.0" />
</PropertySection>
<PropertySection id="inspectorMesh" height="156" title="Mesh Renderer" subtitle="Materials, shadow flags, probes">
<FieldRow id="fieldMaterial" title="Material" subtitle="M_StylizedTerrain" />
<FieldRow id="fieldShadows" title="Cast Shadows" subtitle="On" />
<FieldRow id="fieldProbe" title="Light Probe" subtitle="Blend Probes" />
</PropertySection>
<PropertySection id="inspectorMetadata" height="132" title="Metadata" subtitle="Tags, labels, import provenance">
<FieldRow id="fieldTags" title="Tags" subtitle="Gameplay, Hero, Traversal" />
<FieldRow id="fieldImportedBy" title="Imported By" subtitle="Asset pipeline v2" />
</PropertySection>
</ScrollView>
</Column>
</Row>
<Overlay id="footerOverlay" height="146">

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 = {};
};