Unlink XCNewEditor default shell from ImGui compat slice

This commit is contained in:
2026-04-05 16:32:43 +08:00
parent 712cc79c44
commit 74b1280aa6
12 changed files with 1128 additions and 57 deletions

View File

@@ -135,8 +135,8 @@ Current gap:
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 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, but the legacy ImGui shell and panel implementations still remain compiled behind `XCNewEditorImGuiCompat`. - 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. - 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. - 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. - 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. - 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. - `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. - 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. - 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. - 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 ## 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: - 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 - 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. - 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: - The real remaining default-path blockers are:

View File

@@ -0,0 +1,126 @@
#pragma once
#include <XCEngine/UI/DrawData.h>
#include <string>
#include <string_view>
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

View File

@@ -0,0 +1,167 @@
#pragma once
#include <cstddef>
#include <span>
#include <utility>
namespace XCEngine {
namespace UI {
namespace Widgets {
inline constexpr std::size_t kInvalidUIFlatHierarchyItemOffset = static_cast<std::size_t>(-1);
namespace Detail {
template <typename ResolveDepthFn>
float ResolveUIFlatHierarchyDepth(
std::span<const std::size_t> itemIndices,
std::size_t itemOffset,
ResolveDepthFn&& resolveDepth) {
if (itemOffset >= itemIndices.size()) {
return 0.0f;
}
const float depth = static_cast<float>(resolveDepth(itemIndices[itemOffset]));
return depth > 0.0f ? depth : 0.0f;
}
} // namespace Detail
template <typename ResolveDepthFn>
bool UIFlatHierarchyHasChildren(
std::span<const std::size_t> itemIndices,
std::size_t itemOffset,
ResolveDepthFn&& resolveDepth) {
if (itemOffset >= itemIndices.size()) {
return false;
}
const float itemDepth = Detail::ResolveUIFlatHierarchyDepth(
itemIndices,
itemOffset,
std::forward<ResolveDepthFn>(resolveDepth));
for (std::size_t candidateOffset = itemOffset + 1u;
candidateOffset < itemIndices.size();
++candidateOffset) {
const float candidateDepth = Detail::ResolveUIFlatHierarchyDepth(
itemIndices,
candidateOffset,
std::forward<ResolveDepthFn>(resolveDepth));
if (candidateDepth <= itemDepth) {
break;
}
return true;
}
return false;
}
template <typename ResolveDepthFn>
std::size_t UIFlatHierarchyFindParentOffset(
std::span<const std::size_t> itemIndices,
std::size_t itemOffset,
ResolveDepthFn&& resolveDepth) {
if (itemOffset >= itemIndices.size()) {
return kInvalidUIFlatHierarchyItemOffset;
}
const float itemDepth = Detail::ResolveUIFlatHierarchyDepth(
itemIndices,
itemOffset,
std::forward<ResolveDepthFn>(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<ResolveDepthFn>(resolveDepth));
if (previousDepth < itemDepth) {
return previousOffset;
}
}
return kInvalidUIFlatHierarchyItemOffset;
}
template <typename ResolveDepthFn, typename IsExpandedFn>
bool UIFlatHierarchyIsVisible(
std::span<const std::size_t> itemIndices,
std::size_t itemOffset,
ResolveDepthFn&& resolveDepth,
IsExpandedFn&& isExpanded) {
if (itemOffset >= itemIndices.size()) {
return false;
}
float requiredAncestorDepth = Detail::ResolveUIFlatHierarchyDepth(
itemIndices,
itemOffset,
std::forward<ResolveDepthFn>(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<ResolveDepthFn>(resolveDepth));
if (previousDepth < requiredAncestorDepth) {
if (!isExpanded(itemIndices[previousOffset])) {
return false;
}
requiredAncestorDepth = previousDepth;
}
}
return true;
}
template <typename ResolveDepthFn, typename IsVisibleFn>
std::size_t UIFlatHierarchyFindFirstVisibleChildOffset(
std::span<const std::size_t> itemIndices,
std::size_t itemOffset,
ResolveDepthFn&& resolveDepth,
IsVisibleFn&& isVisible) {
if (!UIFlatHierarchyHasChildren(
itemIndices,
itemOffset,
std::forward<ResolveDepthFn>(resolveDepth))) {
return kInvalidUIFlatHierarchyItemOffset;
}
const float itemDepth = Detail::ResolveUIFlatHierarchyDepth(
itemIndices,
itemOffset,
std::forward<ResolveDepthFn>(resolveDepth));
for (std::size_t candidateOffset = itemOffset + 1u;
candidateOffset < itemIndices.size();
++candidateOffset) {
const float candidateDepth = Detail::ResolveUIFlatHierarchyDepth(
itemIndices,
candidateOffset,
std::forward<ResolveDepthFn>(resolveDepth));
if (candidateDepth <= itemDepth) {
break;
}
if (isVisible(itemIndices[candidateOffset])) {
return candidateOffset;
}
}
return kInvalidUIFlatHierarchyItemOffset;
}
} // namespace Widgets
} // namespace UI
} // namespace XCEngine

View File

@@ -44,6 +44,7 @@ endif()
set(NEW_EDITOR_SOURCES set(NEW_EDITOR_SOURCES
src/main.cpp src/main.cpp
src/Application.cpp src/Application.cpp
src/ApplicationDefaultShell.cpp
src/panels/Panel.cpp src/panels/Panel.cpp
src/Rendering/MainWindowBackdropPass.cpp src/Rendering/MainWindowBackdropPass.cpp
src/Rendering/MainWindowNativeBackdropRenderer.cpp src/Rendering/MainWindowNativeBackdropRenderer.cpp
@@ -156,7 +157,6 @@ target_link_libraries(XCNewEditorImGuiCompat PRIVATE
target_link_libraries(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE
XCEngine XCEngine
XCNewEditorImGuiCompat
d3d12.lib d3d12.lib
dxgi.lib dxgi.lib
user32 user32

View File

@@ -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

View File

@@ -10,6 +10,7 @@
#include <XCEngine/UI/Types.h> #include <XCEngine/UI/Types.h>
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h> #include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h> #include <XCEngine/UI/Widgets/UIExpansionModel.h>
#include <XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h>
#include <XCEngine/UI/Widgets/UIKeyboardNavigationModel.h> #include <XCEngine/UI/Widgets/UIKeyboardNavigationModel.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h> #include <XCEngine/UI/Widgets/UISelectionModel.h>
@@ -556,23 +557,17 @@ bool HasTreeItemChildren(const RuntimeBuildContext& state, std::size_t nodeIndex
return false; return false;
} }
const float indentLevel = ResolveTreeIndentLevel(node);
const auto siblingIt = std::find(parent.children.begin(), parent.children.end(), nodeIndex); const auto siblingIt = std::find(parent.children.begin(), parent.children.end(), nodeIndex);
if (siblingIt == parent.children.end()) { if (siblingIt == parent.children.end()) {
return false; return false;
} }
for (auto it = siblingIt + 1; it != parent.children.end(); ++it) { return UIWidgets::UIFlatHierarchyHasChildren(
const LayoutNode& sibling = state.nodes[*it]; std::span<const std::size_t>(parent.children),
const float siblingIndentLevel = ResolveTreeIndentLevel(sibling); static_cast<std::size_t>(siblingIt - parent.children.begin()),
if (siblingIndentLevel <= indentLevel) { [&state](std::size_t siblingIndex) {
break; return ResolveTreeIndentLevel(state.nodes[siblingIndex]);
} });
return true;
}
return false;
} }
bool IsNodeCollapsible(const RuntimeBuildContext& state, std::size_t nodeIndex) { bool IsNodeCollapsible(const RuntimeBuildContext& state, std::size_t nodeIndex) {
@@ -626,32 +621,20 @@ bool IsNodeVisible(const RuntimeBuildContext& state, std::size_t nodeIndex) {
return true; return true;
} }
float requiredAncestorIndent = ResolveTreeIndentLevel(node);
if (requiredAncestorIndent <= 0.0f) {
return true;
}
const auto siblingIt = std::find(parent.children.begin(), parent.children.end(), nodeIndex); const auto siblingIt = std::find(parent.children.begin(), parent.children.end(), nodeIndex);
if (siblingIt == parent.children.end()) { if (siblingIt == parent.children.end()) {
return true; return true;
} }
const std::size_t siblingOffset = return UIWidgets::UIFlatHierarchyIsVisible(
static_cast<std::size_t>(siblingIt - parent.children.begin()); std::span<const std::size_t>(parent.children),
for (std::size_t offset = siblingOffset; offset > 0u && requiredAncestorIndent > 0.0f; --offset) { static_cast<std::size_t>(siblingIt - parent.children.begin()),
const std::size_t siblingIndex = parent.children[offset - 1u]; [&state](std::size_t siblingIndex) {
const LayoutNode& sibling = state.nodes[siblingIndex]; return ResolveTreeIndentLevel(state.nodes[siblingIndex]);
const float siblingIndent = ResolveTreeIndentLevel(sibling); },
if (siblingIndent < requiredAncestorIndent) { [&state](std::size_t siblingIndex) {
if (!IsNodeExpanded(state, siblingIndex)) { return IsNodeExpanded(state, siblingIndex);
return false; });
}
requiredAncestorIndent = siblingIndent;
}
}
return true;
} }
std::vector<std::size_t> CollectVisibleChildren( std::vector<std::size_t> CollectVisibleChildren(
@@ -961,14 +944,17 @@ std::size_t FindTreeParentItemIndex(
return kInvalidIndex; return kInvalidIndex;
} }
for (auto it = siblingIt; it != treeView.children.begin();) { const std::size_t parentOffset = UIWidgets::UIFlatHierarchyFindParentOffset(
--it; std::span<const std::size_t>(treeView.children),
if (ResolveTreeIndentLevel(state.nodes[*it]) < indentLevel) { static_cast<std::size_t>(siblingIt - treeView.children.begin()),
return *it; [&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( std::size_t FindFirstTreeChildItemIndex(
@@ -981,27 +967,27 @@ std::size_t FindFirstTreeChildItemIndex(
const LayoutNode& node = state.nodes[nodeIndex]; const LayoutNode& node = state.nodes[nodeIndex];
const LayoutNode& treeView = state.nodes[node.parentIndex]; const LayoutNode& treeView = state.nodes[node.parentIndex];
const float indentLevel = ResolveTreeIndentLevel(node);
const auto siblingIt = std::find(treeView.children.begin(), treeView.children.end(), nodeIndex); const auto siblingIt = std::find(treeView.children.begin(), treeView.children.end(), nodeIndex);
if (siblingIt == treeView.children.end()) { if (siblingIt == treeView.children.end()) {
return kInvalidIndex; return kInvalidIndex;
} }
for (auto it = siblingIt + 1; it != treeView.children.end(); ++it) { const std::size_t childOffset = UIWidgets::UIFlatHierarchyFindFirstVisibleChildOffset(
const std::size_t candidateIndex = *it; std::span<const std::size_t>(treeView.children),
const float candidateIndent = ResolveTreeIndentLevel(state.nodes[candidateIndex]); static_cast<std::size_t>(siblingIt - treeView.children.begin()),
if (candidateIndent <= indentLevel) { [&state](std::size_t siblingIndex) {
break; return ResolveTreeIndentLevel(state.nodes[siblingIndex]);
} },
[&state](std::size_t siblingIndex) {
if (IsNodeVisible(state, candidateIndex)) { return IsNodeVisible(state, siblingIndex);
return candidateIndex; });
} if (childOffset == UIWidgets::kInvalidUIFlatHierarchyItemOffset) {
}
return kInvalidIndex; return kInvalidIndex;
} }
return treeView.children[childOffset];
}
bool HandleKeyboardExpand(RuntimeBuildContext& state) { bool HandleKeyboardExpand(RuntimeBuildContext& state) {
const std::size_t selectedIndex = FindNodeIndexById( const std::size_t selectedIndex = FindNodeIndexById(
state, state,

View File

@@ -0,0 +1,227 @@
#pragma once
#include "XCUIShellChromeState.h"
#include <XCEngine/UI/Types.h>
#include <algorithm>
#include <cstddef>
#include <initializer_list>
#include <string>
#include <string_view>
#include <vector>
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<XCUINativeShellPanelLayout> 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<XCUINativeShellPanelLayout>& 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<XCUINativeShellPanelSpec> 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<XCUINativeShellPanelSpec> 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

View File

@@ -6,6 +6,7 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string>
#include <string_view> #include <string_view>
namespace XCEngine { namespace XCEngine {
@@ -14,13 +15,15 @@ namespace XCUIBackend {
enum class XCUIPanelCanvasHostBackend : std::uint8_t { enum class XCUIPanelCanvasHostBackend : std::uint8_t {
Null = 0, Null = 0,
ImGui ImGui,
Native
}; };
struct XCUIPanelCanvasHostCapabilities { struct XCUIPanelCanvasHostCapabilities {
bool supportsPointerHitTesting = false; bool supportsPointerHitTesting = false;
bool supportsHostedSurfaceImages = false; bool supportsHostedSurfaceImages = false;
bool supportsPrimitiveOverlays = false; bool supportsPrimitiveOverlays = false;
bool supportsExternallyDrivenSession = false;
}; };
struct XCUIPanelCanvasRequest { struct XCUIPanelCanvasRequest {
@@ -46,6 +49,20 @@ struct XCUIPanelCanvasSession {
bool windowFocused = false; 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( inline const char* ResolveXCUIPanelCanvasChildId(
const XCUIPanelCanvasRequest& request, const XCUIPanelCanvasRequest& request,
const char* fallback = "XCUIPanelCanvasHost") { const char* fallback = "XCUIPanelCanvasHost") {
@@ -99,6 +116,10 @@ public:
const ::XCEngine::UI::UIColor& color, const ::XCEngine::UI::UIColor& color,
float fontSize = 0.0f) = 0; float fontSize = 0.0f) = 0;
virtual void EndCanvas() = 0; virtual void EndCanvas() = 0;
virtual bool TryGetLatestFrameSnapshot(XCUIPanelCanvasFrameSnapshot& outSnapshot) const {
outSnapshot = {};
return false;
}
}; };
std::unique_ptr<IXCUIPanelCanvasHost> CreateNullXCUIPanelCanvasHost(); std::unique_ptr<IXCUIPanelCanvasHost> CreateNullXCUIPanelCanvasHost();

View File

@@ -5,7 +5,9 @@
set(UI_TEST_SOURCES set(UI_TEST_SOURCES
test_ui_core.cpp test_ui_core.cpp
test_ui_editor_collection_primitives.cpp test_ui_editor_collection_primitives.cpp
test_ui_editor_panel_chrome.cpp
test_ui_expansion_model.cpp test_ui_expansion_model.cpp
test_ui_flat_hierarchy_helpers.cpp
test_ui_keyboard_navigation_model.cpp test_ui_keyboard_navigation_model.cpp
test_ui_property_edit_model.cpp test_ui_property_edit_model.cpp
test_layout_engine.cpp test_layout_engine.cpp

View File

@@ -0,0 +1,126 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Widgets/UIEditorPanelChrome.h>
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

View File

@@ -0,0 +1,145 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h>
#include <array>
#include <unordered_set>
#include <vector>
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<FlatHierarchyItem, 5> items = {{
{ 0.0f },
{ 1.0f },
{ 2.0f },
{ 1.0f },
{ 0.0f },
}};
const std::vector<std::size_t> 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<FlatHierarchyItem, 4> items = {{
{ 0.0f },
{ 1.0f },
{ 2.0f },
{ 1.0f },
}};
const std::vector<std::size_t> itemIndices = { 0u, 1u, 2u, 3u };
const auto resolveDepth = [&items](std::size_t itemIndex) {
return items[itemIndex].depth;
};
std::unordered_set<std::size_t> 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<FlatHierarchyItem, 4> items = {{
{ 0.0f },
{ 1.0f },
{ 2.0f },
{ 1.0f },
}};
const std::vector<std::size_t> itemIndices = { 0u, 1u, 2u, 3u };
const auto resolveDepth = [&items](std::size_t itemIndex) {
return items[itemIndex].depth;
};
std::unordered_set<std::size_t> 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<FlatHierarchyItem, 3> items = {{
{ -3.0f },
{ 1.0f },
{ -1.0f },
}};
const std::vector<std::size_t> 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

View File

@@ -0,0 +1,133 @@
#include "XCUIBackend/XCUINativeShellLayout.h"
#include <gtest/gtest.h>
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