From 74b1280aa6e12c722de1c624a2e3e740d26f4286 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 16:32:43 +0800 Subject: [PATCH] Unlink XCNewEditor default shell from ImGui compat slice --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 8 +- .../XCEngine/UI/Widgets/UIEditorPanelChrome.h | 126 ++++++++++ .../UI/Widgets/UIFlatHierarchyHelpers.h | 167 +++++++++++++ new_editor/CMakeLists.txt | 2 +- new_editor/src/ApplicationDefaultShell.cpp | 138 +++++++++++ .../src/XCUIBackend/XCUILayoutLabRuntime.cpp | 88 +++---- .../src/XCUIBackend/XCUINativeShellLayout.h | 227 ++++++++++++++++++ .../src/XCUIBackend/XCUIPanelCanvasHost.h | 23 +- tests/Core/UI/CMakeLists.txt | 2 + tests/Core/UI/test_ui_editor_panel_chrome.cpp | 126 ++++++++++ .../UI/test_ui_flat_hierarchy_helpers.cpp | 145 +++++++++++ .../test_xcui_native_shell_layout.cpp | 133 ++++++++++ 12 files changed, 1128 insertions(+), 57 deletions(-) create mode 100644 engine/include/XCEngine/UI/Widgets/UIEditorPanelChrome.h create mode 100644 engine/include/XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h create mode 100644 new_editor/src/ApplicationDefaultShell.cpp create mode 100644 new_editor/src/XCUIBackend/XCUINativeShellLayout.h create mode 100644 tests/Core/UI/test_ui_editor_panel_chrome.cpp create mode 100644 tests/Core/UI/test_ui_flat_hierarchy_helpers.cpp create mode 100644 tests/NewEditor/test_xcui_native_shell_layout.cpp diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 34006698..c4b32d61 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -135,8 +135,8 @@ Current gap: Current gap: -- The default shell host is now native, but the legacy ImGui shell and panel path still exists as a compatibility host and is still compiled into `new_editor`. -- The default native shell path is now split away from direct `ImGui::*` calls at the main-target header/include level, but the legacy ImGui shell and panel implementations still remain compiled behind `XCNewEditorImGuiCompat`. +- The default shell host is now native, and the legacy ImGui shell/panel path has been split out of the default executable into the standalone `XCNewEditorImGuiCompat` compatibility slice. +- The default native shell path is now split away from direct `ImGui::*` calls at the main-target header/include level and no longer links the compatibility slice by default. - The native shell currently proves direct runtime composition, but its shell chrome is still a bespoke `Application`-side layout rather than a fully shared XCUI-authored editor shell document. - Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/focus traversal, toolbar/menu chrome, menu interaction widgets, and icon-atlas widgets are not yet extracted into reusable XCUI modules. - The default native text path now uses a standalone Windows/GDI atlas through `XCUIStandaloneTextAtlasProvider`, but that provider still lives inside `new_editor` and is not yet promoted into a shared/cross-platform text subsystem. @@ -309,7 +309,7 @@ Current gap: - Schema instance validation is still open beyond `.xcschema` self-definition and artifact round-trip coverage. - `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet. -- `XCNewEditor` no longer depends on ImGui at the default-path header/include level, but `XCNewEditorImGuiCompat` is still linked into the executable and still houses the legacy shell/panel path. +- `XCNewEditor` no longer depends on ImGui at the default-path header/include level and no longer links `XCNewEditorImGuiCompat`; the legacy shell/panel path now lives only in the standalone compatibility slice. - Legacy panel implementations such as `XCUIDemoPanel` / `XCUILayoutLabPanel` still render as ImGui windows inside the compatibility slice, so editor-layer behavior is not yet fully carried by XCUI-native shell composition. - The default native text path now owns its atlas without ImGui, but the provider is still Windows-only and remains trapped inside `new_editor` instead of a shared/cross-platform text layer. - Hosted-preview compatibility presentation still depends on an ImGui-only inline presenter path when not using the queued native surface path. @@ -318,7 +318,7 @@ Current gap: ## Execution-Plan Alignment - Against `XCUI完整架构设计与执行计划.md`, current `new_editor` progress should be treated as an early `Phase 8` foothold rather than full `Milestone E` completion: - - landed: `NativeWindowUICompositor`, native shell packet composition, native hosted-preview publication, XCUI-owned texture registrations, native panel surface-image presentation, standalone native text-atlas ownership inside `new_editor`, legacy `Application` TU split, `XCNewEditorImGuiCompat`, main-target header/include isolation from ImGui, and compat-only factory/font seam tightening + - landed: `NativeWindowUICompositor`, native shell packet composition, native hosted-preview publication, XCUI-owned texture registrations, native panel surface-image presentation, standalone native text-atlas ownership inside `new_editor`, legacy `Application` TU split, `XCNewEditorImGuiCompat`, main-target header/include isolation from ImGui, default-executable unlinking from the compatibility slice, compat-only factory/font seam tightening, shared native shell layout helpers, and shared panel-chrome/flat-hierarchy helpers - not yet landed: promotion of the native text-atlas path into a shared/cross-platform text subsystem, shared XCUI-authored editor shell chrome, and retirement of legacy ImGui-window panel implementations - That means the next de-ImGui push should not keep centering on hosted-preview publication; that milestone is now effectively closed for the default native shell path. - The real remaining default-path blockers are: diff --git a/engine/include/XCEngine/UI/Widgets/UIEditorPanelChrome.h b/engine/include/XCEngine/UI/Widgets/UIEditorPanelChrome.h new file mode 100644 index 00000000..2401fc21 --- /dev/null +++ b/engine/include/XCEngine/UI/Widgets/UIEditorPanelChrome.h @@ -0,0 +1,126 @@ +#pragma once + +#include + +#include +#include + +namespace XCEngine { +namespace UI { +namespace Widgets { + +struct UIEditorPanelChromeState { + bool active = false; + bool hovered = false; +}; + +struct UIEditorPanelChromeText { + std::string_view title = {}; + std::string_view subtitle = {}; + std::string_view footer = {}; +}; + +struct UIEditorPanelChromeMetrics { + float cornerRounding = 18.0f; + float headerHeight = 42.0f; + float titleInsetX = 16.0f; + float titleInsetY = 12.0f; + float subtitleInsetY = 28.0f; + float footerInsetX = 16.0f; + float footerInsetBottom = 18.0f; + float activeBorderThickness = 2.0f; + float inactiveBorderThickness = 1.0f; +}; + +struct UIEditorPanelChromePalette { + UIColor surfaceColor = UIColor(9.0f / 255.0f, 13.0f / 255.0f, 18.0f / 255.0f, 212.0f / 255.0f); + UIColor borderColor = UIColor(53.0f / 255.0f, 72.0f / 255.0f, 96.0f / 255.0f, 1.0f); + UIColor accentColor = UIColor(84.0f / 255.0f, 176.0f / 255.0f, 244.0f / 255.0f, 1.0f); + UIColor hoveredAccentColor = UIColor(1.0f, 206.0f / 255.0f, 112.0f / 255.0f, 1.0f); + UIColor headerColor = UIColor(13.0f / 255.0f, 20.0f / 255.0f, 28.0f / 255.0f, 242.0f / 255.0f); + UIColor textPrimary = UIColor(232.0f / 255.0f, 238.0f / 255.0f, 246.0f / 255.0f, 1.0f); + UIColor textSecondary = UIColor(150.0f / 255.0f, 164.0f / 255.0f, 184.0f / 255.0f, 1.0f); + UIColor textMuted = UIColor(108.0f / 255.0f, 123.0f / 255.0f, 145.0f / 255.0f, 1.0f); +}; + +inline UIRect BuildUIEditorPanelChromeHeaderRect( + const UIRect& panelRect, + const UIEditorPanelChromeMetrics& metrics = {}) { + return UIRect( + panelRect.x, + panelRect.y, + panelRect.width, + metrics.headerHeight); +} + +inline UIColor ResolveUIEditorPanelChromeBorderColor( + const UIEditorPanelChromeState& state, + const UIEditorPanelChromePalette& palette = {}) { + if (state.active) { + return palette.accentColor; + } + + if (state.hovered) { + return palette.hoveredAccentColor; + } + + return palette.borderColor; +} + +inline float ResolveUIEditorPanelChromeBorderThickness( + const UIEditorPanelChromeState& state, + const UIEditorPanelChromeMetrics& metrics = {}) { + return state.active + ? metrics.activeBorderThickness + : metrics.inactiveBorderThickness; +} + +inline void AppendUIEditorPanelChromeBackground( + UIDrawList& drawList, + const UIRect& panelRect, + const UIEditorPanelChromeState& state, + const UIEditorPanelChromePalette& palette = {}, + const UIEditorPanelChromeMetrics& metrics = {}) { + drawList.AddFilledRect(panelRect, palette.surfaceColor, metrics.cornerRounding); + drawList.AddRectOutline( + panelRect, + ResolveUIEditorPanelChromeBorderColor(state, palette), + ResolveUIEditorPanelChromeBorderThickness(state, metrics), + metrics.cornerRounding); + drawList.AddFilledRect( + BuildUIEditorPanelChromeHeaderRect(panelRect, metrics), + palette.headerColor, + metrics.cornerRounding); +} + +inline void AppendUIEditorPanelChromeForeground( + UIDrawList& drawList, + const UIRect& panelRect, + const UIEditorPanelChromeText& text, + const UIEditorPanelChromePalette& palette = {}, + const UIEditorPanelChromeMetrics& metrics = {}) { + if (!text.title.empty()) { + drawList.AddText( + UIPoint(panelRect.x + metrics.titleInsetX, panelRect.y + metrics.titleInsetY), + std::string(text.title), + palette.textPrimary); + } + + if (!text.subtitle.empty()) { + drawList.AddText( + UIPoint(panelRect.x + metrics.titleInsetX, panelRect.y + metrics.subtitleInsetY), + std::string(text.subtitle), + palette.textSecondary); + } + + if (!text.footer.empty()) { + drawList.AddText( + UIPoint(panelRect.x + metrics.footerInsetX, panelRect.y + panelRect.height - metrics.footerInsetBottom), + std::string(text.footer), + palette.textMuted); + } +} + +} // namespace Widgets +} // namespace UI +} // namespace XCEngine diff --git a/engine/include/XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h b/engine/include/XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h new file mode 100644 index 00000000..0d51261c --- /dev/null +++ b/engine/include/XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h @@ -0,0 +1,167 @@ +#pragma once + +#include +#include +#include + +namespace XCEngine { +namespace UI { +namespace Widgets { + +inline constexpr std::size_t kInvalidUIFlatHierarchyItemOffset = static_cast(-1); + +namespace Detail { + +template +float ResolveUIFlatHierarchyDepth( + std::span itemIndices, + std::size_t itemOffset, + ResolveDepthFn&& resolveDepth) { + if (itemOffset >= itemIndices.size()) { + return 0.0f; + } + + const float depth = static_cast(resolveDepth(itemIndices[itemOffset])); + return depth > 0.0f ? depth : 0.0f; +} + +} // namespace Detail + +template +bool UIFlatHierarchyHasChildren( + std::span itemIndices, + std::size_t itemOffset, + ResolveDepthFn&& resolveDepth) { + if (itemOffset >= itemIndices.size()) { + return false; + } + + const float itemDepth = Detail::ResolveUIFlatHierarchyDepth( + itemIndices, + itemOffset, + std::forward(resolveDepth)); + for (std::size_t candidateOffset = itemOffset + 1u; + candidateOffset < itemIndices.size(); + ++candidateOffset) { + const float candidateDepth = Detail::ResolveUIFlatHierarchyDepth( + itemIndices, + candidateOffset, + std::forward(resolveDepth)); + if (candidateDepth <= itemDepth) { + break; + } + + return true; + } + + return false; +} + +template +std::size_t UIFlatHierarchyFindParentOffset( + std::span itemIndices, + std::size_t itemOffset, + ResolveDepthFn&& resolveDepth) { + if (itemOffset >= itemIndices.size()) { + return kInvalidUIFlatHierarchyItemOffset; + } + + const float itemDepth = Detail::ResolveUIFlatHierarchyDepth( + itemIndices, + itemOffset, + std::forward(resolveDepth)); + if (itemDepth <= 0.0f) { + return kInvalidUIFlatHierarchyItemOffset; + } + + for (std::size_t candidateOffset = itemOffset; candidateOffset > 0u; --candidateOffset) { + const std::size_t previousOffset = candidateOffset - 1u; + const float previousDepth = Detail::ResolveUIFlatHierarchyDepth( + itemIndices, + previousOffset, + std::forward(resolveDepth)); + if (previousDepth < itemDepth) { + return previousOffset; + } + } + + return kInvalidUIFlatHierarchyItemOffset; +} + +template +bool UIFlatHierarchyIsVisible( + std::span itemIndices, + std::size_t itemOffset, + ResolveDepthFn&& resolveDepth, + IsExpandedFn&& isExpanded) { + if (itemOffset >= itemIndices.size()) { + return false; + } + + float requiredAncestorDepth = Detail::ResolveUIFlatHierarchyDepth( + itemIndices, + itemOffset, + std::forward(resolveDepth)); + if (requiredAncestorDepth <= 0.0f) { + return true; + } + + for (std::size_t candidateOffset = itemOffset; + candidateOffset > 0u && requiredAncestorDepth > 0.0f; + --candidateOffset) { + const std::size_t previousOffset = candidateOffset - 1u; + const float previousDepth = Detail::ResolveUIFlatHierarchyDepth( + itemIndices, + previousOffset, + std::forward(resolveDepth)); + if (previousDepth < requiredAncestorDepth) { + if (!isExpanded(itemIndices[previousOffset])) { + return false; + } + + requiredAncestorDepth = previousDepth; + } + } + + return true; +} + +template +std::size_t UIFlatHierarchyFindFirstVisibleChildOffset( + std::span itemIndices, + std::size_t itemOffset, + ResolveDepthFn&& resolveDepth, + IsVisibleFn&& isVisible) { + if (!UIFlatHierarchyHasChildren( + itemIndices, + itemOffset, + std::forward(resolveDepth))) { + return kInvalidUIFlatHierarchyItemOffset; + } + + const float itemDepth = Detail::ResolveUIFlatHierarchyDepth( + itemIndices, + itemOffset, + std::forward(resolveDepth)); + for (std::size_t candidateOffset = itemOffset + 1u; + candidateOffset < itemIndices.size(); + ++candidateOffset) { + const float candidateDepth = Detail::ResolveUIFlatHierarchyDepth( + itemIndices, + candidateOffset, + std::forward(resolveDepth)); + if (candidateDepth <= itemDepth) { + break; + } + + if (isVisible(itemIndices[candidateOffset])) { + return candidateOffset; + } + } + + return kInvalidUIFlatHierarchyItemOffset; +} + +} // namespace Widgets +} // namespace UI +} // namespace XCEngine diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 3eac9d15..5acbba4d 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -44,6 +44,7 @@ endif() set(NEW_EDITOR_SOURCES src/main.cpp src/Application.cpp + src/ApplicationDefaultShell.cpp src/panels/Panel.cpp src/Rendering/MainWindowBackdropPass.cpp src/Rendering/MainWindowNativeBackdropRenderer.cpp @@ -156,7 +157,6 @@ target_link_libraries(XCNewEditorImGuiCompat PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE XCEngine - XCNewEditorImGuiCompat d3d12.lib dxgi.lib user32 diff --git a/new_editor/src/ApplicationDefaultShell.cpp b/new_editor/src/ApplicationDefaultShell.cpp new file mode 100644 index 00000000..5e54805e --- /dev/null +++ b/new_editor/src/ApplicationDefaultShell.cpp @@ -0,0 +1,138 @@ +#include "Application.h" + +#include "XCUIBackend/NativeWindowUICompositor.h" +#include "panels/XCUIDemoPanel.h" +#include "panels/XCUILayoutLabPanel.h" + +namespace XCEngine { +namespace NewEditor { + +Application::Application() = default; +Application::~Application() = default; + +std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> +Application::CreateHostedPreviewPresenter(bool nativePreview) { + if (nativePreview) { + return ::XCEngine::Editor::XCUIBackend::CreateQueuedNativeXCUIHostedPreviewPresenter( + m_hostedPreviewQueue, + m_hostedPreviewSurfaceRegistry); + } + + return ::XCEngine::Editor::XCUIBackend::CreateNullXCUIHostedPreviewPresenter(); +} + +void Application::InitializePanelsForActiveWindowHost() { + InitializeNativeShell(); +} + +void Application::InitializeLegacyImGuiPanels() { +} + +void Application::ConfigureHostedPreviewPresenters() { +} + +void Application::ResetLegacyPanels() { + m_demoPanel.reset(); + m_layoutLabPanel.reset(); +} + +void Application::SyncShellChromePanelStateFromPanels() { +} + +void Application::ConfigureShellCommandRouter() { + m_shellCommandRouter.Clear(); + + ShellCommandBindings bindings = {}; + bindings.getXCUIDemoPanelVisible = [this]() { + return m_shellChromeState.IsPanelVisible(ShellPanelId::XCUIDemo); + }; + bindings.setXCUIDemoPanelVisible = [this](bool visible) { + m_shellChromeState.SetPanelVisible(ShellPanelId::XCUIDemo, visible); + }; + bindings.getXCUILayoutLabPanelVisible = [this]() { + return m_shellChromeState.IsPanelVisible(ShellPanelId::XCUILayoutLab); + }; + bindings.setXCUILayoutLabPanelVisible = [this](bool visible) { + m_shellChromeState.SetPanelVisible(ShellPanelId::XCUILayoutLab, visible); + }; + bindings.getNativeBackdropVisible = [this]() { + return IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop); + }; + bindings.setNativeBackdropVisible = [this](bool visible) { + SetShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop, visible); + }; + bindings.getPulseAccentEnabled = [this]() { + return IsShellViewToggleEnabled(ShellViewToggleId::PulseAccent); + }; + bindings.setPulseAccentEnabled = [this](bool enabled) { + SetShellViewToggleEnabled(ShellViewToggleId::PulseAccent, enabled); + }; + bindings.getNativeXCUIOverlayVisible = [this]() { + return IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay); + }; + bindings.setNativeXCUIOverlayVisible = [this](bool visible) { + SetShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay, visible); + }; + bindings.getHostedPreviewHudVisible = [this]() { + return IsShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud); + }; + bindings.setHostedPreviewHudVisible = [this](bool visible) { + SetShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud, visible); + }; + bindings.getNativeDemoPanelPreviewEnabled = [this]() { + return IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo); + }; + bindings.setNativeDemoPanelPreviewEnabled = [this](bool enabled) { + m_shellChromeState.SetHostedPreviewMode( + ShellPanelId::XCUIDemo, + enabled ? ShellHostedPreviewMode::NativeOffscreen : ShellHostedPreviewMode::HostedPresenter); + }; + bindings.getNativeLayoutLabPreviewEnabled = [this]() { + return IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab); + }; + bindings.setNativeLayoutLabPreviewEnabled = [this](bool enabled) { + m_shellChromeState.SetHostedPreviewMode( + ShellPanelId::XCUILayoutLab, + enabled ? ShellHostedPreviewMode::NativeOffscreen : ShellHostedPreviewMode::HostedPresenter); + }; + + Application::RegisterShellViewCommands(m_shellCommandRouter, bindings); +} + +::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta +Application::DispatchShellShortcuts( + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot& snapshot) { + if (!m_shellInputBridge.HasBaseline()) { + m_shellInputBridge.Prime(snapshot); + } + + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta frameDelta = + m_shellInputBridge.Translate(snapshot); + const ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot commandSnapshot = + Application::BuildShellShortcutSnapshot(frameDelta); + m_shellCommandRouter.InvokeMatchingShortcut({ &commandSnapshot }); + return frameDelta; +} + +void Application::InitializeWindowCompositor() { + m_windowCompositor = ::XCEngine::Editor::XCUIBackend::CreateNativeWindowUICompositor(); + if (m_windowCompositor != nullptr) { + m_windowCompositor->Initialize(m_hwnd, m_windowRenderer, {}); + } +} + +void Application::FrameLegacyImGuiHost() { + m_xcuiInputSource.ClearFrameTransients(); +} + +void Application::RenderLegacyImGuiUiFrame() { +} + +void Application::RenderShellChrome() { +} + +void Application::RenderHostedPreviewHud() { +} + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp index 20c6796c..f53d7194 100644 --- a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp +++ b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -556,23 +557,17 @@ bool HasTreeItemChildren(const RuntimeBuildContext& state, std::size_t nodeIndex 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; + return UIWidgets::UIFlatHierarchyHasChildren( + std::span(parent.children), + static_cast(siblingIt - parent.children.begin()), + [&state](std::size_t siblingIndex) { + return ResolveTreeIndentLevel(state.nodes[siblingIndex]); + }); } bool IsNodeCollapsible(const RuntimeBuildContext& state, std::size_t nodeIndex) { @@ -626,32 +621,20 @@ bool IsNodeVisible(const RuntimeBuildContext& state, std::size_t nodeIndex) { 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(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; + return UIWidgets::UIFlatHierarchyIsVisible( + std::span(parent.children), + static_cast(siblingIt - parent.children.begin()), + [&state](std::size_t siblingIndex) { + return ResolveTreeIndentLevel(state.nodes[siblingIndex]); + }, + [&state](std::size_t siblingIndex) { + return IsNodeExpanded(state, siblingIndex); + }); } std::vector CollectVisibleChildren( @@ -961,14 +944,17 @@ std::size_t FindTreeParentItemIndex( return kInvalidIndex; } - for (auto it = siblingIt; it != treeView.children.begin();) { - --it; - if (ResolveTreeIndentLevel(state.nodes[*it]) < indentLevel) { - return *it; - } + const std::size_t parentOffset = UIWidgets::UIFlatHierarchyFindParentOffset( + std::span(treeView.children), + static_cast(siblingIt - treeView.children.begin()), + [&state](std::size_t siblingIndex) { + return ResolveTreeIndentLevel(state.nodes[siblingIndex]); + }); + if (parentOffset == UIWidgets::kInvalidUIFlatHierarchyItemOffset) { + return kInvalidIndex; } - return kInvalidIndex; + return treeView.children[parentOffset]; } std::size_t FindFirstTreeChildItemIndex( @@ -981,25 +967,25 @@ std::size_t FindFirstTreeChildItemIndex( const LayoutNode& node = state.nodes[nodeIndex]; const LayoutNode& treeView = state.nodes[node.parentIndex]; - const float indentLevel = ResolveTreeIndentLevel(node); const auto siblingIt = std::find(treeView.children.begin(), treeView.children.end(), nodeIndex); if (siblingIt == treeView.children.end()) { return kInvalidIndex; } - for (auto it = siblingIt + 1; it != treeView.children.end(); ++it) { - const std::size_t candidateIndex = *it; - const float candidateIndent = ResolveTreeIndentLevel(state.nodes[candidateIndex]); - if (candidateIndent <= indentLevel) { - break; - } - - if (IsNodeVisible(state, candidateIndex)) { - return candidateIndex; - } + const std::size_t childOffset = UIWidgets::UIFlatHierarchyFindFirstVisibleChildOffset( + std::span(treeView.children), + static_cast(siblingIt - treeView.children.begin()), + [&state](std::size_t siblingIndex) { + return ResolveTreeIndentLevel(state.nodes[siblingIndex]); + }, + [&state](std::size_t siblingIndex) { + return IsNodeVisible(state, siblingIndex); + }); + if (childOffset == UIWidgets::kInvalidUIFlatHierarchyItemOffset) { + return kInvalidIndex; } - return kInvalidIndex; + return treeView.children[childOffset]; } bool HandleKeyboardExpand(RuntimeBuildContext& state) { diff --git a/new_editor/src/XCUIBackend/XCUINativeShellLayout.h b/new_editor/src/XCUIBackend/XCUINativeShellLayout.h new file mode 100644 index 00000000..fc029e2c --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUINativeShellLayout.h @@ -0,0 +1,227 @@ +#pragma once + +#include "XCUIShellChromeState.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +struct XCUINativeShellMetrics { + float outerMargin = 22.0f; + float innerGap = 18.0f; + float headerHeight = 58.0f; + float footerHeight = 34.0f; + float panelHeaderHeight = 42.0f; + float panelPadding = 14.0f; + float panelMinWidth = 260.0f; + float panelMinHeight = 180.0f; + float primaryPanelWidthRatio = 0.60f; +}; + +struct XCUINativeShellPanelSpec { + XCUIShellPanelId panelId = XCUIShellPanelId::XCUIDemo; + std::string_view title = {}; + bool visible = true; +}; + +struct XCUINativeShellPanelLayout { + XCUIShellPanelId panelId = XCUIShellPanelId::XCUIDemo; + std::string title = {}; + UI::UIRect panelRect = {}; + UI::UIRect canvasRect = {}; + bool visible = false; + bool hovered = false; + bool active = false; +}; + +struct XCUINativeShellLayoutResult { + UI::UIRect shellRect = {}; + UI::UIRect topBarRect = {}; + UI::UIRect footerRect = {}; + UI::UIRect workspaceRect = {}; + std::vector panelLayouts = {}; + XCUIShellPanelId activePanel = XCUIShellPanelId::XCUIDemo; +}; + +inline bool XCUINativeShellContainsPoint( + const UI::UIRect& rect, + const UI::UIPoint& point) { + return point.x >= rect.x && + point.y >= rect.y && + point.x <= rect.x + rect.width && + point.y <= rect.y + rect.height; +} + +inline UI::UIRect BuildXCUINativeShellCanvasRect( + const UI::UIRect& panelRect, + const XCUINativeShellMetrics& metrics = {}) { + return UI::UIRect( + panelRect.x + metrics.panelPadding, + panelRect.y + metrics.panelHeaderHeight, + (std::max)(0.0f, panelRect.width - metrics.panelPadding * 2.0f), + (std::max)(0.0f, panelRect.height - metrics.panelHeaderHeight - metrics.panelPadding)); +} + +inline XCUINativeShellPanelLayout BuildXCUINativeShellPanelLayout( + const XCUINativeShellPanelSpec& spec, + const UI::UIRect& panelRect, + const UI::UIPoint& pointerPosition, + bool windowFocused, + bool active, + const XCUINativeShellMetrics& metrics = {}) { + XCUINativeShellPanelLayout layout = {}; + layout.panelId = spec.panelId; + layout.title = std::string(spec.title); + layout.panelRect = panelRect; + layout.canvasRect = BuildXCUINativeShellCanvasRect(panelRect, metrics); + layout.visible = + spec.visible && + panelRect.width >= metrics.panelMinWidth && + panelRect.height >= metrics.panelMinHeight; + layout.hovered = windowFocused && XCUINativeShellContainsPoint(layout.canvasRect, pointerPosition); + layout.active = active; + return layout; +} + +inline XCUIShellPanelId ResolveXCUINativeShellActivePanel( + XCUIShellPanelId currentActivePanel, + const std::vector& panelLayouts, + bool pointerPressed) { + if (panelLayouts.empty()) { + return currentActivePanel; + } + + bool activePanelStillPresent = false; + for (const XCUINativeShellPanelLayout& panelLayout : panelLayouts) { + if (panelLayout.panelId == currentActivePanel) { + activePanelStillPresent = true; + break; + } + } + + XCUIShellPanelId resolvedActivePanel = activePanelStillPresent + ? currentActivePanel + : panelLayouts.front().panelId; + if (!pointerPressed) { + return resolvedActivePanel; + } + + for (const XCUINativeShellPanelLayout& panelLayout : panelLayouts) { + if (panelLayout.hovered) { + return panelLayout.panelId; + } + } + + return resolvedActivePanel; +} + +inline XCUINativeShellLayoutResult BuildXCUINativeShellLayout( + const UI::UIRect& shellRect, + std::initializer_list panelSpecs, + XCUIShellPanelId currentActivePanel, + const UI::UIPoint& pointerPosition, + bool windowFocused, + bool pointerPressed, + const XCUINativeShellMetrics& metrics = {}) { + XCUINativeShellLayoutResult result = {}; + result.shellRect = shellRect; + result.activePanel = currentActivePanel; + + result.topBarRect = UI::UIRect( + shellRect.x + metrics.outerMargin, + shellRect.y + metrics.outerMargin, + (std::max)(0.0f, shellRect.width - metrics.outerMargin * 2.0f), + metrics.headerHeight); + + result.footerRect = UI::UIRect( + shellRect.x + metrics.outerMargin, + (std::max)( + result.topBarRect.y + result.topBarRect.height + metrics.innerGap, + shellRect.y + shellRect.height - metrics.outerMargin - metrics.footerHeight), + (std::max)(0.0f, shellRect.width - metrics.outerMargin * 2.0f), + metrics.footerHeight); + + const float workspaceTop = result.topBarRect.y + result.topBarRect.height + metrics.innerGap; + const float workspaceBottom = result.footerRect.y - metrics.innerGap; + result.workspaceRect = UI::UIRect( + shellRect.x + metrics.outerMargin, + workspaceTop, + (std::max)(0.0f, shellRect.width - metrics.outerMargin * 2.0f), + (std::max)(0.0f, workspaceBottom - workspaceTop)); + + std::vector visiblePanels = {}; + visiblePanels.reserve(panelSpecs.size()); + for (const XCUINativeShellPanelSpec& panelSpec : panelSpecs) { + if (panelSpec.visible) { + visiblePanels.push_back(panelSpec); + } + } + + if (visiblePanels.empty()) { + return result; + } + + result.panelLayouts.reserve((std::min)(std::size_t(2), visiblePanels.size())); + if (visiblePanels.size() >= 2u) { + const float leftWidth = (std::max)( + metrics.panelMinWidth, + (std::min)( + result.workspaceRect.width * metrics.primaryPanelWidthRatio, + result.workspaceRect.width - metrics.panelMinWidth - metrics.innerGap)); + const float rightWidth = (std::max)(0.0f, result.workspaceRect.width - leftWidth - metrics.innerGap); + + result.panelLayouts.push_back(BuildXCUINativeShellPanelLayout( + visiblePanels[0], + UI::UIRect( + result.workspaceRect.x, + result.workspaceRect.y, + leftWidth, + result.workspaceRect.height), + pointerPosition, + windowFocused, + false, + metrics)); + result.panelLayouts.push_back(BuildXCUINativeShellPanelLayout( + visiblePanels[1], + UI::UIRect( + result.workspaceRect.x + leftWidth + metrics.innerGap, + result.workspaceRect.y, + rightWidth, + result.workspaceRect.height), + pointerPosition, + windowFocused, + false, + metrics)); + } else { + result.panelLayouts.push_back(BuildXCUINativeShellPanelLayout( + visiblePanels.front(), + result.workspaceRect, + pointerPosition, + windowFocused, + false, + metrics)); + } + + result.activePanel = ResolveXCUINativeShellActivePanel( + currentActivePanel, + result.panelLayouts, + pointerPressed); + for (XCUINativeShellPanelLayout& panelLayout : result.panelLayouts) { + panelLayout.active = panelLayout.panelId == result.activePanel; + } + + return result; +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h b/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h index 1e1426b2..c21fa78d 100644 --- a/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h +++ b/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h @@ -6,6 +6,7 @@ #include #include +#include #include namespace XCEngine { @@ -14,13 +15,15 @@ namespace XCUIBackend { enum class XCUIPanelCanvasHostBackend : std::uint8_t { Null = 0, - ImGui + ImGui, + Native }; struct XCUIPanelCanvasHostCapabilities { bool supportsPointerHitTesting = false; bool supportsHostedSurfaceImages = false; bool supportsPrimitiveOverlays = false; + bool supportsExternallyDrivenSession = false; }; struct XCUIPanelCanvasRequest { @@ -46,6 +49,20 @@ struct XCUIPanelCanvasSession { bool windowFocused = false; }; +struct XCUIPanelCanvasFrameSnapshot { + std::string childId = {}; + XCUIPanelCanvasSession session = {}; + bool bordered = true; + bool showingSurfaceImage = false; + bool drawPreviewFrame = true; + std::string placeholderTitle = {}; + std::string placeholderSubtitle = {}; + std::string badgeTitle = {}; + std::string badgeSubtitle = {}; + XCUIHostedPreviewSurfaceImage surfaceImage = {}; + ::XCEngine::UI::UIDrawData overlayDrawData = {}; +}; + inline const char* ResolveXCUIPanelCanvasChildId( const XCUIPanelCanvasRequest& request, const char* fallback = "XCUIPanelCanvasHost") { @@ -99,6 +116,10 @@ public: const ::XCEngine::UI::UIColor& color, float fontSize = 0.0f) = 0; virtual void EndCanvas() = 0; + virtual bool TryGetLatestFrameSnapshot(XCUIPanelCanvasFrameSnapshot& outSnapshot) const { + outSnapshot = {}; + return false; + } }; std::unique_ptr CreateNullXCUIPanelCanvasHost(); diff --git a/tests/Core/UI/CMakeLists.txt b/tests/Core/UI/CMakeLists.txt index 5b0e864b..22b90b75 100644 --- a/tests/Core/UI/CMakeLists.txt +++ b/tests/Core/UI/CMakeLists.txt @@ -5,7 +5,9 @@ set(UI_TEST_SOURCES test_ui_core.cpp test_ui_editor_collection_primitives.cpp + test_ui_editor_panel_chrome.cpp test_ui_expansion_model.cpp + test_ui_flat_hierarchy_helpers.cpp test_ui_keyboard_navigation_model.cpp test_ui_property_edit_model.cpp test_layout_engine.cpp diff --git a/tests/Core/UI/test_ui_editor_panel_chrome.cpp b/tests/Core/UI/test_ui_editor_panel_chrome.cpp new file mode 100644 index 00000000..c63d9986 --- /dev/null +++ b/tests/Core/UI/test_ui_editor_panel_chrome.cpp @@ -0,0 +1,126 @@ +#include + +#include +#include + +namespace { + +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawCommandType; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIRect; +using XCEngine::UI::Widgets::AppendUIEditorPanelChromeBackground; +using XCEngine::UI::Widgets::AppendUIEditorPanelChromeForeground; +using XCEngine::UI::Widgets::BuildUIEditorPanelChromeHeaderRect; +using XCEngine::UI::Widgets::ResolveUIEditorPanelChromeBorderColor; +using XCEngine::UI::Widgets::ResolveUIEditorPanelChromeBorderThickness; +using XCEngine::UI::Widgets::UIEditorPanelChromePalette; +using XCEngine::UI::Widgets::UIEditorPanelChromeState; +using XCEngine::UI::Widgets::UIEditorPanelChromeText; + +void ExpectColorEq( + const UIColor& actual, + const UIColor& expected) { + EXPECT_FLOAT_EQ(actual.r, expected.r); + EXPECT_FLOAT_EQ(actual.g, expected.g); + EXPECT_FLOAT_EQ(actual.b, expected.b); + EXPECT_FLOAT_EQ(actual.a, expected.a); +} + +TEST(UIEditorPanelChromeTest, HeaderRectAndBorderPolicyMatchNativeShellCardChrome) { + const UIRect panelRect(100.0f, 200.0f, 320.0f, 180.0f); + const UIEditorPanelChromePalette palette = {}; + + const auto headerRect = BuildUIEditorPanelChromeHeaderRect(panelRect); + EXPECT_FLOAT_EQ(headerRect.x, 100.0f); + EXPECT_FLOAT_EQ(headerRect.y, 200.0f); + EXPECT_FLOAT_EQ(headerRect.width, 320.0f); + EXPECT_FLOAT_EQ(headerRect.height, 42.0f); + + ExpectColorEq( + ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState(), palette), + palette.borderColor); + ExpectColorEq( + ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState{ false, true }, palette), + palette.hoveredAccentColor); + ExpectColorEq( + ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState{ true, true }, palette), + palette.accentColor); + + EXPECT_FLOAT_EQ(ResolveUIEditorPanelChromeBorderThickness(UIEditorPanelChromeState()), 1.0f); + EXPECT_FLOAT_EQ(ResolveUIEditorPanelChromeBorderThickness(UIEditorPanelChromeState{ true, false }), 2.0f); +} + +TEST(UIEditorPanelChromeTest, BackgroundAppendEmitsSurfaceOutlineAndHeaderFill) { + UIDrawList drawList("PanelChrome"); + const UIRect panelRect(40.0f, 60.0f, 400.0f, 280.0f); + const UIEditorPanelChromeState state{ true, false }; + const UIEditorPanelChromePalette palette = {}; + + AppendUIEditorPanelChromeBackground(drawList, panelRect, state, palette); + + ASSERT_EQ(drawList.GetCommandCount(), 3u); + const auto& commands = drawList.GetCommands(); + EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect); + EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline); + EXPECT_EQ(commands[2].type, UIDrawCommandType::FilledRect); + + EXPECT_FLOAT_EQ(commands[0].rect.x, 40.0f); + EXPECT_FLOAT_EQ(commands[0].rounding, 18.0f); + ExpectColorEq(commands[0].color, palette.surfaceColor); + + EXPECT_FLOAT_EQ(commands[1].thickness, 2.0f); + EXPECT_FLOAT_EQ(commands[1].rounding, 18.0f); + ExpectColorEq(commands[1].color, palette.accentColor); + + EXPECT_FLOAT_EQ(commands[2].rect.height, 42.0f); + EXPECT_FLOAT_EQ(commands[2].rounding, 18.0f); + ExpectColorEq(commands[2].color, palette.headerColor); +} + +TEST(UIEditorPanelChromeTest, ForegroundAppendPlacesTitleSubtitleAndFooterAtCurrentOffsets) { + UIDrawList drawList("PanelChromeText"); + const UIRect panelRect(100.0f, 200.0f, 320.0f, 180.0f); + const UIEditorPanelChromePalette palette = {}; + const UIEditorPanelChromeText text{ + "XCUI Demo", + "native queued offscreen surface", + "Active | 42 elements | 9 cmds" + }; + + AppendUIEditorPanelChromeForeground(drawList, panelRect, text, palette); + + ASSERT_EQ(drawList.GetCommandCount(), 3u); + const auto& commands = drawList.GetCommands(); + EXPECT_EQ(commands[0].type, UIDrawCommandType::Text); + EXPECT_EQ(commands[1].type, UIDrawCommandType::Text); + EXPECT_EQ(commands[2].type, UIDrawCommandType::Text); + + EXPECT_EQ(commands[0].text, "XCUI Demo"); + EXPECT_FLOAT_EQ(commands[0].position.x, 116.0f); + EXPECT_FLOAT_EQ(commands[0].position.y, 212.0f); + ExpectColorEq(commands[0].color, palette.textPrimary); + + EXPECT_EQ(commands[1].text, "native queued offscreen surface"); + EXPECT_FLOAT_EQ(commands[1].position.x, 116.0f); + EXPECT_FLOAT_EQ(commands[1].position.y, 228.0f); + ExpectColorEq(commands[1].color, palette.textSecondary); + + EXPECT_EQ(commands[2].text, "Active | 42 elements | 9 cmds"); + EXPECT_FLOAT_EQ(commands[2].position.x, 116.0f); + EXPECT_FLOAT_EQ(commands[2].position.y, 362.0f); + ExpectColorEq(commands[2].color, palette.textMuted); +} + +TEST(UIEditorPanelChromeTest, ForegroundAppendSkipsEmptyStrings) { + UIDrawList drawList("PanelChromeEmptyText"); + + AppendUIEditorPanelChromeForeground( + drawList, + UIRect(0.0f, 0.0f, 320.0f, 180.0f), + UIEditorPanelChromeText{}); + + EXPECT_EQ(drawList.GetCommandCount(), 0u); +} + +} // namespace diff --git a/tests/Core/UI/test_ui_flat_hierarchy_helpers.cpp b/tests/Core/UI/test_ui_flat_hierarchy_helpers.cpp new file mode 100644 index 00000000..86dd97c8 --- /dev/null +++ b/tests/Core/UI/test_ui_flat_hierarchy_helpers.cpp @@ -0,0 +1,145 @@ +#include + +#include + +#include +#include +#include + +namespace { + +using XCEngine::UI::Widgets::kInvalidUIFlatHierarchyItemOffset; +using XCEngine::UI::Widgets::UIFlatHierarchyFindFirstVisibleChildOffset; +using XCEngine::UI::Widgets::UIFlatHierarchyFindParentOffset; +using XCEngine::UI::Widgets::UIFlatHierarchyHasChildren; +using XCEngine::UI::Widgets::UIFlatHierarchyIsVisible; + +struct FlatHierarchyItem { + float depth = 0.0f; +}; + +TEST(UIFlatHierarchyHelpersTest, HasChildrenAndParentResolutionTrackIndentedBranches) { + const std::array items = {{ + { 0.0f }, + { 1.0f }, + { 2.0f }, + { 1.0f }, + { 0.0f }, + }}; + const std::vector itemIndices = { 0u, 1u, 2u, 3u, 4u }; + + const auto resolveDepth = [&items](std::size_t itemIndex) { + return items[itemIndex].depth; + }; + + EXPECT_TRUE(UIFlatHierarchyHasChildren(itemIndices, 0u, resolveDepth)); + EXPECT_TRUE(UIFlatHierarchyHasChildren(itemIndices, 1u, resolveDepth)); + EXPECT_FALSE(UIFlatHierarchyHasChildren(itemIndices, 2u, resolveDepth)); + EXPECT_FALSE(UIFlatHierarchyHasChildren(itemIndices, 3u, resolveDepth)); + EXPECT_FALSE(UIFlatHierarchyHasChildren(itemIndices, 4u, resolveDepth)); + + EXPECT_EQ( + UIFlatHierarchyFindParentOffset(itemIndices, 0u, resolveDepth), + kInvalidUIFlatHierarchyItemOffset); + EXPECT_EQ(UIFlatHierarchyFindParentOffset(itemIndices, 1u, resolveDepth), 0u); + EXPECT_EQ(UIFlatHierarchyFindParentOffset(itemIndices, 2u, resolveDepth), 1u); + EXPECT_EQ(UIFlatHierarchyFindParentOffset(itemIndices, 3u, resolveDepth), 0u); + EXPECT_EQ( + UIFlatHierarchyFindParentOffset(itemIndices, 99u, resolveDepth), + kInvalidUIFlatHierarchyItemOffset); +} + +TEST(UIFlatHierarchyHelpersTest, VisibilityFollowsCollapsedAncestorStateAcrossDepthTransitions) { + const std::array items = {{ + { 0.0f }, + { 1.0f }, + { 2.0f }, + { 1.0f }, + }}; + const std::vector itemIndices = { 0u, 1u, 2u, 3u }; + + const auto resolveDepth = [&items](std::size_t itemIndex) { + return items[itemIndex].depth; + }; + + std::unordered_set expandedItems = { 0u }; + const auto isExpanded = [&expandedItems](std::size_t itemIndex) { + return expandedItems.contains(itemIndex); + }; + + EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 0u, resolveDepth, isExpanded)); + EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 1u, resolveDepth, isExpanded)); + EXPECT_FALSE(UIFlatHierarchyIsVisible(itemIndices, 2u, resolveDepth, isExpanded)); + EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 3u, resolveDepth, isExpanded)); + + expandedItems.insert(1u); + EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 2u, resolveDepth, isExpanded)); + + expandedItems.clear(); + EXPECT_FALSE(UIFlatHierarchyIsVisible(itemIndices, 1u, resolveDepth, isExpanded)); + EXPECT_FALSE(UIFlatHierarchyIsVisible(itemIndices, 2u, resolveDepth, isExpanded)); +} + +TEST(UIFlatHierarchyHelpersTest, FirstVisibleChildSkipsCollapsedDescendantsUntilExpanded) { + const std::array items = {{ + { 0.0f }, + { 1.0f }, + { 2.0f }, + { 1.0f }, + }}; + const std::vector itemIndices = { 0u, 1u, 2u, 3u }; + + const auto resolveDepth = [&items](std::size_t itemIndex) { + return items[itemIndex].depth; + }; + + std::unordered_set expandedItems = { 0u }; + const auto isExpanded = [&expandedItems](std::size_t itemIndex) { + return expandedItems.contains(itemIndex); + }; + const auto isVisible = [&itemIndices, &resolveDepth, &isExpanded](std::size_t itemIndex) { + for (std::size_t itemOffset = 0; itemOffset < itemIndices.size(); ++itemOffset) { + if (itemIndices[itemOffset] == itemIndex) { + return UIFlatHierarchyIsVisible(itemIndices, itemOffset, resolveDepth, isExpanded); + } + } + return false; + }; + + EXPECT_EQ( + UIFlatHierarchyFindFirstVisibleChildOffset(itemIndices, 0u, resolveDepth, isVisible), + 1u); + EXPECT_EQ( + UIFlatHierarchyFindFirstVisibleChildOffset(itemIndices, 1u, resolveDepth, isVisible), + kInvalidUIFlatHierarchyItemOffset); + + expandedItems.insert(1u); + EXPECT_EQ( + UIFlatHierarchyFindFirstVisibleChildOffset(itemIndices, 1u, resolveDepth, isVisible), + 2u); +} + +TEST(UIFlatHierarchyHelpersTest, NegativeDepthsClampToRootsForHierarchyQueries) { + const std::array items = {{ + { -3.0f }, + { 1.0f }, + { -1.0f }, + }}; + const std::vector itemIndices = { 0u, 1u, 2u }; + + const auto resolveDepth = [&items](std::size_t itemIndex) { + return items[itemIndex].depth; + }; + const auto isExpanded = [](std::size_t itemIndex) { + return itemIndex == 0u; + }; + + EXPECT_TRUE(UIFlatHierarchyHasChildren(itemIndices, 0u, resolveDepth)); + EXPECT_EQ(UIFlatHierarchyFindParentOffset(itemIndices, 1u, resolveDepth), 0u); + EXPECT_EQ( + UIFlatHierarchyFindParentOffset(itemIndices, 2u, resolveDepth), + kInvalidUIFlatHierarchyItemOffset); + EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 2u, resolveDepth, isExpanded)); +} + +} // namespace diff --git a/tests/NewEditor/test_xcui_native_shell_layout.cpp b/tests/NewEditor/test_xcui_native_shell_layout.cpp new file mode 100644 index 00000000..1fb2edd3 --- /dev/null +++ b/tests/NewEditor/test_xcui_native_shell_layout.cpp @@ -0,0 +1,133 @@ +#include "XCUIBackend/XCUINativeShellLayout.h" + +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::BuildXCUINativeShellCanvasRect; +using XCEngine::Editor::XCUIBackend::BuildXCUINativeShellLayout; +using XCEngine::Editor::XCUIBackend::XCUINativeShellMetrics; +using XCEngine::Editor::XCUIBackend::XCUINativeShellPanelLayout; +using XCEngine::Editor::XCUIBackend::XCUINativeShellPanelSpec; +using XCEngine::Editor::XCUIBackend::XCUIShellPanelId; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; + +void ExpectRectEq( + const UIRect& actual, + const UIRect& expected) { + EXPECT_FLOAT_EQ(actual.x, expected.x); + EXPECT_FLOAT_EQ(actual.y, expected.y); + EXPECT_FLOAT_EQ(actual.width, expected.width); + EXPECT_FLOAT_EQ(actual.height, expected.height); +} + +TEST(XCUINativeShellLayoutTest, CanvasRectUsesPanelHeaderAndPaddingInsets) { + const XCUINativeShellMetrics metrics = {}; + const UIRect panelRect(10.0f, 20.0f, 400.0f, 300.0f); + + const UIRect canvasRect = BuildXCUINativeShellCanvasRect(panelRect, metrics); + + ExpectRectEq(canvasRect, UIRect(24.0f, 62.0f, 372.0f, 244.0f)); +} + +TEST(XCUINativeShellLayoutTest, ActivePanelFallsBackToVisiblePanelWhenRequestedPanelIsHidden) { + const auto layout = BuildXCUINativeShellLayout( + UIRect(0.0f, 0.0f, 1200.0f, 800.0f), + { + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUIDemo, "XCUI Demo", false }, + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUILayoutLab, "XCUI Layout Lab", true }, + }, + XCUIShellPanelId::XCUIDemo, + UIPoint(), + false, + false); + + ASSERT_EQ(layout.panelLayouts.size(), 1u); + EXPECT_EQ(layout.activePanel, XCUIShellPanelId::XCUILayoutLab); + EXPECT_TRUE(layout.panelLayouts.front().active); +} + +TEST(XCUINativeShellLayoutTest, TwoPanelSplitMatchesCurrentSandboxPolicy) { + const XCUINativeShellMetrics metrics = {}; + const auto layout = BuildXCUINativeShellLayout( + UIRect(0.0f, 0.0f, 1200.0f, 800.0f), + { + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUIDemo, "XCUI Demo", true }, + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUILayoutLab, "XCUI Layout Lab", true }, + }, + XCUIShellPanelId::XCUIDemo, + UIPoint(), + false, + false, + metrics); + + ExpectRectEq(layout.topBarRect, UIRect(22.0f, 22.0f, 1156.0f, 58.0f)); + ExpectRectEq(layout.footerRect, UIRect(22.0f, 744.0f, 1156.0f, 34.0f)); + ExpectRectEq(layout.workspaceRect, UIRect(22.0f, 98.0f, 1156.0f, 628.0f)); + + ASSERT_EQ(layout.panelLayouts.size(), 2u); + const XCUINativeShellPanelLayout& leftPanel = layout.panelLayouts[0]; + const XCUINativeShellPanelLayout& rightPanel = layout.panelLayouts[1]; + EXPECT_EQ(leftPanel.panelId, XCUIShellPanelId::XCUIDemo); + EXPECT_EQ(rightPanel.panelId, XCUIShellPanelId::XCUILayoutLab); + EXPECT_NEAR(leftPanel.panelRect.width, 693.6f, 0.001f); + EXPECT_NEAR(rightPanel.panelRect.width, 444.4f, 0.001f); + EXPECT_NEAR(rightPanel.panelRect.x, 733.6f, 0.001f); + EXPECT_FLOAT_EQ(leftPanel.panelRect.height, 628.0f); + EXPECT_FLOAT_EQ(rightPanel.panelRect.height, 628.0f); +} + +TEST(XCUINativeShellLayoutTest, PointerPressTransfersActivePanelToHoveredCanvas) { + const auto layout = BuildXCUINativeShellLayout( + UIRect(0.0f, 0.0f, 1200.0f, 800.0f), + { + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUIDemo, "XCUI Demo", true }, + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUILayoutLab, "XCUI Layout Lab", true }, + }, + XCUIShellPanelId::XCUIDemo, + UIPoint(900.0f, 200.0f), + true, + true); + + ASSERT_EQ(layout.panelLayouts.size(), 2u); + EXPECT_EQ(layout.activePanel, XCUIShellPanelId::XCUILayoutLab); + EXPECT_FALSE(layout.panelLayouts[0].active); + EXPECT_TRUE(layout.panelLayouts[1].active); + EXPECT_TRUE(layout.panelLayouts[1].hovered); +} + +TEST(XCUINativeShellLayoutTest, SingleVisiblePanelFillsWorkspaceAndBecomesActive) { + const auto layout = BuildXCUINativeShellLayout( + UIRect(0.0f, 0.0f, 1000.0f, 720.0f), + { + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUIDemo, "XCUI Demo", true }, + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUILayoutLab, "XCUI Layout Lab", false }, + }, + XCUIShellPanelId::XCUILayoutLab, + UIPoint(), + false, + false); + + ASSERT_EQ(layout.panelLayouts.size(), 1u); + EXPECT_EQ(layout.activePanel, XCUIShellPanelId::XCUIDemo); + ExpectRectEq(layout.panelLayouts[0].panelRect, layout.workspaceRect); +} + +TEST(XCUINativeShellLayoutTest, UndersizedPanelStillParticipatesButReportsNotVisible) { + const auto layout = BuildXCUINativeShellLayout( + UIRect(0.0f, 0.0f, 240.0f, 400.0f), + { + XCUINativeShellPanelSpec{ XCUIShellPanelId::XCUIDemo, "XCUI Demo", true }, + }, + XCUIShellPanelId::XCUIDemo, + UIPoint(), + false, + false); + + ASSERT_EQ(layout.panelLayouts.size(), 1u); + EXPECT_FALSE(layout.panelLayouts[0].visible); + ExpectRectEq(layout.panelLayouts[0].panelRect, layout.workspaceRect); +} + +} // namespace