From 6dcf881967e6bd6bb6f4e145bef0cbe92851d0b1 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 05:44:07 +0800 Subject: [PATCH] Expand XCUI layout lab editor widgets --- Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme | 4 + Assets/XCUI/NewEditor/LayoutLab/View.xcui | 60 ++- docs/plan/XCUI_Phase_Status_2026-04-05.md | 13 +- .../src/UI/Runtime/UIScreenDocumentHost.cpp | 9 + engine/src/UI/Runtime/UISystem.cpp | 118 +++--- .../resources/xcui_layout_lab_theme.xctheme | 5 + .../resources/xcui_layout_lab_view.xcui | 108 +++-- .../src/XCUIBackend/XCUILayoutLabRuntime.cpp | 230 ++++++++++- .../src/XCUIBackend/XCUILayoutLabRuntime.h | 6 + tests/Core/UI/test_ui_core.cpp | 43 ++ tests/Core/UI/test_ui_runtime.cpp | 375 +++++------------- .../test_xcui_layout_lab_runtime.cpp | 26 ++ 12 files changed, 608 insertions(+), 389 deletions(-) diff --git a/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme b/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme index 798a42ba..93b5460e 100644 --- a/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme +++ b/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme @@ -13,6 +13,10 @@ + + + + diff --git a/Assets/XCUI/NewEditor/LayoutLab/View.xcui b/Assets/XCUI/NewEditor/LayoutLab/View.xcui index 99b88dfd..471b2e24 100644 --- a/Assets/XCUI/NewEditor/LayoutLab/View.xcui +++ b/Assets/XCUI/NewEditor/LayoutLab/View.xcui @@ -14,21 +14,30 @@ tone="accent-alt" title="Tool Shelf" subtitle="Scene, asset, and play-mode actions." /> - + - - - - - - - - + + + + + + + + + + + + + + + + + @@ -69,12 +78,33 @@ title="Inspector Summary" subtitle="Transform, renderer, and prefab overrides." /> - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 2ff236b9..8d12a40a 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -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. diff --git a/engine/src/UI/Runtime/UIScreenDocumentHost.cpp b/engine/src/UI/Runtime/UIScreenDocumentHost.cpp index a85c22a8..08d3b689 100644 --- a/engine/src/UI/Runtime/UIScreenDocumentHost.cpp +++ b/engine/src/UI/Runtime/UIScreenDocumentHost.cpp @@ -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); } diff --git a/engine/src/UI/Runtime/UISystem.cpp b/engine/src/UI/Runtime/UISystem.cpp index dc724bec..23f81e5b 100644 --- a/engine/src/UI/Runtime/UISystem.cpp +++ b/engine/src/UI/Runtime/UISystem.cpp @@ -1,11 +1,43 @@ #include -#include - namespace XCEngine { namespace UI { namespace Runtime { +namespace { + +std::size_t FindTopInputLayerIndex( + const std::vector& layerOptions, + std::size_t lowestPresentedIndex) { + if (layerOptions.empty() || lowestPresentedIndex >= layerOptions.size()) { + return static_cast(-1); + } + + for (std::size_t index = layerOptions.size(); index-- > lowestPresentedIndex;) { + if (layerOptions[index].visible && layerOptions[index].acceptsInput) { + return index; + } + } + + return static_cast(-1); +} + +std::size_t FindLowestPresentedLayerIndex(const std::vector& 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 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& 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(-1); } } // namespace Runtime diff --git a/new_editor/resources/xcui_layout_lab_theme.xctheme b/new_editor/resources/xcui_layout_lab_theme.xctheme index 57c2f96e..77e3fe83 100644 --- a/new_editor/resources/xcui_layout_lab_theme.xctheme +++ b/new_editor/resources/xcui_layout_lab_theme.xctheme @@ -11,6 +11,11 @@ + + + + + diff --git a/new_editor/resources/xcui_layout_lab_view.xcui b/new_editor/resources/xcui_layout_lab_view.xcui index cb4d8410..ea81d5d9 100644 --- a/new_editor/resources/xcui_layout_lab_view.xcui +++ b/new_editor/resources/xcui_layout_lab_view.xcui @@ -7,37 +7,89 @@ title="XCUI Layout Lab" subtitle="Resource-driven row / column / overlay stress." /> - - - + + + + + + + + + + + + + + + + + + + - + + id="viewportToolbar" + height="62" + title="Viewport Toolbar" + subtitle="Gizmos, snap presets, camera bookmarks." /> + + + + + + + - - - - - + id="inspectorSummary" + height="88" + title="Inspector Summary" + subtitle="Transform, renderer, and prefab overrides." /> + + + + + + + + + + + + + + + + diff --git a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp index 555827f8..af871c12 100644 --- a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp +++ b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp @@ -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 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; } } diff --git a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h index 08f64a47..e23d4cd6 100644 --- a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h +++ b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h @@ -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 = {}; }; diff --git a/tests/Core/UI/test_ui_core.cpp b/tests/Core/UI/test_ui_core.cpp index 8cbd5e29..3fc54814 100644 --- a/tests/Core/UI/test_ui_core.cpp +++ b/tests/Core/UI/test_ui_core.cpp @@ -1,5 +1,7 @@ #include +#include +#include #include 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 diff --git a/tests/Core/UI/test_ui_runtime.cpp b/tests/Core/UI/test_ui_runtime.cpp index 5ae8789e..457e320a 100644 --- a/tests/Core/UI/test_ui_runtime.cpp +++ b/tests/Core/UI/test_ui_runtime.cpp @@ -4,323 +4,140 @@ #include #include +#include #include #include +#include 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 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 = + "\n" + " \n" + " \n" + " \n" + " \n" + "