Add XCUI expansion state and coverage tests
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
tone="accent-alt"
|
||||
title="Tool Shelf"
|
||||
subtitle="Scene, asset, and play-mode actions." />
|
||||
<ScrollView id="assetList" height="stretch" padding="10" gap="8" scrollY="132">
|
||||
<ScrollView id="assetList" height="stretch" padding="10" gap="8" scrollY="40">
|
||||
<Card
|
||||
id="assetListHeader"
|
||||
height="54"
|
||||
@@ -29,9 +29,9 @@
|
||||
<TreeItem id="treeUi" title="UI" subtitle="Runtime atlas + themes" indent="1" />
|
||||
</TreeView>
|
||||
<ListView id="recentAssetList" height="496" padding="8" gap="6">
|
||||
<ListItem id="assetLighting" title="Lighting_GlobalRig" subtitle="Prefab -Directional setup" />
|
||||
<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" />
|
||||
@@ -77,7 +77,7 @@
|
||||
height="88"
|
||||
title="Inspector Summary"
|
||||
subtitle="Transform, renderer, and prefab overrides." />
|
||||
<ScrollView id="inspectorSections" height="stretch" padding="10" gap="8" scrollY="52">
|
||||
<ScrollView id="inspectorSections" height="stretch" padding="10" gap="8" scrollY="0">
|
||||
<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" />
|
||||
|
||||
@@ -33,13 +33,14 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
||||
- Shared text-input controller/state now also lives under `engine/include/XCEngine/UI/Text` and `engine/src/UI/Text`, so character insertion, backspace/delete, submit, and multiline key handling no longer need to be reimplemented per host.
|
||||
- Shared editor collection primitive classification and metric helpers now also live under `engine/include/XCEngine/UI/Widgets` and `engine/src/UI/Widgets`, covering the current `ScrollView` / `TreeView` / `ListView` / `PropertySection` / `FieldRow` prototype taxonomy.
|
||||
- Shared single-selection state now also lives under `engine/include/XCEngine/UI/Widgets` and `engine/src/UI/Widgets` as `UISelectionModel`, so collection-style widget selection no longer has to stay private to `LayoutLab`.
|
||||
- Shared expansion state now also lives under `engine/include/XCEngine/UI/Widgets` and `engine/src/UI/Widgets` as `UIExpansionModel`, so collapsible tree/property-style widget state no longer has to stay private to `LayoutLab`.
|
||||
- Core regression coverage now includes `UIContext`, layout, style, runtime screen player/system, and real document-host tests through `core_ui_tests`.
|
||||
|
||||
Current gap:
|
||||
|
||||
- Minimal schema self-definition support is landed, including consistency checks for enum/document-only schema metadata, but schema-driven validation for `.xcui` / `.xctheme` instances is still not implemented.
|
||||
- Shared widget/runtime instantiation is still thin and mostly editor-side.
|
||||
- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, real tree/list/property widget state on top of the new editor-primitive helpers, and native image/source-rect level APIs.
|
||||
- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, multi-selection/keyboard-navigation/virtualized collection state on top of the new editor-primitive helpers, and native image/source-rect level APIs.
|
||||
|
||||
### 2. Runtime/Game Layer
|
||||
|
||||
@@ -68,6 +69,7 @@ Current gap:
|
||||
- `LayoutLab` now also covers editor-facing widget prototypes: `TreeView`, `TreeItem`, `ListView`, `ListItem`, `PropertySection`, and `FieldRow`.
|
||||
- `LayoutLab` now consumes the shared `UIEditorCollectionPrimitives` helper layer for collection-widget tag classification, clipping flags, and default metric resolution instead of keeping that taxonomy private to the sandbox runtime.
|
||||
- `LayoutLab` now also consumes the shared `UISelectionModel` for click-selection persistence across collection-style widgets, and the diagnostics panel now exposes both hovered and selected element ids.
|
||||
- `LayoutLab` now also consumes the shared `UIExpansionModel` for tree expansion and property-section collapse, with reserved property headers, disclosure glyphs, and persisted click-toggle behavior in the sandbox runtime.
|
||||
- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths.
|
||||
- The editor bridge layer now has smoke coverage for swapchain after-UI rendering hooks and SRV-backed ImGui texture descriptor registration.
|
||||
- `Application` no longer owns the ImGui backend directly; window presentation now routes through `IWindowUICompositor` with an `ImGuiWindowUICompositor` implementation, which currently delegates to `IEditorHostCompositor` / `ImGuiHostCompositor`.
|
||||
@@ -80,18 +82,19 @@ Current gap:
|
||||
|
||||
- The shell is still ImGui-hosted.
|
||||
- Legacy hosted preview still depends on an active ImGui window context for inline presentation.
|
||||
- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/tree expansion state, command routing, property editing models, toolbar/menu chrome, and icon-atlas widgets are not yet extracted into reusable XCUI modules.
|
||||
- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/keyboard-navigation state, 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`: `7/7`
|
||||
- `new_editor_xcui_layout_lab_runtime_tests`: `9/9`
|
||||
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
|
||||
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
||||
- `new_editor_xcui_hosted_preview_presenter_tests`: `12/12`
|
||||
- `new_editor_imgui_window_ui_compositor_tests`: `5/5`
|
||||
- `XCNewEditor` Debug target builds successfully
|
||||
- `core_ui_tests`: `30/30`
|
||||
- `scene_tests`: `65/65`
|
||||
- `core_ui_tests`: `38 total` (`36` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`)
|
||||
- `scene_tests`: `68/68`
|
||||
- `core_ui_style_tests`: `5/5`
|
||||
- `ui_resource_tests`: `11/11`
|
||||
- `editor_tests` targeted bridge smoke: `3/3`
|
||||
@@ -104,6 +107,7 @@ Current gap:
|
||||
- Common-core `UITextInputController` extraction now owns per-field text state, character insertion, enter-submit, and multiline keyboard editing behavior with dedicated `core_ui_tests` coverage.
|
||||
- Common-core `UIEditorCollectionPrimitives` extraction now owns the editor collection tag taxonomy and default metric resolution used by current `LayoutLab` widget prototypes, with dedicated `core_ui_tests` coverage.
|
||||
- Common-core `UISelectionModel` extraction now owns reusable single-selection state for collection-style widgets, with dedicated `core_ui_tests` coverage.
|
||||
- Common-core `UIExpansionModel` extraction now owns reusable expansion/collapse state for tree/property-style widgets, with dedicated `core_ui_tests` coverage.
|
||||
- Demo runtime text editing was extended with:
|
||||
- click-to-place caret
|
||||
- `Delete` support
|
||||
@@ -113,6 +117,7 @@ Current gap:
|
||||
- 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.
|
||||
- LayoutLab click-selection now persists through the shared `UISelectionModel`, including selected-state diagnostics and reusable visual selection feedback on cards, collection rows, and field rows.
|
||||
- LayoutLab tree expansion and property-section collapse now persist through the shared `UIExpansionModel`, including reserved property headers, disclosure glyphs, and runtime coverage for collapsed/expanded visibility.
|
||||
- Schema document support extended with:
|
||||
- retained `UISchemaDefinition` data on `UIDocumentModel`
|
||||
- artifact schema version bump for UI documents
|
||||
@@ -156,6 +161,12 @@ Current gap:
|
||||
- `IEditorHostCompositor`
|
||||
- `ImGuiHostCompositor`
|
||||
- `Application` frame/present flow routed through the compositor instead of direct `m_imguiBackend` ownership
|
||||
- The window-level XCUI compositor seam now also has a dedicated regression target around `ImGuiWindowUICompositor`, covering initialization, render-frame ordering, Win32 message forwarding, texture registration forwarding, and shutdown safety.
|
||||
- `SceneRuntime` layered XCUI routing now has dedicated regression coverage for:
|
||||
- top-interactive layer input ownership
|
||||
- blocking/modal layer suppression of lower layers
|
||||
- hidden top-layer pass-through back to visible underlying layers
|
||||
- Shared `UITextInputController` coverage now includes more caret-boundary / modifier branches; the remaining `Delete` distinction stays blocked on `KeyCode::Delete` and `KeyCode::Backspace` still sharing the same enum value.
|
||||
- Window compositor texture registration now also flows back into `Application` as XCUI-owned `UITextureRegistration` / `UITextureHandle` data instead of exposing raw `ImTextureID` there.
|
||||
- Hosted preview contracts were tightened again:
|
||||
- generic preview surface metadata stays on XCUI-owned value types
|
||||
@@ -174,7 +185,7 @@ 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.
|
||||
- Editor widget coverage is still prototype-driven inside `LayoutLab`; it has not yet been promoted into a full reusable shared widget/runtime layer with command routing, virtualization, and property-edit transactions.
|
||||
|
||||
## Next Phase
|
||||
|
||||
|
||||
@@ -521,8 +521,10 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextEditing.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextInputController.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIExpansionModel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UISelectionModel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIEditorCollectionPrimitives.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIExpansionModel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UISelectionModel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h
|
||||
|
||||
30
engine/include/XCEngine/UI/Widgets/UIExpansionModel.h
Normal file
30
engine/include/XCEngine/UI/Widgets/UIExpansionModel.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Widgets {
|
||||
|
||||
class UIExpansionModel {
|
||||
public:
|
||||
bool HasExpandedItems() const;
|
||||
std::size_t GetExpandedCount() const;
|
||||
bool IsExpanded(std::string_view id) const;
|
||||
|
||||
bool SetExpanded(std::string itemId, bool expanded);
|
||||
bool Expand(std::string itemId);
|
||||
bool Collapse(std::string itemId);
|
||||
bool ToggleExpanded(std::string itemId);
|
||||
bool Clear();
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> m_expandedIds = {};
|
||||
};
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
@@ -64,6 +64,7 @@ bool UsesUIEditorCollectionPrimitiveColumnLayout(UIEditorCollectionPrimitiveKind
|
||||
bool IsUIEditorCollectionPrimitiveHoverable(UIEditorCollectionPrimitiveKind kind) {
|
||||
return kind == UIEditorCollectionPrimitiveKind::TreeItem ||
|
||||
kind == UIEditorCollectionPrimitiveKind::ListItem ||
|
||||
kind == UIEditorCollectionPrimitiveKind::PropertySection ||
|
||||
kind == UIEditorCollectionPrimitiveKind::FieldRow;
|
||||
}
|
||||
|
||||
|
||||
57
engine/src/UI/Widgets/UIExpansionModel.cpp
Normal file
57
engine/src/UI/Widgets/UIExpansionModel.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Widgets {
|
||||
|
||||
bool UIExpansionModel::HasExpandedItems() const {
|
||||
return !m_expandedIds.empty();
|
||||
}
|
||||
|
||||
std::size_t UIExpansionModel::GetExpandedCount() const {
|
||||
return m_expandedIds.size();
|
||||
}
|
||||
|
||||
bool UIExpansionModel::IsExpanded(std::string_view id) const {
|
||||
return m_expandedIds.contains(std::string(id));
|
||||
}
|
||||
|
||||
bool UIExpansionModel::SetExpanded(std::string itemId, bool expanded) {
|
||||
return expanded
|
||||
? Expand(std::move(itemId))
|
||||
: Collapse(std::move(itemId));
|
||||
}
|
||||
|
||||
bool UIExpansionModel::Expand(std::string itemId) {
|
||||
return m_expandedIds.insert(std::move(itemId)).second;
|
||||
}
|
||||
|
||||
bool UIExpansionModel::Collapse(std::string itemId) {
|
||||
return m_expandedIds.erase(itemId) > 0u;
|
||||
}
|
||||
|
||||
bool UIExpansionModel::ToggleExpanded(std::string itemId) {
|
||||
const auto it = m_expandedIds.find(itemId);
|
||||
if (it != m_expandedIds.end()) {
|
||||
m_expandedIds.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_expandedIds.insert(std::move(itemId));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIExpansionModel::Clear() {
|
||||
if (m_expandedIds.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_expandedIds.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
@@ -14,7 +14,7 @@
|
||||
tone="accent-alt"
|
||||
title="Tool Shelf"
|
||||
subtitle="Scene, asset, and play-mode actions." />
|
||||
<ScrollView id="assetList" height="stretch" padding="10" gap="8" scrollY="132">
|
||||
<ScrollView id="assetList" height="stretch" padding="10" gap="8" scrollY="40">
|
||||
<Card
|
||||
id="assetListHeader"
|
||||
height="54"
|
||||
@@ -29,9 +29,9 @@
|
||||
<TreeItem id="treeUi" title="UI" subtitle="Runtime atlas + themes" indent="1" />
|
||||
</TreeView>
|
||||
<ListView id="recentAssetList" height="360" padding="8" gap="6">
|
||||
<ListItem id="assetLighting" title="Lighting_GlobalRig" subtitle="Prefab -Directional setup" />
|
||||
<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>
|
||||
@@ -74,7 +74,7 @@
|
||||
height="88"
|
||||
title="Inspector Summary"
|
||||
subtitle="Transform, renderer, and prefab overrides." />
|
||||
<ScrollView id="inspectorSections" height="stretch" padding="10" gap="8" scrollY="52">
|
||||
<ScrollView id="inspectorSections" height="stretch" padding="10" gap="8" scrollY="0">
|
||||
<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" />
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <XCEngine/UI/Style/Theme.h>
|
||||
#include <XCEngine/UI/Types.h>
|
||||
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -78,6 +79,7 @@ struct RuntimeBuildContext {
|
||||
std::vector<LayoutNode> nodes = {};
|
||||
std::unordered_map<std::string, std::size_t> nodeIndexById = {};
|
||||
std::unordered_map<std::string, UIRect> rectsById = {};
|
||||
UIWidgets::UIExpansionModel expansionModel = {};
|
||||
UIWidgets::UISelectionModel selectionModel = {};
|
||||
bool documentsReady = false;
|
||||
std::string statusMessage = {};
|
||||
@@ -491,16 +493,20 @@ float ResolveListItemHeight(const Style::UITheme& theme) {
|
||||
theme);
|
||||
}
|
||||
|
||||
float ResolveTreeIndent(const LayoutNode& node, const Style::UITheme& theme) {
|
||||
float ResolveTreeIndentLevel(const LayoutNode& node) {
|
||||
float indentLevel = 0.0f;
|
||||
if (!node.indentAttr.empty()) {
|
||||
TryParseFloat(node.indentAttr, indentLevel);
|
||||
}
|
||||
|
||||
return (std::max)(0.0f, indentLevel);
|
||||
}
|
||||
|
||||
float ResolveTreeIndent(const LayoutNode& node, const Style::UITheme& theme) {
|
||||
return UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(
|
||||
UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem,
|
||||
theme,
|
||||
(std::max)(0.0f, indentLevel));
|
||||
ResolveTreeIndentLevel(node));
|
||||
}
|
||||
|
||||
float ResolveFieldRowHeight(const Style::UITheme& theme) {
|
||||
@@ -519,23 +525,195 @@ float ResolveDefaultHeight(const LayoutNode& node, const Style::UITheme& theme)
|
||||
theme);
|
||||
}
|
||||
|
||||
float ResolvePropertySectionHeaderHeight(
|
||||
const RuntimeBuildContext& state,
|
||||
const LayoutNode& node) {
|
||||
const float titleFont = ResolveFloatToken(state.theme, "font.title", 16.0f);
|
||||
const float bodyFont = ResolveFloatToken(state.theme, "font.body", 13.0f);
|
||||
return titleFont + 8.0f + (node.subtitle.empty() ? 0.0f : bodyFont + 8.0f);
|
||||
}
|
||||
|
||||
bool HasTreeItemChildren(const RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
const LayoutNode& node = state.nodes[nodeIndex];
|
||||
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName) !=
|
||||
UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem ||
|
||||
node.parentIndex == kInvalidIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const LayoutNode& parent = state.nodes[node.parentIndex];
|
||||
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(parent.tagName) !=
|
||||
UIWidgets::UIEditorCollectionPrimitiveKind::TreeView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float indentLevel = ResolveTreeIndentLevel(node);
|
||||
const auto siblingIt = std::find(parent.children.begin(), parent.children.end(), nodeIndex);
|
||||
if (siblingIt == parent.children.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto it = siblingIt + 1; it != parent.children.end(); ++it) {
|
||||
const LayoutNode& sibling = state.nodes[*it];
|
||||
const float siblingIndentLevel = ResolveTreeIndentLevel(sibling);
|
||||
if (siblingIndentLevel <= indentLevel) {
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsNodeCollapsible(const RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
const LayoutNode& node = state.nodes[nodeIndex];
|
||||
const UIWidgets::UIEditorCollectionPrimitiveKind primitiveKind =
|
||||
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName);
|
||||
if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
|
||||
return !node.children.empty();
|
||||
}
|
||||
|
||||
return HasTreeItemChildren(state, nodeIndex);
|
||||
}
|
||||
|
||||
bool IsNodeExpanded(const RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
return !IsNodeCollapsible(state, nodeIndex) ||
|
||||
state.expansionModel.IsExpanded(state.nodes[nodeIndex].id);
|
||||
}
|
||||
|
||||
float ResolvePropertySectionCollapsedHeight(
|
||||
const RuntimeBuildContext& state,
|
||||
const LayoutNode& node) {
|
||||
const float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f);
|
||||
return inset * 2.0f + ResolvePropertySectionHeaderHeight(state, node);
|
||||
}
|
||||
|
||||
bool IsNodeVisible(const RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
const LayoutNode& node = state.nodes[nodeIndex];
|
||||
const UIWidgets::UIEditorCollectionPrimitiveKind primitiveKind =
|
||||
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName);
|
||||
|
||||
std::size_t ancestorIndex = node.parentIndex;
|
||||
while (ancestorIndex != kInvalidIndex) {
|
||||
const LayoutNode& ancestor = state.nodes[ancestorIndex];
|
||||
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(ancestor.tagName) ==
|
||||
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection &&
|
||||
!IsNodeExpanded(state, ancestorIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ancestorIndex = ancestor.parentIndex;
|
||||
}
|
||||
|
||||
if (primitiveKind != UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem ||
|
||||
node.parentIndex == kInvalidIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const LayoutNode& parent = state.nodes[node.parentIndex];
|
||||
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(parent.tagName) !=
|
||||
UIWidgets::UIEditorCollectionPrimitiveKind::TreeView) {
|
||||
return true;
|
||||
}
|
||||
|
||||
float requiredAncestorIndent = ResolveTreeIndentLevel(node);
|
||||
if (requiredAncestorIndent <= 0.0f) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto siblingIt = std::find(parent.children.begin(), parent.children.end(), nodeIndex);
|
||||
if (siblingIt == parent.children.end()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::size_t siblingOffset =
|
||||
static_cast<std::size_t>(siblingIt - parent.children.begin());
|
||||
for (std::size_t offset = siblingOffset; offset > 0u && requiredAncestorIndent > 0.0f; --offset) {
|
||||
const std::size_t siblingIndex = parent.children[offset - 1u];
|
||||
const LayoutNode& sibling = state.nodes[siblingIndex];
|
||||
const float siblingIndent = ResolveTreeIndentLevel(sibling);
|
||||
if (siblingIndent < requiredAncestorIndent) {
|
||||
if (!IsNodeExpanded(state, siblingIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
requiredAncestorIndent = siblingIndent;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::size_t> CollectVisibleChildren(
|
||||
const RuntimeBuildContext& state,
|
||||
std::size_t nodeIndex) {
|
||||
std::vector<std::size_t> visibleChildren = {};
|
||||
const LayoutNode& node = state.nodes[nodeIndex];
|
||||
visibleChildren.reserve(node.children.size());
|
||||
for (const std::size_t childIndex : node.children) {
|
||||
if (IsNodeVisible(state, childIndex)) {
|
||||
visibleChildren.push_back(childIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return visibleChildren;
|
||||
}
|
||||
|
||||
void SeedDefaultExpansionState(RuntimeBuildContext& state) {
|
||||
state.expansionModel.Clear();
|
||||
for (std::size_t nodeIndex = 0; nodeIndex < state.nodes.size(); ++nodeIndex) {
|
||||
const LayoutNode& node = state.nodes[nodeIndex];
|
||||
const UIWidgets::UIEditorCollectionPrimitiveKind primitiveKind =
|
||||
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName);
|
||||
if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection ||
|
||||
HasTreeItemChildren(state, nodeIndex)) {
|
||||
state.expansionModel.Expand(node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex);
|
||||
|
||||
void LayoutColumnChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
LayoutNode& node = state.nodes[nodeIndex];
|
||||
const float padding = ResolvePadding(node, state.theme);
|
||||
const float gap = ResolveGap(node, state.theme);
|
||||
const UIRect contentRect = InsetRect(node.rect, padding);
|
||||
if (node.children.empty()) {
|
||||
UIRect contentRect = InsetRect(node.rect, padding);
|
||||
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName) ==
|
||||
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
|
||||
const float headerHeight = ResolvePropertySectionHeaderHeight(state, node);
|
||||
contentRect.y += headerHeight;
|
||||
contentRect.height = (std::max)(0.0f, contentRect.height - headerHeight);
|
||||
}
|
||||
const std::vector<std::size_t> visibleChildren = CollectVisibleChildren(state, nodeIndex);
|
||||
if (visibleChildren.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float fixedHeight = 0.0f;
|
||||
std::size_t stretchCount = 0;
|
||||
std::vector<float> resolvedHeights(node.children.size(), 0.0f);
|
||||
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
|
||||
const LayoutNode& child = state.nodes[node.children[childOffset]];
|
||||
std::vector<float> resolvedHeights(visibleChildren.size(), 0.0f);
|
||||
std::vector<bool> childUsesStretchHeight(visibleChildren.size(), false);
|
||||
for (std::size_t childOffset = 0; childOffset < visibleChildren.size(); ++childOffset) {
|
||||
const LayoutNode& child = state.nodes[visibleChildren[childOffset]];
|
||||
const float defaultHeight = ResolveDefaultHeight(child, state.theme);
|
||||
if (UIWidgets::ClassifyUIEditorCollectionPrimitive(child.tagName) ==
|
||||
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection &&
|
||||
!IsNodeExpanded(state, visibleChildren[childOffset])) {
|
||||
resolvedHeights[childOffset] = ResolvePropertySectionCollapsedHeight(state, child);
|
||||
fixedHeight += resolvedHeights[childOffset];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child.heightAttr.empty() && defaultHeight > 0.0f) {
|
||||
resolvedHeights[childOffset] = defaultHeight;
|
||||
fixedHeight += resolvedHeights[childOffset];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsStretch(child.heightAttr)) {
|
||||
childUsesStretchHeight[childOffset] = true;
|
||||
++stretchCount;
|
||||
continue;
|
||||
}
|
||||
@@ -543,27 +721,28 @@ void LayoutColumnChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
resolvedHeights[childOffset] = ResolveScalar(
|
||||
child.heightAttr,
|
||||
contentRect.height,
|
||||
ResolveDefaultHeight(child, state.theme));
|
||||
defaultHeight);
|
||||
fixedHeight += resolvedHeights[childOffset];
|
||||
}
|
||||
|
||||
const float totalGap = gap * static_cast<float>((std::max<std::size_t>)(1u, node.children.size()) - 1u);
|
||||
const float totalGap =
|
||||
gap * static_cast<float>((std::max<std::size_t>)(1u, visibleChildren.size()) - 1u);
|
||||
const float remainingHeight = (std::max)(0.0f, contentRect.height - fixedHeight - totalGap);
|
||||
const float stretchHeight =
|
||||
stretchCount > 0u ? remainingHeight / static_cast<float>(stretchCount) : 0.0f;
|
||||
|
||||
float cursorY = contentRect.y;
|
||||
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
|
||||
LayoutNode& child = state.nodes[node.children[childOffset]];
|
||||
for (std::size_t childOffset = 0; childOffset < visibleChildren.size(); ++childOffset) {
|
||||
LayoutNode& child = state.nodes[visibleChildren[childOffset]];
|
||||
const float childHeight =
|
||||
!IsStretch(child.heightAttr) ? resolvedHeights[childOffset] : stretchHeight;
|
||||
childUsesStretchHeight[childOffset] ? stretchHeight : resolvedHeights[childOffset];
|
||||
const float childWidth =
|
||||
!IsStretch(child.widthAttr)
|
||||
? ResolveScalar(child.widthAttr, contentRect.width, contentRect.width)
|
||||
: contentRect.width;
|
||||
child.rect = UIRect(contentRect.x, cursorY, childWidth, childHeight);
|
||||
cursorY += childHeight + gap;
|
||||
LayoutNodeTree(state, node.children[childOffset]);
|
||||
LayoutNodeTree(state, visibleChildren[childOffset]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,15 +751,16 @@ void LayoutRowChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
const float padding = ResolvePadding(node, state.theme);
|
||||
const float gap = ResolveGap(node, state.theme);
|
||||
const UIRect contentRect = InsetRect(node.rect, padding);
|
||||
if (node.children.empty()) {
|
||||
const std::vector<std::size_t> visibleChildren = CollectVisibleChildren(state, nodeIndex);
|
||||
if (visibleChildren.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float fixedWidth = 0.0f;
|
||||
std::size_t stretchCount = 0;
|
||||
std::vector<float> resolvedWidths(node.children.size(), 0.0f);
|
||||
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
|
||||
const LayoutNode& child = state.nodes[node.children[childOffset]];
|
||||
std::vector<float> resolvedWidths(visibleChildren.size(), 0.0f);
|
||||
for (std::size_t childOffset = 0; childOffset < visibleChildren.size(); ++childOffset) {
|
||||
const LayoutNode& child = state.nodes[visibleChildren[childOffset]];
|
||||
if (IsStretch(child.widthAttr)) {
|
||||
++stretchCount;
|
||||
continue;
|
||||
@@ -590,14 +770,15 @@ void LayoutRowChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
fixedWidth += resolvedWidths[childOffset];
|
||||
}
|
||||
|
||||
const float totalGap = gap * static_cast<float>((std::max<std::size_t>)(1u, node.children.size()) - 1u);
|
||||
const float totalGap =
|
||||
gap * static_cast<float>((std::max<std::size_t>)(1u, visibleChildren.size()) - 1u);
|
||||
const float remainingWidth = (std::max)(0.0f, contentRect.width - fixedWidth - totalGap);
|
||||
const float stretchWidth =
|
||||
stretchCount > 0u ? remainingWidth / static_cast<float>(stretchCount) : 0.0f;
|
||||
|
||||
float cursorX = contentRect.x;
|
||||
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
|
||||
LayoutNode& child = state.nodes[node.children[childOffset]];
|
||||
for (std::size_t childOffset = 0; childOffset < visibleChildren.size(); ++childOffset) {
|
||||
LayoutNode& child = state.nodes[visibleChildren[childOffset]];
|
||||
const float childWidth =
|
||||
!IsStretch(child.widthAttr) ? resolvedWidths[childOffset] : stretchWidth;
|
||||
const float childHeight =
|
||||
@@ -606,7 +787,7 @@ void LayoutRowChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
: contentRect.height;
|
||||
child.rect = UIRect(cursorX, contentRect.y, childWidth, childHeight);
|
||||
cursorX += childWidth + gap;
|
||||
LayoutNodeTree(state, node.children[childOffset]);
|
||||
LayoutNodeTree(state, visibleChildren[childOffset]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,7 +795,7 @@ void LayoutOverlayChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
LayoutNode& node = state.nodes[nodeIndex];
|
||||
const UIRect contentRect = GetContentRect(node, state.theme);
|
||||
|
||||
for (std::size_t childIndex : node.children) {
|
||||
for (const std::size_t childIndex : CollectVisibleChildren(state, nodeIndex)) {
|
||||
LayoutNode& child = state.nodes[childIndex];
|
||||
const float offsetX = ResolveScalar(child.xAttr, contentRect.width, 0.0f);
|
||||
const float offsetY = ResolveScalar(child.yAttr, contentRect.height, 0.0f);
|
||||
@@ -644,17 +825,22 @@ void LayoutScrollViewChildren(RuntimeBuildContext& state, std::size_t nodeIndex)
|
||||
const UIRect contentRect = GetContentRect(node, state.theme);
|
||||
|
||||
float cursorY = contentRect.y - scrollOffset;
|
||||
for (std::size_t childIndex : node.children) {
|
||||
for (const std::size_t childIndex : CollectVisibleChildren(state, nodeIndex)) {
|
||||
LayoutNode& child = state.nodes[childIndex];
|
||||
const float defaultHeight = ResolveDefaultHeight(child, state.theme);
|
||||
const float childHeight = child.heightAttr.empty()
|
||||
? (defaultHeight > 0.0f ? defaultHeight : defaultItemHeight)
|
||||
: (!IsStretch(child.heightAttr)
|
||||
? ResolveScalar(
|
||||
child.heightAttr,
|
||||
contentRect.height,
|
||||
defaultHeight > 0.0f ? defaultHeight : defaultItemHeight)
|
||||
: defaultItemHeight);
|
||||
const float childHeight =
|
||||
UIWidgets::ClassifyUIEditorCollectionPrimitive(child.tagName) ==
|
||||
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection &&
|
||||
!IsNodeExpanded(state, childIndex)
|
||||
? ResolvePropertySectionCollapsedHeight(state, child)
|
||||
: (child.heightAttr.empty()
|
||||
? (defaultHeight > 0.0f ? defaultHeight : defaultItemHeight)
|
||||
: (!IsStretch(child.heightAttr)
|
||||
? ResolveScalar(
|
||||
child.heightAttr,
|
||||
contentRect.height,
|
||||
defaultHeight > 0.0f ? defaultHeight : defaultItemHeight)
|
||||
: defaultItemHeight));
|
||||
const float childWidth =
|
||||
!IsStretch(child.widthAttr)
|
||||
? (std::min)(
|
||||
@@ -686,6 +872,23 @@ void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
void DrawDisclosureGlyph(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const UIColor& color,
|
||||
bool expanded) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(rect.x, rect.y + rect.height * 0.5f - 1.0f, rect.width, 2.0f),
|
||||
color,
|
||||
1.0f);
|
||||
if (!expanded) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(rect.x + rect.width * 0.5f - 1.0f, rect.y, 2.0f, rect.height),
|
||||
color,
|
||||
1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawNode(
|
||||
RuntimeBuildContext& state,
|
||||
std::size_t nodeIndex,
|
||||
@@ -716,14 +919,15 @@ void DrawNode(
|
||||
drawList.AddFilledRect(node.rect, ToUIColor(surfaceColor), rounding);
|
||||
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
|
||||
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
|
||||
const bool selected = state.selectionModel.IsSelected(node.id);
|
||||
const bool expanded = IsNodeExpanded(state, nodeIndex);
|
||||
const Color sectionColor = ResolveColorToken(
|
||||
state.theme,
|
||||
"color.card",
|
||||
Color(0.12f, 0.17f, 0.23f, 1.0f));
|
||||
const Color borderColor = ResolveColorToken(
|
||||
state.theme,
|
||||
"color.border",
|
||||
Color(0.24f, 0.34f, 0.43f, 1.0f));
|
||||
const Color borderColor = selected
|
||||
? ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))
|
||||
: ResolveColorToken(state.theme, "color.border", Color(0.24f, 0.34f, 0.43f, 1.0f));
|
||||
const Color textColor = ResolveColorToken(
|
||||
state.theme,
|
||||
"color.text",
|
||||
@@ -753,6 +957,17 @@ void DrawNode(
|
||||
ToUIColor(mutedColor),
|
||||
bodyFont);
|
||||
}
|
||||
if (IsNodeCollapsible(state, nodeIndex)) {
|
||||
DrawDisclosureGlyph(
|
||||
drawList,
|
||||
UIRect(
|
||||
node.rect.x + node.rect.width - inset - 10.0f,
|
||||
node.rect.y + inset + 3.0f,
|
||||
10.0f,
|
||||
10.0f),
|
||||
ToUIColor(textColor),
|
||||
expanded);
|
||||
}
|
||||
} else if (node.tagName == "Card") {
|
||||
const bool selected = state.selectionModel.IsSelected(node.id);
|
||||
const Color cardColor = ResolveColorToken(
|
||||
@@ -813,6 +1028,8 @@ void DrawNode(
|
||||
primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::ListItem) {
|
||||
const bool hovered = node.id == hoveredId;
|
||||
const bool selected = state.selectionModel.IsSelected(node.id);
|
||||
const bool hasTreeChildren = primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem &&
|
||||
HasTreeItemChildren(state, nodeIndex);
|
||||
const Color rowColor = selected
|
||||
? ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))
|
||||
: (hovered
|
||||
@@ -840,10 +1057,18 @@ void DrawNode(
|
||||
drawList.AddFilledRect(node.rect, ToUIColor(rowColor), rounding);
|
||||
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
|
||||
if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(node.rect.x + inset + indent - 8.0f, node.rect.y + 11.0f, 4.0f, 4.0f),
|
||||
ToUIColor(textColor),
|
||||
2.0f);
|
||||
if (hasTreeChildren) {
|
||||
DrawDisclosureGlyph(
|
||||
drawList,
|
||||
UIRect(node.rect.x + inset + indent - 10.0f, node.rect.y + 10.0f, 8.0f, 8.0f),
|
||||
ToUIColor(textColor),
|
||||
IsNodeExpanded(state, nodeIndex));
|
||||
} else {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(node.rect.x + inset + indent - 8.0f, node.rect.y + 11.0f, 4.0f, 4.0f),
|
||||
ToUIColor(textColor),
|
||||
2.0f);
|
||||
}
|
||||
}
|
||||
drawList.AddText(
|
||||
UIPoint(node.rect.x + inset + indent, node.rect.y + 8.0f),
|
||||
@@ -891,7 +1116,10 @@ void DrawNode(
|
||||
if (clipsChildren) {
|
||||
drawList.PushClipRect(GetContentRect(node, state.theme));
|
||||
}
|
||||
for (std::size_t childIndex : node.children) {
|
||||
for (const std::size_t childIndex : node.children) {
|
||||
if (!IsNodeVisible(state, childIndex)) {
|
||||
continue;
|
||||
}
|
||||
DrawNode(state, childIndex, drawList, hoveredId);
|
||||
}
|
||||
if (clipsChildren) {
|
||||
@@ -928,6 +1156,7 @@ std::size_t HitTest(
|
||||
UIWidgets::IsUIEditorCollectionPrimitiveHoverable(
|
||||
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName));
|
||||
if (!hoverable ||
|
||||
!IsNodeVisible(state, index) ||
|
||||
!ContainsPoint(node.rect, point) ||
|
||||
!IsPointInsideNodeClipping(state, index, point)) {
|
||||
continue;
|
||||
@@ -967,6 +1196,7 @@ bool XCUILayoutLabRuntime::ReloadDocuments() {
|
||||
state.nodes.clear();
|
||||
state.nodeIndexById.clear();
|
||||
state.rectsById.clear();
|
||||
state.expansionModel.Clear();
|
||||
state.selectionModel.ClearSelection();
|
||||
|
||||
state.documentSource.SetPathSet(XCUIAssetDocumentSource::MakeLayoutLabPathSet());
|
||||
@@ -1001,6 +1231,7 @@ bool XCUILayoutLabRuntime::ReloadDocuments() {
|
||||
std::string(),
|
||||
0u,
|
||||
0);
|
||||
SeedDefaultExpansionState(state);
|
||||
state.documentsReady = !state.nodes.empty();
|
||||
state.statusMessage = state.documentsReady
|
||||
? (loadState.usedLegacyFallback
|
||||
@@ -1050,6 +1281,9 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
|
||||
|
||||
if (input.pointerPressed) {
|
||||
if (hoveredIndex != kInvalidIndex) {
|
||||
if (IsNodeCollapsible(state, hoveredIndex)) {
|
||||
state.expansionModel.ToggleExpanded(state.nodes[hoveredIndex].id);
|
||||
}
|
||||
state.selectionModel.SetSelection(state.nodes[hoveredIndex].id);
|
||||
} else {
|
||||
state.selectionModel.ClearSelection();
|
||||
@@ -1069,7 +1303,12 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
|
||||
state.frameResult.stats.drawListCount = state.frameResult.drawData.GetDrawListCount();
|
||||
state.frameResult.stats.commandCount = state.frameResult.drawData.GetTotalCommandCount();
|
||||
AnalyzeNativeOverlayCompatibility(state.frameResult.drawData, state.frameResult.stats);
|
||||
for (const LayoutNode& node : state.nodes) {
|
||||
for (std::size_t nodeIndex = 0; nodeIndex < state.nodes.size(); ++nodeIndex) {
|
||||
const LayoutNode& node = state.nodes[nodeIndex];
|
||||
if (!IsNodeVisible(state, nodeIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIWidgets::UIEditorCollectionPrimitiveKind primitiveKind =
|
||||
UIWidgets::ClassifyUIEditorCollectionPrimitive(node.tagName);
|
||||
if (node.tagName == "Row") {
|
||||
@@ -1084,12 +1323,18 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
|
||||
++state.frameResult.stats.treeViewCount;
|
||||
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem) {
|
||||
++state.frameResult.stats.treeItemCount;
|
||||
if (HasTreeItemChildren(state, nodeIndex) && IsNodeExpanded(state, nodeIndex)) {
|
||||
++state.frameResult.stats.expandedTreeItemCount;
|
||||
}
|
||||
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::ListView) {
|
||||
++state.frameResult.stats.listViewCount;
|
||||
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::ListItem) {
|
||||
++state.frameResult.stats.listItemCount;
|
||||
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
|
||||
++state.frameResult.stats.propertySectionCount;
|
||||
if (IsNodeExpanded(state, nodeIndex)) {
|
||||
++state.frameResult.stats.expandedPropertySectionCount;
|
||||
}
|
||||
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow) {
|
||||
++state.frameResult.stats.fieldRowCount;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ struct XCUILayoutLabFrameStats {
|
||||
std::size_t listItemCount = 0;
|
||||
std::size_t propertySectionCount = 0;
|
||||
std::size_t fieldRowCount = 0;
|
||||
std::size_t expandedTreeItemCount = 0;
|
||||
std::size_t expandedPropertySectionCount = 0;
|
||||
std::string hoveredElementId = {};
|
||||
std::string selectedElementId = {};
|
||||
};
|
||||
|
||||
@@ -317,6 +317,12 @@ void XCUILayoutLabPanel::Render() {
|
||||
stats.columnCount,
|
||||
stats.overlayCount,
|
||||
stats.scrollViewCount);
|
||||
ImGui::Text(
|
||||
"Tree items: %zu (%zu expanded) | Property sections: %zu (%zu expanded)",
|
||||
stats.treeItemCount,
|
||||
stats.expandedTreeItemCount,
|
||||
stats.propertySectionCount,
|
||||
stats.expandedPropertySectionCount);
|
||||
ImGui::Text(
|
||||
"Draw lists: %zu | Draw commands: %zu",
|
||||
stats.drawListCount,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
set(UI_TEST_SOURCES
|
||||
test_ui_core.cpp
|
||||
test_ui_editor_collection_primitives.cpp
|
||||
test_ui_expansion_model.cpp
|
||||
test_layout_engine.cpp
|
||||
test_ui_selection_model.cpp
|
||||
test_ui_runtime.cpp
|
||||
|
||||
@@ -50,6 +50,7 @@ TEST(UIEditorCollectionPrimitivesTest, ClassifyAndFlagsMatchEditorCollectionTags
|
||||
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::TreeItem));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::ListItem));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::PropertySection));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::FieldRow));
|
||||
EXPECT_FALSE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::TreeView));
|
||||
}
|
||||
|
||||
45
tests/Core/UI/test_ui_expansion_model.cpp
Normal file
45
tests/Core/UI/test_ui_expansion_model.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Widgets::UIExpansionModel;
|
||||
|
||||
TEST(UIExpansionModelTest, ExpandCollapseAndClearTrackExpandedItems) {
|
||||
UIExpansionModel expansion = {};
|
||||
|
||||
EXPECT_FALSE(expansion.HasExpandedItems());
|
||||
EXPECT_EQ(expansion.GetExpandedCount(), 0u);
|
||||
|
||||
EXPECT_TRUE(expansion.Expand("treeAssetsRoot"));
|
||||
EXPECT_TRUE(expansion.IsExpanded("treeAssetsRoot"));
|
||||
EXPECT_TRUE(expansion.HasExpandedItems());
|
||||
EXPECT_EQ(expansion.GetExpandedCount(), 1u);
|
||||
|
||||
EXPECT_FALSE(expansion.Expand("treeAssetsRoot"));
|
||||
EXPECT_TRUE(expansion.Collapse("treeAssetsRoot"));
|
||||
EXPECT_FALSE(expansion.IsExpanded("treeAssetsRoot"));
|
||||
EXPECT_EQ(expansion.GetExpandedCount(), 0u);
|
||||
EXPECT_FALSE(expansion.Collapse("treeAssetsRoot"));
|
||||
EXPECT_FALSE(expansion.Clear());
|
||||
}
|
||||
|
||||
TEST(UIExpansionModelTest, SetAndToggleExpandedReplaceStateForMatchingItem) {
|
||||
UIExpansionModel expansion = {};
|
||||
|
||||
EXPECT_TRUE(expansion.SetExpanded("inspectorTransform", true));
|
||||
EXPECT_TRUE(expansion.IsExpanded("inspectorTransform"));
|
||||
EXPECT_EQ(expansion.GetExpandedCount(), 1u);
|
||||
|
||||
EXPECT_FALSE(expansion.SetExpanded("inspectorTransform", true));
|
||||
EXPECT_TRUE(expansion.ToggleExpanded("inspectorTransform"));
|
||||
EXPECT_FALSE(expansion.IsExpanded("inspectorTransform"));
|
||||
|
||||
EXPECT_TRUE(expansion.ToggleExpanded("inspectorMesh"));
|
||||
EXPECT_TRUE(expansion.IsExpanded("inspectorMesh"));
|
||||
EXPECT_TRUE(expansion.SetExpanded("inspectorMesh", false));
|
||||
EXPECT_FALSE(expansion.IsExpanded("inspectorMesh"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -41,6 +41,50 @@ TEST(UITextInputControllerTest, BackspaceAndArrowKeysUseUtf8Boundaries) {
|
||||
EXPECT_EQ(state.caret, 1u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, DeleteUsesUtf8BoundariesAndLeavesCaretAtDeletePoint) {
|
||||
if (static_cast<std::int32_t>(KeyCode::Delete) ==
|
||||
static_cast<std::int32_t>(KeyCode::Backspace)) {
|
||||
GTEST_SKIP() << "KeyCode::Delete currently aliases Backspace.";
|
||||
}
|
||||
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = std::string("A") + "\xE4\xBD\xA0" + "B";
|
||||
state.caret = 1u;
|
||||
|
||||
const auto result = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Delete),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(result.handled);
|
||||
EXPECT_TRUE(result.valueChanged);
|
||||
EXPECT_FALSE(result.submitRequested);
|
||||
EXPECT_EQ(state.value, "AB");
|
||||
EXPECT_EQ(state.caret, 1u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, DeleteClampsOversizedCaretAndDoesNotMutateAtDocumentEnd) {
|
||||
if (static_cast<std::int32_t>(KeyCode::Delete) ==
|
||||
static_cast<std::int32_t>(KeyCode::Backspace)) {
|
||||
GTEST_SKIP() << "KeyCode::Delete currently aliases Backspace.";
|
||||
}
|
||||
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "AB";
|
||||
state.caret = 99u;
|
||||
|
||||
const auto result = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Delete),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(result.handled);
|
||||
EXPECT_FALSE(result.valueChanged);
|
||||
EXPECT_FALSE(result.submitRequested);
|
||||
EXPECT_EQ(state.value, "AB");
|
||||
EXPECT_EQ(state.caret, state.value.size());
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, SingleLineEnterRequestsSubmitWithoutMutatingValue) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "prompt";
|
||||
@@ -82,6 +126,64 @@ TEST(UITextInputControllerTest, MultilineEnterAndVerticalMovementStayInControlle
|
||||
EXPECT_EQ(state.value, std::string("A") + "\xE4\xBD\xA0" + "Z\nBC\n");
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, HomeAndEndRespectSingleLineAndMultilineBounds) {
|
||||
UIText::UITextInputState singleLine = {};
|
||||
singleLine.value = "prompt";
|
||||
singleLine.caret = 2u;
|
||||
|
||||
const auto singleHome = UIText::HandleKeyDown(
|
||||
singleLine,
|
||||
static_cast<std::int32_t>(KeyCode::Home),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(singleHome.handled);
|
||||
EXPECT_EQ(singleLine.caret, 0u);
|
||||
|
||||
const auto singleEnd = UIText::HandleKeyDown(
|
||||
singleLine,
|
||||
static_cast<std::int32_t>(KeyCode::End),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(singleEnd.handled);
|
||||
EXPECT_EQ(singleLine.caret, singleLine.value.size());
|
||||
|
||||
UIText::UITextInputState multiline = {};
|
||||
multiline.value = "root\nleaf\nend";
|
||||
multiline.caret = 7u;
|
||||
const UIText::UITextInputOptions options = { true, 4u };
|
||||
|
||||
const auto multilineHome = UIText::HandleKeyDown(
|
||||
multiline,
|
||||
static_cast<std::int32_t>(KeyCode::Home),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(multilineHome.handled);
|
||||
EXPECT_EQ(multiline.caret, 5u);
|
||||
|
||||
multiline.caret = 7u;
|
||||
const auto multilineEnd = UIText::HandleKeyDown(
|
||||
multiline,
|
||||
static_cast<std::int32_t>(KeyCode::End),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(multilineEnd.handled);
|
||||
EXPECT_EQ(multiline.caret, 9u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, ClampCaretAndInsertCharacterRecoverFromOversizedCaret) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "go";
|
||||
state.caret = 42u;
|
||||
|
||||
UIText::ClampCaret(state);
|
||||
EXPECT_EQ(state.caret, state.value.size());
|
||||
|
||||
state.caret = 42u;
|
||||
EXPECT_TRUE(UIText::InsertCharacter(state, '!'));
|
||||
EXPECT_EQ(state.value, "go!");
|
||||
EXPECT_EQ(state.caret, state.value.size());
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, MultilineTabAndShiftTabIndentAndOutdentCurrentLine) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "root\nnode";
|
||||
@@ -112,4 +214,74 @@ TEST(UITextInputControllerTest, MultilineTabAndShiftTabIndentAndOutdentCurrentLi
|
||||
EXPECT_EQ(state.caret, 5u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, ShiftTabWithoutLeadingSpacesIsHandledWithoutMutatingText) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "root\nnode";
|
||||
state.caret = 5u;
|
||||
|
||||
XCEngine::UI::UIInputModifiers modifiers = {};
|
||||
modifiers.shift = true;
|
||||
|
||||
const auto result = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
modifiers,
|
||||
{ true, 4u });
|
||||
EXPECT_TRUE(result.handled);
|
||||
EXPECT_FALSE(result.valueChanged);
|
||||
EXPECT_FALSE(result.submitRequested);
|
||||
EXPECT_EQ(state.value, "root\nnode");
|
||||
EXPECT_EQ(state.caret, 5u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, MultilineTabIgnoresSystemModifiers) {
|
||||
const auto buildState = []() {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "root\nnode";
|
||||
state.caret = 5u;
|
||||
return state;
|
||||
};
|
||||
|
||||
const UIText::UITextInputOptions options = { true, 4u };
|
||||
|
||||
XCEngine::UI::UIInputModifiers control = {};
|
||||
control.control = true;
|
||||
auto controlState = buildState();
|
||||
const auto controlResult = UIText::HandleKeyDown(
|
||||
controlState,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
control,
|
||||
options);
|
||||
EXPECT_FALSE(controlResult.handled);
|
||||
EXPECT_FALSE(controlResult.valueChanged);
|
||||
EXPECT_EQ(controlState.value, "root\nnode");
|
||||
EXPECT_EQ(controlState.caret, 5u);
|
||||
|
||||
XCEngine::UI::UIInputModifiers alt = {};
|
||||
alt.alt = true;
|
||||
auto altState = buildState();
|
||||
const auto altResult = UIText::HandleKeyDown(
|
||||
altState,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
alt,
|
||||
options);
|
||||
EXPECT_FALSE(altResult.handled);
|
||||
EXPECT_FALSE(altResult.valueChanged);
|
||||
EXPECT_EQ(altState.value, "root\nnode");
|
||||
EXPECT_EQ(altState.caret, 5u);
|
||||
|
||||
XCEngine::UI::UIInputModifiers superModifier = {};
|
||||
superModifier.super = true;
|
||||
auto superState = buildState();
|
||||
const auto superResult = UIText::HandleKeyDown(
|
||||
superState,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
superModifier,
|
||||
options);
|
||||
EXPECT_FALSE(superResult.handled);
|
||||
EXPECT_FALSE(superResult.valueChanged);
|
||||
EXPECT_EQ(superState.value, "root\nnode");
|
||||
EXPECT_EQ(superState.caret, 5u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -100,6 +100,12 @@ set(NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE
|
||||
set(NEW_EDITOR_HOSTED_PREVIEW_PRESENTER_HEADER
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h
|
||||
)
|
||||
set(NEW_EDITOR_WINDOW_UI_COMPOSITOR_HEADER
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/IWindowUICompositor.h
|
||||
)
|
||||
set(NEW_EDITOR_IMGUI_WINDOW_UI_COMPOSITOR_HEADER
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiWindowUICompositor.h
|
||||
)
|
||||
set(NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h
|
||||
)
|
||||
@@ -265,6 +271,37 @@ else()
|
||||
message(STATUS "Skipping new_editor_xcui_hosted_preview_presenter_tests because presenter header or ImGui sources are missing.")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NEW_EDITOR_WINDOW_UI_COMPOSITOR_HEADER}" AND
|
||||
EXISTS "${NEW_EDITOR_IMGUI_WINDOW_UI_COMPOSITOR_HEADER}" AND
|
||||
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_imgui_window_ui_compositor.cpp")
|
||||
add_executable(new_editor_imgui_window_ui_compositor_tests
|
||||
test_imgui_window_ui_compositor.cpp
|
||||
)
|
||||
|
||||
xcengine_configure_new_editor_test_target(new_editor_imgui_window_ui_compositor_tests)
|
||||
|
||||
target_link_libraries(new_editor_imgui_window_ui_compositor_tests
|
||||
PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
user32
|
||||
comdlg32
|
||||
)
|
||||
|
||||
target_include_directories(new_editor_imgui_window_ui_compositor_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src
|
||||
${CMAKE_SOURCE_DIR}/editor/src
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
|
||||
)
|
||||
|
||||
xcengine_discover_new_editor_gtests(new_editor_imgui_window_ui_compositor_tests)
|
||||
else()
|
||||
message(STATUS "Skipping new_editor_imgui_window_ui_compositor_tests because compositor headers or the test source are missing.")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER}")
|
||||
add_executable(new_editor_native_backdrop_renderer_api_tests
|
||||
test_main_window_native_backdrop_renderer_api.cpp
|
||||
|
||||
283
tests/NewEditor/test_imgui_window_ui_compositor.cpp
Normal file
283
tests/NewEditor/test_imgui_window_ui_compositor.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "XCUIBackend/IEditorHostCompositor.h"
|
||||
#include "XCUIBackend/ImGuiWindowUICompositor.h"
|
||||
#include "XCUIBackend/UITextureRegistration.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Editor::Platform::D3D12WindowRenderer;
|
||||
using XCEngine::Editor::XCUIBackend::IEditorHostCompositor;
|
||||
using XCEngine::Editor::XCUIBackend::ImGuiWindowUICompositor;
|
||||
using XCEngine::Editor::XCUIBackend::UITextureRegistration;
|
||||
|
||||
class RecordingHostCompositor final : public IEditorHostCompositor {
|
||||
public:
|
||||
bool initializeResult = true;
|
||||
bool handleWindowMessageResult = false;
|
||||
bool createTextureDescriptorResult = false;
|
||||
bool invokeConfigureFonts = false;
|
||||
|
||||
int initializeCount = 0;
|
||||
int shutdownCount = 0;
|
||||
int beginFrameCount = 0;
|
||||
int endFrameCount = 0;
|
||||
int handleWindowMessageCount = 0;
|
||||
int createTextureDescriptorCount = 0;
|
||||
int freeTextureDescriptorCount = 0;
|
||||
|
||||
HWND lastHwnd = nullptr;
|
||||
UINT lastMessage = 0u;
|
||||
WPARAM lastWParam = 0u;
|
||||
LPARAM lastLParam = 0u;
|
||||
|
||||
D3D12WindowRenderer* initializedRenderer = nullptr;
|
||||
D3D12WindowRenderer* presentedRenderer = nullptr;
|
||||
::XCEngine::RHI::RHIDevice* lastDevice = nullptr;
|
||||
::XCEngine::RHI::RHITexture* lastTexture = nullptr;
|
||||
|
||||
bool beforeUiRenderProvided = false;
|
||||
bool afterUiRenderProvided = false;
|
||||
|
||||
std::array<float, 4> lastClearColor = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
UITextureRegistration nextRegistration = {};
|
||||
UITextureRegistration freedRegistration = {};
|
||||
std::vector<std::string> callOrder = {};
|
||||
|
||||
bool Initialize(
|
||||
HWND hwnd,
|
||||
D3D12WindowRenderer& windowRenderer,
|
||||
const ConfigureFontsCallback& configureFonts) override {
|
||||
++initializeCount;
|
||||
lastHwnd = hwnd;
|
||||
initializedRenderer = &windowRenderer;
|
||||
callOrder.push_back("initialize");
|
||||
if (invokeConfigureFonts && configureFonts) {
|
||||
configureFonts();
|
||||
}
|
||||
return initializeResult;
|
||||
}
|
||||
|
||||
void Shutdown() override {
|
||||
++shutdownCount;
|
||||
callOrder.push_back("shutdown");
|
||||
}
|
||||
|
||||
bool HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) override {
|
||||
++handleWindowMessageCount;
|
||||
lastHwnd = hwnd;
|
||||
lastMessage = message;
|
||||
lastWParam = wParam;
|
||||
lastLParam = lParam;
|
||||
callOrder.push_back("message");
|
||||
return handleWindowMessageResult;
|
||||
}
|
||||
|
||||
void BeginFrame() override {
|
||||
++beginFrameCount;
|
||||
callOrder.push_back("begin");
|
||||
}
|
||||
|
||||
void EndFrameAndPresent(
|
||||
D3D12WindowRenderer& windowRenderer,
|
||||
const float clearColor[4],
|
||||
const RenderCallback& beforeUiRender,
|
||||
const RenderCallback& afterUiRender) override {
|
||||
++endFrameCount;
|
||||
presentedRenderer = &windowRenderer;
|
||||
beforeUiRenderProvided = static_cast<bool>(beforeUiRender);
|
||||
afterUiRenderProvided = static_cast<bool>(afterUiRender);
|
||||
for (std::size_t index = 0; index < lastClearColor.size(); ++index) {
|
||||
lastClearColor[index] = clearColor[index];
|
||||
}
|
||||
callOrder.push_back("present");
|
||||
}
|
||||
|
||||
bool CreateTextureDescriptor(
|
||||
::XCEngine::RHI::RHIDevice* device,
|
||||
::XCEngine::RHI::RHITexture* texture,
|
||||
UITextureRegistration& outRegistration) override {
|
||||
++createTextureDescriptorCount;
|
||||
lastDevice = device;
|
||||
lastTexture = texture;
|
||||
if (createTextureDescriptorResult) {
|
||||
outRegistration = nextRegistration;
|
||||
}
|
||||
return createTextureDescriptorResult;
|
||||
}
|
||||
|
||||
void FreeTextureDescriptor(const UITextureRegistration& registration) override {
|
||||
++freeTextureDescriptorCount;
|
||||
freedRegistration = registration;
|
||||
}
|
||||
};
|
||||
|
||||
HWND MakeFakeHwnd() {
|
||||
return reinterpret_cast<HWND>(static_cast<std::uintptr_t>(0x1234u));
|
||||
}
|
||||
|
||||
UITextureRegistration MakeRegistration() {
|
||||
UITextureRegistration registration = {};
|
||||
registration.cpuHandle.ptr = 11u;
|
||||
registration.gpuHandle.ptr = 29u;
|
||||
registration.texture.nativeHandle = 43u;
|
||||
registration.texture.width = 256u;
|
||||
registration.texture.height = 128u;
|
||||
registration.texture.kind = ::XCEngine::UI::UITextureHandleKind::ShaderResourceView;
|
||||
return registration;
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, InitializeForwardsToHostAndConfigureFontsCallback) {
|
||||
auto host = std::make_unique<RecordingHostCompositor>();
|
||||
RecordingHostCompositor* hostPtr = host.get();
|
||||
hostPtr->invokeConfigureFonts = true;
|
||||
|
||||
ImGuiWindowUICompositor compositor(std::move(host));
|
||||
D3D12WindowRenderer renderer = {};
|
||||
bool configureFontsCalled = false;
|
||||
|
||||
EXPECT_TRUE(compositor.Initialize(
|
||||
MakeFakeHwnd(),
|
||||
renderer,
|
||||
[&configureFontsCalled]() { configureFontsCalled = true; }));
|
||||
EXPECT_TRUE(configureFontsCalled);
|
||||
ASSERT_NE(hostPtr, nullptr);
|
||||
EXPECT_EQ(hostPtr->initializeCount, 1);
|
||||
EXPECT_EQ(hostPtr->lastHwnd, MakeFakeHwnd());
|
||||
EXPECT_EQ(hostPtr->initializedRenderer, &renderer);
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, RenderFrameCallsHostBeginUiAndPresentInOrder) {
|
||||
auto host = std::make_unique<RecordingHostCompositor>();
|
||||
RecordingHostCompositor* hostPtr = host.get();
|
||||
|
||||
ImGuiWindowUICompositor compositor(std::move(host));
|
||||
D3D12WindowRenderer renderer = {};
|
||||
ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), renderer, {}));
|
||||
hostPtr->callOrder.clear();
|
||||
|
||||
bool uiRendered = false;
|
||||
constexpr float clearColor[4] = { 0.1f, 0.2f, 0.3f, 0.4f };
|
||||
compositor.RenderFrame(
|
||||
clearColor,
|
||||
[&]() {
|
||||
uiRendered = true;
|
||||
hostPtr->callOrder.push_back("ui");
|
||||
},
|
||||
[](const ::XCEngine::Rendering::RenderContext&, const ::XCEngine::Rendering::RenderSurface&) {},
|
||||
[](const ::XCEngine::Rendering::RenderContext&, const ::XCEngine::Rendering::RenderSurface&) {});
|
||||
|
||||
EXPECT_TRUE(uiRendered);
|
||||
EXPECT_EQ(hostPtr->beginFrameCount, 1);
|
||||
EXPECT_EQ(hostPtr->endFrameCount, 1);
|
||||
EXPECT_EQ(hostPtr->presentedRenderer, &renderer);
|
||||
EXPECT_TRUE(hostPtr->beforeUiRenderProvided);
|
||||
EXPECT_TRUE(hostPtr->afterUiRenderProvided);
|
||||
EXPECT_EQ(hostPtr->lastClearColor[0], clearColor[0]);
|
||||
EXPECT_EQ(hostPtr->lastClearColor[1], clearColor[1]);
|
||||
EXPECT_EQ(hostPtr->lastClearColor[2], clearColor[2]);
|
||||
EXPECT_EQ(hostPtr->lastClearColor[3], clearColor[3]);
|
||||
EXPECT_EQ(
|
||||
hostPtr->callOrder,
|
||||
(std::vector<std::string>{ "begin", "ui", "present" }));
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, HandleWindowMessageAndTextureRegistrationForwardToHost) {
|
||||
auto host = std::make_unique<RecordingHostCompositor>();
|
||||
RecordingHostCompositor* hostPtr = host.get();
|
||||
hostPtr->handleWindowMessageResult = true;
|
||||
hostPtr->createTextureDescriptorResult = true;
|
||||
hostPtr->nextRegistration = MakeRegistration();
|
||||
|
||||
ImGuiWindowUICompositor compositor(std::move(host));
|
||||
|
||||
EXPECT_TRUE(compositor.HandleWindowMessage(MakeFakeHwnd(), WM_SIZE, 7u, 19u));
|
||||
EXPECT_EQ(hostPtr->handleWindowMessageCount, 1);
|
||||
EXPECT_EQ(hostPtr->lastMessage, static_cast<UINT>(WM_SIZE));
|
||||
EXPECT_EQ(hostPtr->lastWParam, static_cast<WPARAM>(7u));
|
||||
EXPECT_EQ(hostPtr->lastLParam, static_cast<LPARAM>(19u));
|
||||
|
||||
UITextureRegistration registration = {};
|
||||
auto* fakeDevice = reinterpret_cast<::XCEngine::RHI::RHIDevice*>(static_cast<std::uintptr_t>(0x41u));
|
||||
auto* fakeTexture = reinterpret_cast<::XCEngine::RHI::RHITexture*>(static_cast<std::uintptr_t>(0x59u));
|
||||
EXPECT_TRUE(compositor.CreateTextureDescriptor(fakeDevice, fakeTexture, registration));
|
||||
EXPECT_EQ(hostPtr->createTextureDescriptorCount, 1);
|
||||
EXPECT_EQ(hostPtr->lastDevice, fakeDevice);
|
||||
EXPECT_EQ(hostPtr->lastTexture, fakeTexture);
|
||||
EXPECT_EQ(registration.cpuHandle.ptr, hostPtr->nextRegistration.cpuHandle.ptr);
|
||||
EXPECT_EQ(registration.gpuHandle.ptr, hostPtr->nextRegistration.gpuHandle.ptr);
|
||||
EXPECT_EQ(registration.texture.nativeHandle, hostPtr->nextRegistration.texture.nativeHandle);
|
||||
|
||||
compositor.FreeTextureDescriptor(registration);
|
||||
EXPECT_EQ(hostPtr->freeTextureDescriptorCount, 1);
|
||||
EXPECT_EQ(hostPtr->freedRegistration.cpuHandle.ptr, registration.cpuHandle.ptr);
|
||||
EXPECT_EQ(hostPtr->freedRegistration.gpuHandle.ptr, registration.gpuHandle.ptr);
|
||||
EXPECT_EQ(hostPtr->freedRegistration.texture.nativeHandle, registration.texture.nativeHandle);
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, ShutdownClearsRendererBindingAndPreventsFurtherRender) {
|
||||
auto host = std::make_unique<RecordingHostCompositor>();
|
||||
RecordingHostCompositor* hostPtr = host.get();
|
||||
|
||||
ImGuiWindowUICompositor compositor(std::move(host));
|
||||
D3D12WindowRenderer renderer = {};
|
||||
ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), renderer, {}));
|
||||
|
||||
bool firstUiRendered = false;
|
||||
compositor.RenderFrame(
|
||||
std::array<float, 4>{ 0.0f, 0.0f, 0.0f, 1.0f }.data(),
|
||||
[&]() { firstUiRendered = true; },
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(firstUiRendered);
|
||||
EXPECT_EQ(hostPtr->beginFrameCount, 1);
|
||||
EXPECT_EQ(hostPtr->endFrameCount, 1);
|
||||
|
||||
compositor.Shutdown();
|
||||
EXPECT_EQ(hostPtr->shutdownCount, 1);
|
||||
|
||||
bool secondUiRendered = false;
|
||||
compositor.RenderFrame(
|
||||
std::array<float, 4>{ 1.0f, 0.0f, 0.0f, 1.0f }.data(),
|
||||
[&]() { secondUiRendered = true; },
|
||||
{},
|
||||
{});
|
||||
EXPECT_FALSE(secondUiRendered);
|
||||
EXPECT_EQ(hostPtr->beginFrameCount, 1);
|
||||
EXPECT_EQ(hostPtr->endFrameCount, 1);
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, NullHostCompositorReturnsSafeDefaults) {
|
||||
ImGuiWindowUICompositor compositor(std::unique_ptr<IEditorHostCompositor>{});
|
||||
D3D12WindowRenderer renderer = {};
|
||||
|
||||
bool configureFontsCalled = false;
|
||||
EXPECT_FALSE(compositor.Initialize(
|
||||
MakeFakeHwnd(),
|
||||
renderer,
|
||||
[&configureFontsCalled]() { configureFontsCalled = true; }));
|
||||
EXPECT_FALSE(configureFontsCalled);
|
||||
EXPECT_FALSE(compositor.HandleWindowMessage(MakeFakeHwnd(), WM_CLOSE, 0u, 0u));
|
||||
|
||||
UITextureRegistration registration = {};
|
||||
EXPECT_FALSE(compositor.CreateTextureDescriptor(nullptr, nullptr, registration));
|
||||
|
||||
bool uiRendered = false;
|
||||
compositor.RenderFrame(
|
||||
std::array<float, 4>{ 0.0f, 0.0f, 0.0f, 1.0f }.data(),
|
||||
[&]() { uiRendered = true; },
|
||||
{},
|
||||
{});
|
||||
EXPECT_FALSE(uiRendered);
|
||||
|
||||
compositor.Shutdown();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -250,3 +250,116 @@ TEST(NewEditorXCUILayoutLabRuntimeTest, ClickSelectionPersistsOnSharedCollection
|
||||
ASSERT_TRUE(persistedFrame.stats.documentsReady);
|
||||
EXPECT_EQ(persistedFrame.stats.selectedElementId, selectedElementId);
|
||||
}
|
||||
|
||||
TEST(NewEditorXCUILayoutLabRuntimeTest, ClickingTreeRootTogglesIndentedChildrenVisibility) {
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
|
||||
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||
|
||||
const auto& baseline = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(baseline.stats.documentsReady);
|
||||
EXPECT_EQ(baseline.stats.expandedTreeItemCount, 1u);
|
||||
|
||||
XCEngine::UI::UIRect treeRootRect = {};
|
||||
XCEngine::UI::UIRect treeChildRect = {};
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("treeAssetsRoot", treeRootRect));
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("treeScenes", treeChildRect));
|
||||
ASSERT_GT(treeRootRect.width, 0.0f);
|
||||
ASSERT_GT(treeRootRect.height, 0.0f);
|
||||
|
||||
const XCEngine::UI::UIPoint rootClickPoint(
|
||||
treeRootRect.x + 18.0f,
|
||||
treeRootRect.y + treeRootRect.height * 0.5f);
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState hoverInput = BuildInputState();
|
||||
hoverInput.pointerPosition = rootClickPoint;
|
||||
const auto& hoveredFrame = runtime.Update(hoverInput);
|
||||
ASSERT_TRUE(hoveredFrame.stats.documentsReady);
|
||||
ASSERT_EQ(hoveredFrame.stats.hoveredElementId, "treeAssetsRoot")
|
||||
<< "treeRootRect=("
|
||||
<< treeRootRect.x << ", "
|
||||
<< treeRootRect.y << ", "
|
||||
<< treeRootRect.width << ", "
|
||||
<< treeRootRect.height << ")";
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState collapseInput = BuildInputState();
|
||||
collapseInput.pointerPosition = rootClickPoint;
|
||||
collapseInput.pointerPressed = true;
|
||||
const auto& collapsedFrame = runtime.Update(collapseInput);
|
||||
|
||||
ASSERT_TRUE(collapsedFrame.stats.documentsReady);
|
||||
EXPECT_EQ(collapsedFrame.stats.selectedElementId, "treeAssetsRoot");
|
||||
const auto& collapsedPersistedFrame = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(collapsedPersistedFrame.stats.documentsReady);
|
||||
EXPECT_EQ(collapsedPersistedFrame.stats.selectedElementId, "treeAssetsRoot");
|
||||
EXPECT_EQ(collapsedPersistedFrame.stats.expandedTreeItemCount, 0u);
|
||||
EXPECT_FALSE(runtime.TryGetElementRect("treeScenes", treeChildRect));
|
||||
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("treeAssetsRoot", treeRootRect));
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState expandInput = BuildInputState();
|
||||
expandInput.pointerPosition = XCEngine::UI::UIPoint(
|
||||
treeRootRect.x + 18.0f,
|
||||
treeRootRect.y + treeRootRect.height * 0.5f);
|
||||
expandInput.pointerPressed = true;
|
||||
const auto& expandedClickFrame = runtime.Update(expandInput);
|
||||
|
||||
ASSERT_TRUE(expandedClickFrame.stats.documentsReady);
|
||||
const auto& expandedFrame = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(expandedFrame.stats.documentsReady);
|
||||
EXPECT_EQ(expandedFrame.stats.expandedTreeItemCount, 1u);
|
||||
EXPECT_TRUE(runtime.TryGetElementRect("treeScenes", treeChildRect));
|
||||
EXPECT_GT(treeChildRect.height, 0.0f);
|
||||
}
|
||||
|
||||
TEST(NewEditorXCUILayoutLabRuntimeTest, ClickingPropertySectionHeaderTogglesFieldVisibility) {
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
|
||||
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||
|
||||
const auto& baseline = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(baseline.stats.documentsReady);
|
||||
|
||||
XCEngine::UI::UIRect sectionRect = {};
|
||||
XCEngine::UI::UIRect fieldRect = {};
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("inspectorTransform", sectionRect));
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("fieldPosition", fieldRect));
|
||||
const float expandedHeight = sectionRect.height;
|
||||
const XCEngine::UI::UIPoint sectionHeaderPoint(
|
||||
sectionRect.x + 18.0f,
|
||||
sectionRect.y + 10.0f);
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState hoverInput = BuildInputState();
|
||||
hoverInput.pointerPosition = sectionHeaderPoint;
|
||||
const auto& hoveredFrame = runtime.Update(hoverInput);
|
||||
ASSERT_TRUE(hoveredFrame.stats.documentsReady);
|
||||
ASSERT_EQ(hoveredFrame.stats.hoveredElementId, "inspectorTransform")
|
||||
<< "sectionRect=("
|
||||
<< sectionRect.x << ", "
|
||||
<< sectionRect.y << ", "
|
||||
<< sectionRect.width << ", "
|
||||
<< sectionRect.height << "), expandedPropertySectionCount="
|
||||
<< baseline.stats.expandedPropertySectionCount;
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState collapseInput = BuildInputState();
|
||||
collapseInput.pointerPosition = sectionHeaderPoint;
|
||||
collapseInput.pointerPressed = true;
|
||||
const auto& collapsedFrame = runtime.Update(collapseInput);
|
||||
|
||||
ASSERT_TRUE(collapsedFrame.stats.documentsReady);
|
||||
const auto& collapsedPersistedFrame = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(collapsedPersistedFrame.stats.documentsReady);
|
||||
EXPECT_EQ(collapsedPersistedFrame.stats.selectedElementId, "inspectorTransform");
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("inspectorTransform", sectionRect));
|
||||
EXPECT_LT(sectionRect.height, expandedHeight);
|
||||
EXPECT_FALSE(runtime.TryGetElementRect("fieldPosition", fieldRect));
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState expandInput = BuildInputState();
|
||||
expandInput.pointerPosition = XCEngine::UI::UIPoint(
|
||||
sectionRect.x + 18.0f,
|
||||
sectionRect.y + 10.0f);
|
||||
expandInput.pointerPressed = true;
|
||||
const auto& expandedClickFrame = runtime.Update(expandInput);
|
||||
|
||||
ASSERT_TRUE(expandedClickFrame.stats.documentsReady);
|
||||
const auto& expandedFrame = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(expandedFrame.stats.documentsReady);
|
||||
EXPECT_TRUE(runtime.TryGetElementRect("fieldPosition", fieldRect));
|
||||
EXPECT_GT(fieldRect.height, 0.0f);
|
||||
}
|
||||
|
||||
@@ -131,6 +131,18 @@ bool DrawDataContainsText(
|
||||
return false;
|
||||
}
|
||||
|
||||
const XCEngine::UI::Runtime::UISystemPresentedLayer* FindPresentedLayerById(
|
||||
const XCEngine::UI::Runtime::UISystemFrameResult& frame,
|
||||
XCEngine::UI::Runtime::UIScreenLayerId layerId) {
|
||||
for (const XCEngine::UI::Runtime::UISystemPresentedLayer& layer : frame.layers) {
|
||||
if (layer.layerId == layerId) {
|
||||
return &layer;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
class RecordingScriptRuntime : public IScriptRuntime {
|
||||
public:
|
||||
explicit RecordingScriptRuntime(std::vector<std::string>* events)
|
||||
@@ -430,4 +442,146 @@ TEST_F(SceneRuntimeTest, StopClearsUiRuntimeState) {
|
||||
EXPECT_TRUE(runtime.GetLastUIFrame().layers.empty());
|
||||
}
|
||||
|
||||
TEST_F(SceneRuntimeTest, LayeredSceneUiRoutesInputOnlyToTopInteractivePresentedLayer) {
|
||||
Scene* runtimeScene = CreateScene("RuntimeScene");
|
||||
runtime.Start(runtimeScene);
|
||||
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
|
||||
runtime.SetUIFocused(true);
|
||||
|
||||
TempFileScope gameplayView("xcui_scene_runtime_gameplay", ".xcui", BuildViewMarkup("Gameplay Layer"));
|
||||
TempFileScope overlayView("xcui_scene_runtime_overlay", ".xcui", BuildViewMarkup("Overlay Layer"));
|
||||
|
||||
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
|
||||
gameplayOptions.debugName = "gameplay";
|
||||
gameplayOptions.acceptsInput = true;
|
||||
gameplayOptions.blocksLayersBelow = false;
|
||||
|
||||
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
|
||||
overlayOptions.debugName = "overlay";
|
||||
overlayOptions.acceptsInput = true;
|
||||
overlayOptions.blocksLayersBelow = false;
|
||||
|
||||
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
||||
BuildScreenAsset(gameplayView.Path(), "runtime.gameplay"),
|
||||
gameplayOptions);
|
||||
const auto overlayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
||||
BuildScreenAsset(overlayView.Path(), "runtime.overlay"),
|
||||
overlayOptions);
|
||||
ASSERT_NE(gameplayLayerId, 0u);
|
||||
ASSERT_NE(overlayLayerId, 0u);
|
||||
|
||||
XCEngine::UI::UIInputEvent textEvent = {};
|
||||
textEvent.type = XCEngine::UI::UIInputEventType::Character;
|
||||
textEvent.character = 'I';
|
||||
runtime.QueueUIInputEvent(textEvent);
|
||||
|
||||
runtime.Update(0.016f);
|
||||
|
||||
const auto& frame = runtime.GetLastUIFrame();
|
||||
ASSERT_EQ(frame.presentedLayerCount, 2u);
|
||||
ASSERT_EQ(frame.skippedLayerCount, 0u);
|
||||
ASSERT_EQ(frame.layers.size(), 2u);
|
||||
|
||||
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
|
||||
const auto* overlayLayer = FindPresentedLayerById(frame, overlayLayerId);
|
||||
ASSERT_NE(gameplayLayer, nullptr);
|
||||
ASSERT_NE(overlayLayer, nullptr);
|
||||
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 0u);
|
||||
EXPECT_EQ(overlayLayer->stats.inputEventCount, 1u);
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Gameplay Layer"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Overlay Layer"));
|
||||
}
|
||||
|
||||
TEST_F(SceneRuntimeTest, BlockingLayerSkipsLowerLayersAndOwnsQueuedInput) {
|
||||
Scene* runtimeScene = CreateScene("RuntimeScene");
|
||||
runtime.Start(runtimeScene);
|
||||
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
|
||||
runtime.SetUIFocused(true);
|
||||
|
||||
TempFileScope gameplayView("xcui_scene_runtime_blocked_gameplay", ".xcui", BuildViewMarkup("Blocked Gameplay"));
|
||||
TempFileScope modalView("xcui_scene_runtime_modal", ".xcui", BuildViewMarkup("Pause Modal"));
|
||||
|
||||
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
|
||||
gameplayOptions.debugName = "gameplay";
|
||||
gameplayOptions.acceptsInput = true;
|
||||
gameplayOptions.blocksLayersBelow = false;
|
||||
|
||||
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
||||
BuildScreenAsset(gameplayView.Path(), "runtime.blocked.gameplay"),
|
||||
gameplayOptions);
|
||||
const auto modalLayerId = runtime.GetUIScreenStackController().PushModal(
|
||||
BuildScreenAsset(modalView.Path(), "runtime.pause.modal"),
|
||||
"pause-modal");
|
||||
ASSERT_NE(gameplayLayerId, 0u);
|
||||
ASSERT_NE(modalLayerId, 0u);
|
||||
|
||||
XCEngine::UI::UIInputEvent textEvent = {};
|
||||
textEvent.type = XCEngine::UI::UIInputEventType::Character;
|
||||
textEvent.character = 'P';
|
||||
runtime.QueueUIInputEvent(textEvent);
|
||||
|
||||
runtime.Update(0.016f);
|
||||
|
||||
const auto& frame = runtime.GetLastUIFrame();
|
||||
ASSERT_EQ(frame.presentedLayerCount, 1u);
|
||||
ASSERT_EQ(frame.skippedLayerCount, 1u);
|
||||
ASSERT_EQ(frame.layers.size(), 1u);
|
||||
|
||||
const auto* modalLayer = FindPresentedLayerById(frame, modalLayerId);
|
||||
ASSERT_NE(modalLayer, nullptr);
|
||||
EXPECT_EQ(modalLayer->stats.inputEventCount, 1u);
|
||||
EXPECT_EQ(FindPresentedLayerById(frame, gameplayLayerId), nullptr);
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Modal"));
|
||||
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Blocked Gameplay"));
|
||||
}
|
||||
|
||||
TEST_F(SceneRuntimeTest, HiddenTopLayerDoesNotStealInputFromVisibleUnderlyingLayer) {
|
||||
Scene* runtimeScene = CreateScene("RuntimeScene");
|
||||
runtime.Start(runtimeScene);
|
||||
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
|
||||
runtime.SetUIFocused(true);
|
||||
|
||||
TempFileScope gameplayView("xcui_scene_runtime_visible_gameplay", ".xcui", BuildViewMarkup("Visible Gameplay"));
|
||||
TempFileScope hiddenOverlayView("xcui_scene_runtime_hidden_overlay", ".xcui", BuildViewMarkup("Hidden Overlay"));
|
||||
|
||||
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
|
||||
gameplayOptions.debugName = "gameplay";
|
||||
gameplayOptions.acceptsInput = true;
|
||||
gameplayOptions.blocksLayersBelow = false;
|
||||
|
||||
XCEngine::UI::Runtime::UIScreenLayerOptions hiddenOverlayOptions = {};
|
||||
hiddenOverlayOptions.debugName = "hidden-overlay";
|
||||
hiddenOverlayOptions.visible = false;
|
||||
hiddenOverlayOptions.acceptsInput = true;
|
||||
hiddenOverlayOptions.blocksLayersBelow = false;
|
||||
|
||||
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
||||
BuildScreenAsset(gameplayView.Path(), "runtime.visible.gameplay"),
|
||||
gameplayOptions);
|
||||
const auto hiddenOverlayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
||||
BuildScreenAsset(hiddenOverlayView.Path(), "runtime.hidden.overlay"),
|
||||
hiddenOverlayOptions);
|
||||
ASSERT_NE(gameplayLayerId, 0u);
|
||||
ASSERT_NE(hiddenOverlayLayerId, 0u);
|
||||
|
||||
XCEngine::UI::UIInputEvent textEvent = {};
|
||||
textEvent.type = XCEngine::UI::UIInputEventType::Character;
|
||||
textEvent.character = 'W';
|
||||
runtime.QueueUIInputEvent(textEvent);
|
||||
|
||||
runtime.Update(0.016f);
|
||||
|
||||
const auto& frame = runtime.GetLastUIFrame();
|
||||
ASSERT_EQ(frame.presentedLayerCount, 1u);
|
||||
ASSERT_EQ(frame.skippedLayerCount, 1u);
|
||||
ASSERT_EQ(frame.layers.size(), 1u);
|
||||
|
||||
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
|
||||
ASSERT_NE(gameplayLayer, nullptr);
|
||||
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 1u);
|
||||
EXPECT_EQ(FindPresentedLayerById(frame, hiddenOverlayLayerId), nullptr);
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Visible Gameplay"));
|
||||
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Hidden Overlay"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user