Unlink XCNewEditor default shell from ImGui compat slice
This commit is contained in:
@@ -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:
|
||||
|
||||
126
engine/include/XCEngine/UI/Widgets/UIEditorPanelChrome.h
Normal file
126
engine/include/XCEngine/UI/Widgets/UIEditorPanelChrome.h
Normal 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
|
||||
167
engine/include/XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h
Normal file
167
engine/include/XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
138
new_editor/src/ApplicationDefaultShell.cpp
Normal file
138
new_editor/src/ApplicationDefaultShell.cpp
Normal 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
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <XCEngine/UI/Types.h>
|
||||
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
#include <XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h>
|
||||
#include <XCEngine/UI/Widgets/UIKeyboardNavigationModel.h>
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
|
||||
@@ -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<const std::size_t>(parent.children),
|
||||
static_cast<std::size_t>(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<std::size_t>(siblingIt - parent.children.begin());
|
||||
for (std::size_t offset = siblingOffset; offset > 0u && requiredAncestorIndent > 0.0f; --offset) {
|
||||
const std::size_t siblingIndex = parent.children[offset - 1u];
|
||||
const LayoutNode& sibling = state.nodes[siblingIndex];
|
||||
const float siblingIndent = ResolveTreeIndentLevel(sibling);
|
||||
if (siblingIndent < requiredAncestorIndent) {
|
||||
if (!IsNodeExpanded(state, siblingIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
requiredAncestorIndent = siblingIndent;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return UIWidgets::UIFlatHierarchyIsVisible(
|
||||
std::span<const std::size_t>(parent.children),
|
||||
static_cast<std::size_t>(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<std::size_t> 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<const std::size_t>(treeView.children),
|
||||
static_cast<std::size_t>(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<const std::size_t>(treeView.children),
|
||||
static_cast<std::size_t>(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) {
|
||||
|
||||
227
new_editor/src/XCUIBackend/XCUINativeShellLayout.h
Normal file
227
new_editor/src/XCUIBackend/XCUINativeShellLayout.h
Normal 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
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
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<IXCUIPanelCanvasHost> CreateNullXCUIPanelCanvasHost();
|
||||
|
||||
@@ -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
|
||||
|
||||
126
tests/Core/UI/test_ui_editor_panel_chrome.cpp
Normal file
126
tests/Core/UI/test_ui_editor_panel_chrome.cpp
Normal 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
|
||||
145
tests/Core/UI/test_ui_flat_hierarchy_helpers.cpp
Normal file
145
tests/Core/UI/test_ui_flat_hierarchy_helpers.cpp
Normal 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
|
||||
133
tests/NewEditor/test_xcui_native_shell_layout.cpp
Normal file
133
tests/NewEditor/test_xcui_native_shell_layout.cpp
Normal 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
|
||||
Reference in New Issue
Block a user