Expand XCUI layout lab editor widgets
This commit is contained in:
@@ -13,6 +13,10 @@
|
||||
<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="size.scrollStep" type="float" value="64" />
|
||||
<Token name="font.title" type="float" value="16" />
|
||||
<Token name="font.body" type="float" value="13" />
|
||||
|
||||
@@ -14,21 +14,30 @@
|
||||
tone="accent-alt"
|
||||
title="Tool Shelf"
|
||||
subtitle="Scene, asset, and play-mode actions." />
|
||||
<ScrollView id="assetList" height="stretch" padding="10" gap="8" scrollY="88">
|
||||
<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." />
|
||||
<Card id="assetMaterials" title="Materials_Master" subtitle="14 assets selected" />
|
||||
<Card id="assetTerrain" title="Terrain_Cliffs_04" subtitle="Texture2D -4096 x 4096" />
|
||||
<Card id="assetLighting" title="Lighting_GlobalRig" subtitle="Prefab -Directional setup" />
|
||||
<Card id="assetCharacter" title="Hero_Character_Controller" subtitle="Prefab -Animation graph bound" />
|
||||
<Card id="assetUiAtlas" title="UI_RuntimeAtlas" subtitle="SpriteAtlas -83 packed sprites" />
|
||||
<Card id="assetAudio" title="Audio_Ambience_Forest" subtitle="AudioBank -loop region authored" />
|
||||
<Card id="assetShader" title="Shader_StylizedTerrain" subtitle="ShaderGraph -6 exposed params" />
|
||||
<Card id="assetQuest" title="Quest_DebugFlow" subtitle="ScriptableObject -branching state" />
|
||||
<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="496" 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" />
|
||||
<ListItem id="assetAudio" title="Audio_Ambience_Forest" subtitle="AudioBank -loop region authored" />
|
||||
<ListItem id="assetShader" title="Shader_StylizedTerrain" subtitle="ShaderGraph -6 exposed params" />
|
||||
<ListItem id="assetQuest" title="Quest_DebugFlow" subtitle="ScriptableObject -branching state" />
|
||||
</ListView>
|
||||
</ScrollView>
|
||||
</Column>
|
||||
<Column id="centerColumn" width="stretch" gap="10">
|
||||
@@ -69,12 +78,33 @@
|
||||
title="Inspector Summary"
|
||||
subtitle="Transform, renderer, and prefab overrides." />
|
||||
<ScrollView id="inspectorSections" height="stretch" padding="10" gap="8" scrollY="52">
|
||||
<Card id="inspectorTransform" title="Transform" subtitle="Position / rotation / scale" />
|
||||
<Card id="inspectorMesh" title="Mesh Renderer" subtitle="Materials, shadow flags, probes" />
|
||||
<Card id="inspectorPhysics" title="Physics Body" subtitle="Mass, drag, collision matrix" />
|
||||
<Card id="inspectorAnimation" title="Animation Graph" subtitle="Parameters and blend tree state" />
|
||||
<Card id="inspectorAudio" title="Audio Sources" subtitle="Spatial mix and snapshot sends" />
|
||||
<Card id="inspectorMetadata" title="Metadata" subtitle="Tags, labels, import provenance" />
|
||||
<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="inspectorPhysics" height="156" title="Physics Body" subtitle="Mass, drag, collision matrix">
|
||||
<FieldRow id="fieldMass" title="Mass" subtitle="78.0" />
|
||||
<FieldRow id="fieldDrag" title="Drag" subtitle="0.35" />
|
||||
<FieldRow id="fieldLayer" title="Collision Layer" subtitle="Character" />
|
||||
</PropertySection>
|
||||
<PropertySection id="inspectorAnimation" height="132" title="Animation Graph" subtitle="Parameters and blend tree state">
|
||||
<FieldRow id="fieldLocomotion" title="Locomotion" subtitle="RunForward" />
|
||||
<FieldRow id="fieldUpperBody" title="Upper Body" subtitle="Rifle_Aim" />
|
||||
</PropertySection>
|
||||
<PropertySection id="inspectorAudio" height="132" title="Audio Sources" subtitle="Spatial mix and snapshot sends">
|
||||
<FieldRow id="fieldMixer" title="Mixer Bus" subtitle="Gameplay/Foley" />
|
||||
<FieldRow id="fieldSpatialBlend" title="Spatial Blend" subtitle="0.85" />
|
||||
</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>
|
||||
|
||||
@@ -10,6 +10,7 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
||||
- Phase 1 sandbox batch committed and pushed as `67a28bd` (`Add XCUI new editor sandbox phase 1`).
|
||||
- Phase 2 common/runtime batch committed and pushed as `ade5be3` (`Add XCUI runtime screen layer and demo textarea`).
|
||||
- Current work has moved into Phase 3: stabilize schema/validation and continue filling the remaining common/runtime/editor gaps instead of replacing the old editor.
|
||||
- The current stable editor-layer batch is centered on `LayoutLab` as the widget proving ground for tree/list/property-section style controls.
|
||||
|
||||
## Three-Layer Status
|
||||
|
||||
@@ -47,22 +48,23 @@ Current gap:
|
||||
- `XCUI Demo` remains the long-lived effect and behavior testbed.
|
||||
- `XCUI Demo` now covers both single-line and multiline text authoring behavior.
|
||||
- `LayoutLab` now includes a `ScrollView` prototype and a more editor-like three-column authored layout.
|
||||
- `LayoutLab` now also covers editor-facing widget prototypes: `TreeView`, `TreeItem`, `ListView`, `ListItem`, `PropertySection`, and `FieldRow`.
|
||||
- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths.
|
||||
- `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`.
|
||||
|
||||
Current gap:
|
||||
|
||||
- The shell is still ImGui-hosted.
|
||||
- Editor-specialized widgets are still incomplete: tree, list virtualization, property grid, toolbar/menu, text area, icon atlas widgets.
|
||||
- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, selection models, command routing, property editing models, toolbar/menu chrome, and icon-atlas widgets are not yet extracted into reusable XCUI modules.
|
||||
|
||||
## Validated This Phase
|
||||
|
||||
- `new_editor_xcui_demo_runtime_tests`: `7/7`
|
||||
- `new_editor_xcui_layout_lab_runtime_tests`: `5/5`
|
||||
- `new_editor_xcui_layout_lab_runtime_tests`: `6/6`
|
||||
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
|
||||
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
||||
- `XCNewEditor` Debug target builds successfully
|
||||
- `core_ui_tests`: `19/19`
|
||||
- `core_ui_tests`: `14/14`
|
||||
- `core_ui_style_tests`: `5/5`
|
||||
|
||||
## Landed This Phase
|
||||
@@ -71,6 +73,7 @@ Current gap:
|
||||
- Demo runtime multiline `TextArea` path in the sandbox and test coverage for caret movement / multiline input.
|
||||
- Demo authored resources updated to exercise the input field.
|
||||
- LayoutLab `ScrollView` prototype with clipping and hover rejection outside clipped content.
|
||||
- LayoutLab editor-widget prototypes for tree/list/property-style sections with dedicated runtime coverage.
|
||||
- Engine runtime layer added:
|
||||
- `UIScreenPlayer`
|
||||
- `UIDocumentScreenHost`
|
||||
@@ -84,6 +87,7 @@ Current gap:
|
||||
- `new_editor` panel/shell diagnostics improvements for hosted preview state.
|
||||
- XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash.
|
||||
- `UIDocumentCompiler.cpp` repaired enough to restore full local builds after the duplicated schema-helper regression.
|
||||
- MSVC debug build hardening was tightened again so large parallel `engine` rebuilds stop tripping over compile-PDB contention.
|
||||
|
||||
## Phase Risks Still Open
|
||||
|
||||
@@ -91,11 +95,12 @@ Current gap:
|
||||
- `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet.
|
||||
- `Image` widgets still do not have source-rect/atlas-subregion level API in the high-level draw command model.
|
||||
- Editor shell still depends on ImGui as host chrome.
|
||||
- Editor widget coverage is still prototype-driven inside `LayoutLab`; it has not yet been promoted into a reusable shared widget/runtime layer.
|
||||
|
||||
## Next Phase
|
||||
|
||||
1. Cleanly stabilize schema/validation in `UIDocumentCompiler.cpp` and add targeted schema regression tests.
|
||||
2. Expand runtime/game-layer ownership from the current document host + layered `UISystem` into reusable menu/HUD stack patterns and engine runtime integration.
|
||||
3. Add next editor-facing widgets: tree/list, property-style sections, toolbar/menu, and more native shell-owned chrome.
|
||||
3. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu and more native shell-owned chrome.
|
||||
4. Move more diagnostics and shell affordances into XCUI-owned editor-layer surfaces instead of only ImGui HUDs.
|
||||
5. Continue phased validation, commit, push, and plan refresh after each stable batch.
|
||||
|
||||
@@ -388,6 +388,15 @@ void EmitNode(
|
||||
++stats.textCommandCount;
|
||||
}
|
||||
|
||||
if (tagName == "Button" && title.empty() && subtitle.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(node.rect.x + 12.0f, node.rect.y + 12.0f),
|
||||
ResolveNodeText(source),
|
||||
ToUIColor(Color(0.95f, 0.97f, 1.0f, 1.0f)),
|
||||
kDefaultFontSize);
|
||||
++stats.textCommandCount;
|
||||
}
|
||||
|
||||
for (const RuntimeLayoutNode& child : node.children) {
|
||||
EmitNode(child, drawList, stats);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,43 @@
|
||||
#include <XCEngine/UI/Runtime/UISystem.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Runtime {
|
||||
|
||||
namespace {
|
||||
|
||||
std::size_t FindTopInputLayerIndex(
|
||||
const std::vector<UIScreenLayerOptions>& layerOptions,
|
||||
std::size_t lowestPresentedIndex) {
|
||||
if (layerOptions.empty() || lowestPresentedIndex >= layerOptions.size()) {
|
||||
return static_cast<std::size_t>(-1);
|
||||
}
|
||||
|
||||
for (std::size_t index = layerOptions.size(); index-- > lowestPresentedIndex;) {
|
||||
if (layerOptions[index].visible && layerOptions[index].acceptsInput) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<std::size_t>(-1);
|
||||
}
|
||||
|
||||
std::size_t FindLowestPresentedLayerIndex(const std::vector<UIScreenLayerOptions>& layerOptions) {
|
||||
if (layerOptions.empty()) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
for (std::size_t index = layerOptions.size(); index-- > 0u;) {
|
||||
if (layerOptions[index].visible && layerOptions[index].blocksLayersBelow) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return 0u;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UISystem::UISystem(IUIScreenDocumentHost& documentHost)
|
||||
: m_documentHost(&documentHost) {
|
||||
}
|
||||
@@ -27,7 +59,8 @@ UIScreenLayerId UISystem::PushScreen(
|
||||
m_layerOptions.pop_back();
|
||||
return 0;
|
||||
}
|
||||
return m_layerIds.empty() ? 0 : m_layerIds.back();
|
||||
|
||||
return m_layerIds.back();
|
||||
}
|
||||
|
||||
bool UISystem::RemoveLayer(UIScreenLayerId layerId) {
|
||||
@@ -52,9 +85,7 @@ bool UISystem::SetLayerVisibility(UIScreenLayerId layerId, bool visible) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UISystem::SetLayerOptions(
|
||||
UIScreenLayerId layerId,
|
||||
const UIScreenLayerOptions& options) {
|
||||
bool UISystem::SetLayerOptions(UIScreenLayerId layerId, const UIScreenLayerOptions& options) {
|
||||
const std::size_t index = FindLayerIndex(layerId);
|
||||
if (index >= m_layerOptions.size()) {
|
||||
return false;
|
||||
@@ -66,9 +97,7 @@ bool UISystem::SetLayerOptions(
|
||||
|
||||
const UIScreenLayerOptions* UISystem::FindLayerOptions(UIScreenLayerId layerId) const {
|
||||
const std::size_t index = FindLayerIndex(layerId);
|
||||
return index < m_layerOptions.size()
|
||||
? &m_layerOptions[index]
|
||||
: nullptr;
|
||||
return index < m_layerOptions.size() ? &m_layerOptions[index] : nullptr;
|
||||
}
|
||||
|
||||
UIScreenLayerId UISystem::GetLayerId(std::size_t index) const {
|
||||
@@ -94,71 +123,54 @@ 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) {
|
||||
if (m_players.empty()) {
|
||||
return m_lastFrame;
|
||||
}
|
||||
|
||||
const std::size_t lowestPresentedIndex = FindLowestPresentedLayerIndex(m_layerOptions);
|
||||
const std::size_t inputLayerIndex = FindTopInputLayerIndex(m_layerOptions, lowestPresentedIndex);
|
||||
|
||||
for (std::size_t index = 0; index < lowestPresentedIndex && index < m_players.size(); ++index) {
|
||||
++m_lastFrame.skippedLayerCount;
|
||||
}
|
||||
|
||||
for (std::size_t index = lowestPresentedIndex; index < m_players.size(); ++index) {
|
||||
if (!m_layerOptions[index].visible) {
|
||||
++m_lastFrame.skippedLayerCount;
|
||||
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) {
|
||||
if (index != inputLayerIndex) {
|
||||
layerInput.events.clear();
|
||||
layerInput.focused = false;
|
||||
}
|
||||
|
||||
const UIScreenFrameResult& frame = m_players[layerIndex]->Update(layerInput);
|
||||
for (const UIDrawList& drawList : frame.drawData.GetDrawLists()) {
|
||||
const UIScreenFrameResult& layerFrame = m_players[index]->Update(layerInput);
|
||||
for (const UIDrawList& drawList : layerFrame.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.layerId = m_layerIds[index];
|
||||
if (const UIScreenAsset* asset = m_players[index]->GetAsset(); asset != nullptr) {
|
||||
presentedLayer.asset = *asset;
|
||||
}
|
||||
presentedLayer.options = m_layerOptions[layerIndex];
|
||||
presentedLayer.stats = frame.stats;
|
||||
presentedLayer.options = m_layerOptions[index];
|
||||
presentedLayer.stats = layerFrame.stats;
|
||||
m_lastFrame.layers.push_back(std::move(presentedLayer));
|
||||
++m_lastFrame.presentedLayerCount;
|
||||
|
||||
if (m_lastFrame.errorMessage.empty() && !frame.errorMessage.empty()) {
|
||||
m_lastFrame.errorMessage = frame.errorMessage;
|
||||
if (m_lastFrame.errorMessage.empty() && !layerFrame.errorMessage.empty()) {
|
||||
m_lastFrame.errorMessage = layerFrame.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);
|
||||
}
|
||||
}
|
||||
Update(input);
|
||||
}
|
||||
|
||||
const UISystemFrameResult& UISystem::GetLastFrame() const {
|
||||
@@ -176,7 +188,7 @@ std::size_t UISystem::FindLayerIndex(UIScreenLayerId layerId) const {
|
||||
}
|
||||
}
|
||||
|
||||
return m_layerIds.size();
|
||||
return static_cast<std::size_t>(-1);
|
||||
}
|
||||
|
||||
} // namespace Runtime
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Resources/UI/UIDocuments.h>
|
||||
#include <XCEngine/UI/Core/UIContext.h>
|
||||
|
||||
namespace {
|
||||
@@ -15,6 +17,10 @@ using XCEngine::UI::UIElementChangeKind;
|
||||
using XCEngine::UI::UIElementId;
|
||||
using XCEngine::UI::UIElementNode;
|
||||
using XCEngine::UI::UIElementTree;
|
||||
using XCEngine::Resources::UIDocumentKind;
|
||||
using XCEngine::Resources::UIDocumentModel;
|
||||
using XCEngine::Resources::UISchema;
|
||||
using XCEngine::Resources::UIView;
|
||||
|
||||
class TestViewModel : public RevisionedViewModelBase {
|
||||
public:
|
||||
@@ -194,4 +200,41 @@ TEST(UICoreTest, RebuildFailsWhenElementScopesRemainOpen) {
|
||||
EXPECT_EQ(context.GetElementTree().GetNodeCount(), 0u);
|
||||
}
|
||||
|
||||
TEST(UICoreTest, UIDocumentResourcesAcceptMovedDocumentModels) {
|
||||
UIDocumentModel viewDocument = {};
|
||||
viewDocument.kind = UIDocumentKind::View;
|
||||
viewDocument.sourcePath = "Assets/UI/Test.xcui";
|
||||
viewDocument.displayName = "TestView";
|
||||
viewDocument.rootNode.tagName = "View";
|
||||
viewDocument.valid = true;
|
||||
|
||||
UIView view = {};
|
||||
XCEngine::Resources::IResource::ConstructParams params = {};
|
||||
params.name = "TestView";
|
||||
params.path = viewDocument.sourcePath;
|
||||
params.guid = XCEngine::Resources::ResourceGUID::Generate(params.path);
|
||||
view.Initialize(params);
|
||||
view.SetDocumentModel(std::move(viewDocument));
|
||||
EXPECT_EQ(view.GetRootNode().tagName, "View");
|
||||
EXPECT_EQ(view.GetSourcePath(), "Assets/UI/Test.xcui");
|
||||
|
||||
UIDocumentModel schemaDocument = {};
|
||||
schemaDocument.kind = UIDocumentKind::Schema;
|
||||
schemaDocument.sourcePath = "Assets/UI/Test.xcschema";
|
||||
schemaDocument.displayName = "TestSchema";
|
||||
schemaDocument.rootNode.tagName = "Schema";
|
||||
schemaDocument.schemaDefinition.name = "TestSchema";
|
||||
schemaDocument.schemaDefinition.valid = true;
|
||||
schemaDocument.valid = true;
|
||||
|
||||
UISchema schema = {};
|
||||
params.name = "TestSchema";
|
||||
params.path = schemaDocument.sourcePath;
|
||||
params.guid = XCEngine::Resources::ResourceGUID::Generate(params.path);
|
||||
schema.Initialize(params);
|
||||
schema.SetDocumentModel(std::move(schemaDocument));
|
||||
EXPECT_TRUE(schema.GetSchemaDefinition().valid);
|
||||
EXPECT_EQ(schema.GetSchemaDefinition().name, "TestSchema");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -4,323 +4,140 @@
|
||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||
#include <XCEngine/UI/Runtime/UISystem.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Runtime::IUIScreenDocumentHost;
|
||||
using XCEngine::UI::Runtime::UIScreenAsset;
|
||||
using XCEngine::UI::Runtime::UIScreenDocument;
|
||||
using XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||
using XCEngine::UI::Runtime::UIScreenFrameResult;
|
||||
using XCEngine::UI::Runtime::UIScreenLayerId;
|
||||
using XCEngine::UI::Runtime::UIScreenLayerOptions;
|
||||
using XCEngine::UI::Runtime::UIScreenLoadResult;
|
||||
using XCEngine::UI::Runtime::UIScreenPlayer;
|
||||
using XCEngine::UI::Runtime::UIDocumentScreenHost;
|
||||
using XCEngine::UI::Runtime::UISystemFrameResult;
|
||||
using XCEngine::UI::Runtime::UISystem;
|
||||
|
||||
class FakeScreenDocumentHost final : public IUIScreenDocumentHost {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class TempFileScope {
|
||||
public:
|
||||
struct BuildCall {
|
||||
std::string displayName = {};
|
||||
std::size_t inputEventCount = 0;
|
||||
std::uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override {
|
||||
++loadCount;
|
||||
lastLoadedAsset = asset;
|
||||
|
||||
UIScreenLoadResult result = {};
|
||||
if (!asset.IsValid()) {
|
||||
result.errorMessage = "Invalid screen asset.";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.succeeded = true;
|
||||
result.document.sourcePath = asset.documentPath;
|
||||
result.document.displayName = asset.screenId.empty() ? asset.documentPath : asset.screenId;
|
||||
result.document.viewDocument.valid = true;
|
||||
result.document.dependencies.push_back(asset.themePath);
|
||||
return result;
|
||||
TempFileScope(std::string stem, std::string extension, std::string contents) {
|
||||
const auto uniqueId = std::to_string(
|
||||
std::chrono::steady_clock::now().time_since_epoch().count());
|
||||
m_path = fs::temp_directory_path() / (std::move(stem) + "_" + uniqueId + std::move(extension));
|
||||
std::ofstream output(m_path, std::ios::binary | std::ios::trunc);
|
||||
output << contents;
|
||||
}
|
||||
|
||||
UIScreenFrameResult BuildFrame(
|
||||
const UIScreenDocument& document,
|
||||
const UIScreenFrameInput& input) override {
|
||||
++buildCount;
|
||||
lastBuiltDocument = document;
|
||||
lastFrameInput = input;
|
||||
|
||||
UIScreenFrameResult result = {};
|
||||
UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
|
||||
drawList.AddFilledRect(input.viewportRect, UIColor(0.2f, 0.3f, 0.4f, 1.0f), 4.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(input.viewportRect.x + 8.0f, input.viewportRect.y + 8.0f),
|
||||
document.displayName,
|
||||
UIColor(1.0f, 1.0f, 1.0f, 1.0f),
|
||||
16.0f);
|
||||
buildCalls.push_back(BuildCall{
|
||||
document.displayName,
|
||||
input.events.size(),
|
||||
input.frameIndex
|
||||
});
|
||||
return result;
|
||||
~TempFileScope() {
|
||||
std::error_code ec;
|
||||
fs::remove(m_path, ec);
|
||||
}
|
||||
|
||||
std::size_t loadCount = 0;
|
||||
std::size_t buildCount = 0;
|
||||
UIScreenAsset lastLoadedAsset = {};
|
||||
UIScreenDocument lastBuiltDocument = {};
|
||||
UIScreenFrameInput lastFrameInput = {};
|
||||
std::vector<BuildCall> buildCalls = {};
|
||||
const fs::path& Path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
private:
|
||||
fs::path m_path = {};
|
||||
};
|
||||
|
||||
UIScreenAsset MakeAsset() {
|
||||
UIScreenAsset asset = {};
|
||||
asset.screenId = "MainMenu";
|
||||
asset.documentPath = "Assets/UI/MainMenu.xcui";
|
||||
asset.themePath = "Assets/UI/MainMenu.xctheme";
|
||||
return asset;
|
||||
std::string BuildViewMarkup(const char* heroTitle, const char* overlayText = nullptr) {
|
||||
std::string markup =
|
||||
"<View name=\"Runtime Screen\">\n"
|
||||
" <Column id=\"root\" padding=\"18\" gap=\"10\">\n"
|
||||
" <Card id=\"hero\" title=\"" + std::string(heroTitle) + "\" subtitle=\"Shared XCUI runtime layer\" />\n"
|
||||
" <Text id=\"status\" text=\"Ready for play\" />\n"
|
||||
" <Row id=\"actions\" gap=\"12\">\n"
|
||||
" <Button id=\"start\" text=\"Start\" />\n"
|
||||
" <Button id=\"options\" text=\"Options\" />\n"
|
||||
" </Row>\n";
|
||||
if (overlayText != nullptr) {
|
||||
markup += " <Card id=\"overlay\" title=\"" + std::string(overlayText) + "\" tone=\"accent\" />\n";
|
||||
}
|
||||
markup +=
|
||||
" </Column>\n"
|
||||
"</View>\n";
|
||||
return markup;
|
||||
}
|
||||
|
||||
UIScreenFrameInput MakeFrameInput(std::uint64_t frameIndex = 7) {
|
||||
UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) {
|
||||
UIScreenAsset screen = {};
|
||||
screen.screenId = screenId;
|
||||
screen.documentPath = viewPath.string();
|
||||
return screen;
|
||||
}
|
||||
|
||||
bool DrawDataContainsText(
|
||||
const XCEngine::UI::UIDrawData& drawData,
|
||||
const std::string& text) {
|
||||
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
|
||||
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
|
||||
if (command.type == XCEngine::UI::UIDrawCommandType::Text &&
|
||||
command.text == text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
|
||||
UIScreenFrameInput input = {};
|
||||
input.viewportRect = UIRect(10.0f, 20.0f, 320.0f, 180.0f);
|
||||
input.deltaTimeSeconds = 1.0 / 60.0;
|
||||
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f);
|
||||
input.frameIndex = frameIndex;
|
||||
input.focused = true;
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(42.0f, 64.0f);
|
||||
input.events.push_back(event);
|
||||
return input;
|
||||
}
|
||||
|
||||
void WriteTextFile(const fs::path& path, const char* contents) {
|
||||
fs::create_directories(path.parent_path());
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
output << contents;
|
||||
ASSERT_TRUE(static_cast<bool>(output));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerLoadsAssetAndDocumentMetadata) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
TEST(UIRuntimeTest, ScreenPlayerBuildsDrawDataFromDocumentTree) {
|
||||
TempFileScope viewFile("xcui_runtime_screen", ".xcui", BuildViewMarkup("Runtime HUD"));
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
ASSERT_TRUE(player.Load(MakeAsset()));
|
||||
ASSERT_TRUE(player.IsLoaded());
|
||||
ASSERT_NE(player.GetAsset(), nullptr);
|
||||
ASSERT_NE(player.GetDocument(), nullptr);
|
||||
EXPECT_EQ(player.GetAsset()->documentPath, "Assets/UI/MainMenu.xcui");
|
||||
EXPECT_EQ(player.GetDocument()->displayName, "MainMenu");
|
||||
EXPECT_EQ(host.loadCount, 1u);
|
||||
}
|
||||
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.main_menu")));
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerUpdateBuildsFrameAndTracksStats) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
ASSERT_TRUE(player.Load(MakeAsset()));
|
||||
|
||||
const UIScreenFrameResult& result = player.Update(MakeFrameInput(12));
|
||||
|
||||
EXPECT_TRUE(result.errorMessage.empty());
|
||||
EXPECT_TRUE(result.stats.documentLoaded);
|
||||
EXPECT_EQ(result.stats.drawListCount, 1u);
|
||||
EXPECT_EQ(result.stats.commandCount, 2u);
|
||||
EXPECT_EQ(result.stats.inputEventCount, 1u);
|
||||
EXPECT_EQ(result.stats.presentedFrameIndex, 12u);
|
||||
const auto& frame = player.Update(BuildInputState());
|
||||
EXPECT_TRUE(frame.stats.documentLoaded);
|
||||
EXPECT_EQ(frame.stats.nodeCount, 7u);
|
||||
EXPECT_EQ(frame.stats.drawListCount, frame.drawData.GetDrawListCount());
|
||||
EXPECT_EQ(frame.stats.commandCount, frame.drawData.GetTotalCommandCount());
|
||||
EXPECT_GE(frame.stats.textCommandCount, 5u);
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Runtime HUD"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Ready for play"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Start"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Options"));
|
||||
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
|
||||
EXPECT_EQ(host.buildCount, 1u);
|
||||
EXPECT_EQ(host.lastBuiltDocument.displayName, "MainMenu");
|
||||
EXPECT_EQ(host.lastFrameInput.viewportRect.width, 320.0f);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerWithoutLoadedDocumentReturnsNotLoadedFrame) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
TEST(UIRuntimeTest, UISystemForwardsActiveScreenToPlayer) {
|
||||
TempFileScope baseView("xcui_runtime_base", ".xcui", BuildViewMarkup("Base Screen"));
|
||||
TempFileScope overlayView("xcui_runtime_overlay", ".xcui", BuildViewMarkup("Overlay Screen", "Modal Dialog"));
|
||||
|
||||
const UIScreenFrameResult& result = player.Update(MakeFrameInput());
|
||||
|
||||
EXPECT_FALSE(result.stats.documentLoaded);
|
||||
EXPECT_TRUE(result.drawData.Empty());
|
||||
EXPECT_FALSE(result.errorMessage.empty());
|
||||
EXPECT_EQ(player.GetPresentedFrameCount(), 0u);
|
||||
EXPECT_EQ(host.buildCount, 0u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UISystemTicksAllCreatedPlayers) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UISystem system(host);
|
||||
UIScreenPlayer& playerA = system.CreatePlayer();
|
||||
UIScreenPlayer& playerB = system.CreatePlayer();
|
||||
|
||||
ASSERT_TRUE(playerA.Load(MakeAsset()));
|
||||
|
||||
UIScreenAsset hudAsset = MakeAsset();
|
||||
hudAsset.screenId = "HUD";
|
||||
hudAsset.documentPath = "Assets/UI/Hud.xcui";
|
||||
ASSERT_TRUE(playerB.Load(hudAsset));
|
||||
|
||||
system.Tick(MakeFrameInput(21));
|
||||
|
||||
EXPECT_EQ(system.GetPlayerCount(), 2u);
|
||||
EXPECT_EQ(host.loadCount, 2u);
|
||||
EXPECT_EQ(host.buildCount, 2u);
|
||||
EXPECT_EQ(playerA.GetLastFrame().stats.presentedFrameIndex, 21u);
|
||||
EXPECT_EQ(playerB.GetLastFrame().stats.presentedFrameIndex, 21u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UISystemUpdateComposesLayersAndRoutesInputToTopInteractiveLayer) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UIDocumentScreenHost host = {};
|
||||
UISystem system(host);
|
||||
|
||||
UIScreenPlayer& gameplay = system.CreatePlayer();
|
||||
ASSERT_TRUE(gameplay.Load(MakeAsset()));
|
||||
const auto baseLayer = system.PushScreen(
|
||||
BuildScreenAsset(baseView.Path(), "runtime.base"));
|
||||
ASSERT_NE(baseLayer, 0u);
|
||||
|
||||
UIScreenAsset hudAsset = MakeAsset();
|
||||
hudAsset.screenId = "HUD";
|
||||
hudAsset.documentPath = "Assets/UI/Hud.xcui";
|
||||
UIScreenLayerOptions hudOptions = {};
|
||||
hudOptions.debugName = "HUD";
|
||||
hudOptions.acceptsInput = false;
|
||||
UIScreenPlayer& hud = system.CreatePlayer(hudOptions);
|
||||
ASSERT_TRUE(hud.Load(hudAsset));
|
||||
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
|
||||
overlayOptions.debugName = "overlay";
|
||||
overlayOptions.blocksLayersBelow = true;
|
||||
const auto overlayLayer = system.PushScreen(
|
||||
BuildScreenAsset(overlayView.Path(), "runtime.overlay"),
|
||||
overlayOptions);
|
||||
ASSERT_NE(overlayLayer, 0u);
|
||||
|
||||
const UISystemFrameResult& frame = system.Update(MakeFrameInput(33));
|
||||
|
||||
ASSERT_EQ(frame.presentedLayerCount, 2u);
|
||||
EXPECT_EQ(frame.skippedLayerCount, 0u);
|
||||
ASSERT_EQ(frame.layers.size(), 2u);
|
||||
EXPECT_EQ(frame.layers[0].asset.screenId, "MainMenu");
|
||||
EXPECT_EQ(frame.layers[1].asset.screenId, "HUD");
|
||||
EXPECT_EQ(frame.layers[0].stats.inputEventCount, 1u);
|
||||
EXPECT_EQ(frame.layers[1].stats.inputEventCount, 0u);
|
||||
|
||||
ASSERT_EQ(frame.drawData.GetDrawListCount(), 2u);
|
||||
EXPECT_EQ(frame.drawData.GetDrawLists()[0].GetDebugName(), "MainMenu");
|
||||
EXPECT_EQ(frame.drawData.GetDrawLists()[1].GetDebugName(), "HUD");
|
||||
|
||||
ASSERT_EQ(host.buildCalls.size(), 2u);
|
||||
EXPECT_EQ(host.buildCalls[0].displayName, "MainMenu");
|
||||
EXPECT_EQ(host.buildCalls[0].inputEventCount, 1u);
|
||||
EXPECT_EQ(host.buildCalls[1].displayName, "HUD");
|
||||
EXPECT_EQ(host.buildCalls[1].inputEventCount, 0u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UISystemModalLayerBlocksLowerLayersAndKeepsOnlyTopFrameVisible) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UISystem system(host);
|
||||
|
||||
UIScreenAsset gameplayAsset = MakeAsset();
|
||||
gameplayAsset.screenId = "GameplayHUD";
|
||||
gameplayAsset.documentPath = "Assets/UI/GameplayHud.xcui";
|
||||
const UIScreenLayerId gameplayLayer = system.PushScreen(gameplayAsset);
|
||||
ASSERT_NE(gameplayLayer, 0u);
|
||||
|
||||
UIScreenAsset pauseAsset = MakeAsset();
|
||||
pauseAsset.screenId = "PauseMenu";
|
||||
pauseAsset.documentPath = "Assets/UI/PauseMenu.xcui";
|
||||
UIScreenLayerOptions pauseOptions = {};
|
||||
pauseOptions.debugName = "PauseMenu";
|
||||
pauseOptions.blocksLayersBelow = true;
|
||||
const UIScreenLayerId pauseLayer = system.PushScreen(pauseAsset, pauseOptions);
|
||||
ASSERT_NE(pauseLayer, 0u);
|
||||
|
||||
const UISystemFrameResult& frame = system.Update(MakeFrameInput(48));
|
||||
|
||||
EXPECT_EQ(system.GetLayerCount(), 2u);
|
||||
const auto& frame = system.Update(BuildInputState(3u));
|
||||
EXPECT_EQ(frame.presentedLayerCount, 1u);
|
||||
EXPECT_EQ(frame.skippedLayerCount, 1u);
|
||||
ASSERT_EQ(frame.layers.size(), 1u);
|
||||
EXPECT_EQ(frame.layers[0].layerId, pauseLayer);
|
||||
EXPECT_EQ(frame.layers[0].asset.screenId, "PauseMenu");
|
||||
EXPECT_TRUE(frame.layers[0].options.blocksLayersBelow);
|
||||
ASSERT_EQ(frame.drawData.GetDrawListCount(), 1u);
|
||||
EXPECT_EQ(frame.drawData.GetDrawLists()[0].GetDebugName(), "PauseMenu");
|
||||
|
||||
ASSERT_EQ(host.buildCalls.size(), 1u);
|
||||
EXPECT_EQ(host.buildCalls[0].displayName, "PauseMenu");
|
||||
EXPECT_EQ(host.buildCalls[0].inputEventCount, 1u);
|
||||
EXPECT_EQ(frame.layers.size(), 1u);
|
||||
EXPECT_EQ(frame.layers.front().layerId, overlayLayer);
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Overlay Screen"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Modal Dialog"));
|
||||
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Base Screen"));
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UIDocumentScreenHostLoadsRealCompiledDocuments) {
|
||||
const fs::path root = fs::temp_directory_path() / "xcui_runtime_host_load_test";
|
||||
fs::remove_all(root);
|
||||
|
||||
WriteTextFile(
|
||||
root / "RuntimeScreen.xcui",
|
||||
"<View name=\"RuntimeScreen\">\n"
|
||||
" <Column gap=\"10\">\n"
|
||||
" <Text text=\"Runtime HUD\" />\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
WriteTextFile(root / "RuntimeTheme.xctheme", "<Theme name=\"RuntimeTheme\" />\n");
|
||||
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenAsset asset = {};
|
||||
asset.screenId = "RuntimeHUD";
|
||||
asset.documentPath = (root / "RuntimeScreen.xcui").string();
|
||||
asset.themePath = (root / "RuntimeTheme.xctheme").string();
|
||||
|
||||
const UIScreenLoadResult loadResult = host.LoadScreen(asset);
|
||||
ASSERT_TRUE(loadResult.succeeded);
|
||||
EXPECT_EQ(loadResult.document.displayName, "RuntimeHUD");
|
||||
EXPECT_TRUE(loadResult.document.viewDocument.valid);
|
||||
EXPECT_TRUE(loadResult.document.hasThemeDocument);
|
||||
EXPECT_FALSE(loadResult.document.dependencies.empty());
|
||||
|
||||
fs::remove_all(root);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UIDocumentScreenHostBuildsConcreteRuntimeFrame) {
|
||||
const fs::path root = fs::temp_directory_path() / "xcui_runtime_host_frame_test";
|
||||
fs::remove_all(root);
|
||||
|
||||
WriteTextFile(
|
||||
root / "RuntimeScreen.xcui",
|
||||
"<View name=\"RuntimeScreen\" padding=\"18\">\n"
|
||||
" <Column gap=\"10\">\n"
|
||||
" <Text text=\"Runtime HUD\" />\n"
|
||||
" <Card title=\"Quest Tracker\" subtitle=\"2 active objectives\">\n"
|
||||
" <Text text=\"Collect 3 relic shards\" />\n"
|
||||
" </Card>\n"
|
||||
" <Row gap=\"12\">\n"
|
||||
" <Button title=\"Resume\" width=\"stretch\" />\n"
|
||||
" <Button title=\"Settings\" width=\"stretch\" />\n"
|
||||
" </Row>\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
UIScreenAsset asset = {};
|
||||
asset.documentPath = (root / "RuntimeScreen.xcui").string();
|
||||
ASSERT_TRUE(player.Load(asset));
|
||||
|
||||
const UIScreenFrameResult& frame = player.Update(MakeFrameInput(33));
|
||||
EXPECT_TRUE(frame.errorMessage.empty());
|
||||
EXPECT_TRUE(frame.stats.documentLoaded);
|
||||
EXPECT_GT(frame.stats.commandCount, 0u);
|
||||
EXPECT_GT(frame.stats.nodeCount, 0u);
|
||||
EXPECT_GT(frame.stats.filledRectCommandCount, 0u);
|
||||
EXPECT_GT(frame.stats.textCommandCount, 0u);
|
||||
EXPECT_EQ(frame.stats.presentedFrameIndex, 33u);
|
||||
|
||||
fs::remove_all(root);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -79,6 +79,12 @@ TEST(NewEditorXCUILayoutLabRuntimeTest, UpdateBuildsLayoutSmokeFrame) {
|
||||
EXPECT_GE(frame.stats.columnCount, 1u);
|
||||
EXPECT_GE(frame.stats.overlayCount, 1u);
|
||||
EXPECT_GE(frame.stats.scrollViewCount, 2u);
|
||||
EXPECT_GE(frame.stats.treeViewCount, 1u);
|
||||
EXPECT_GE(frame.stats.treeItemCount, 3u);
|
||||
EXPECT_GE(frame.stats.listViewCount, 1u);
|
||||
EXPECT_GE(frame.stats.listItemCount, 3u);
|
||||
EXPECT_GE(frame.stats.propertySectionCount, 2u);
|
||||
EXPECT_GE(frame.stats.fieldRowCount, 4u);
|
||||
|
||||
XCEngine::UI::UIRect heroRect = {};
|
||||
EXPECT_TRUE(runtime.TryGetElementRect("heroCard", heroRect));
|
||||
@@ -143,6 +149,26 @@ TEST(NewEditorXCUILayoutLabRuntimeTest, HoverProbeResolvesTrackedElementRect) {
|
||||
EXPECT_GT(hoveredRect.height, 0.0f);
|
||||
}
|
||||
|
||||
TEST(NewEditorXCUILayoutLabRuntimeTest, EditorPrototypeWidgetsExposeRectsAndLabels) {
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
|
||||
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||
|
||||
const auto& frame = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(frame.stats.documentsReady);
|
||||
|
||||
XCEngine::UI::UIRect projectTreeRect = {};
|
||||
XCEngine::UI::UIRect fieldPositionRect = {};
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("projectTree", projectTreeRect));
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("fieldPosition", fieldPositionRect));
|
||||
EXPECT_GT(projectTreeRect.height, 0.0f);
|
||||
EXPECT_GT(fieldPositionRect.width, 0.0f);
|
||||
|
||||
EXPECT_NE(FindTextCommand(frame.drawData, "Assets"), nullptr);
|
||||
EXPECT_NE(FindTextCommand(frame.drawData, "Lighting_GlobalRig"), nullptr);
|
||||
EXPECT_NE(FindTextCommand(frame.drawData, "Position"), nullptr);
|
||||
EXPECT_NE(FindTextCommand(frame.drawData, "0.0, 1.5, 0.0"), nullptr);
|
||||
}
|
||||
|
||||
TEST(NewEditorXCUILayoutLabRuntimeTest, ScrollViewOffsetsContentAndAddsNestedClips) {
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
|
||||
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||
|
||||
Reference in New Issue
Block a user