Build XCUI splitter foundation and test harness
This commit is contained in:
@@ -16,7 +16,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
option(XCENGINE_ENABLE_MONO_SCRIPTING "Build the Mono-based C# scripting runtime" ON)
|
option(XCENGINE_ENABLE_MONO_SCRIPTING "Build the Mono-based C# scripting runtime" ON)
|
||||||
option(XCENGINE_BUILD_NEW_EDITOR "Build the experimental new_editor skeleton" ON)
|
option(XCENGINE_BUILD_NEW_EDITOR "Build the XCUI new_editor shell app" ON)
|
||||||
set(
|
set(
|
||||||
XCENGINE_MONO_ROOT_DIR
|
XCENGINE_MONO_ROOT_DIR
|
||||||
"${CMAKE_SOURCE_DIR}/参考/Fermion/Fermion/external/mono"
|
"${CMAKE_SOURCE_DIR}/参考/Fermion/Fermion/external/mono"
|
||||||
@@ -25,9 +25,7 @@ set(
|
|||||||
|
|
||||||
add_subdirectory(engine)
|
add_subdirectory(engine)
|
||||||
add_subdirectory(editor)
|
add_subdirectory(editor)
|
||||||
if(XCENGINE_BUILD_NEW_EDITOR)
|
add_subdirectory(new_editor)
|
||||||
add_subdirectory(new_editor)
|
|
||||||
endif()
|
|
||||||
add_subdirectory(managed)
|
add_subdirectory(managed)
|
||||||
add_subdirectory(mvs/RenderDoc)
|
add_subdirectory(mvs/RenderDoc)
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
|
|||||||
@@ -533,12 +533,10 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Text/UITextInputController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Text/UITextInputController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextEditing.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextEditing.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextInputController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextInputController.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIExpansionModel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIExpansionModel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIKeyboardNavigationModel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIKeyboardNavigationModel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIPropertyEditModel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIPropertyEditModel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UISelectionModel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UISelectionModel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIEditorCollectionPrimitives.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIExpansionModel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIExpansionModel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIKeyboardNavigationModel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIKeyboardNavigationModel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIPropertyEditModel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIPropertyEditModel.cpp
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ struct UIInputDispatcherOptions {
|
|||||||
struct UIInputDispatchSummary {
|
struct UIInputDispatchSummary {
|
||||||
UIFocusChange focusChange = {};
|
UIFocusChange focusChange = {};
|
||||||
UIInputDispatchResult routing = {};
|
UIInputDispatchResult routing = {};
|
||||||
|
bool shortcutMatched = false;
|
||||||
bool shortcutHandled = false;
|
bool shortcutHandled = false;
|
||||||
|
bool shortcutSuppressed = false;
|
||||||
|
UIShortcutScope shortcutScope = UIShortcutScope::Global;
|
||||||
|
UIElementId shortcutOwnerId = 0;
|
||||||
std::string commandId = {};
|
std::string commandId = {};
|
||||||
|
|
||||||
bool Handled() const {
|
bool Handled() const {
|
||||||
@@ -50,6 +54,14 @@ public:
|
|||||||
return m_shortcutRegistry;
|
return m_shortcutRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetShortcutContext(const UIShortcutContext& context) {
|
||||||
|
m_shortcutContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIShortcutContext& GetShortcutContext() const {
|
||||||
|
return m_shortcutContext;
|
||||||
|
}
|
||||||
|
|
||||||
template <typename HandlerFn>
|
template <typename HandlerFn>
|
||||||
UIInputDispatchSummary Dispatch(
|
UIInputDispatchSummary Dispatch(
|
||||||
const UIInputEvent& event,
|
const UIInputEvent& event,
|
||||||
@@ -66,15 +78,23 @@ public:
|
|||||||
m_focusController.SetActivePath(capturePath.Empty() ? hoveredPath : capturePath);
|
m_focusController.SetActivePath(capturePath.Empty() ? hoveredPath : capturePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const UIShortcutContext shortcutContext = {
|
if (ShouldStartActivePathOnKeyDown(event)) {
|
||||||
m_focusController.GetFocusedPath(),
|
m_focusController.SetActivePath(m_focusController.GetFocusedPath());
|
||||||
m_focusController.GetActivePath(),
|
}
|
||||||
hoveredPath };
|
|
||||||
|
const UIShortcutContext shortcutContext = BuildEffectiveShortcutContext(hoveredPath);
|
||||||
const UIShortcutMatch shortcutMatch = m_shortcutRegistry.Match(event, shortcutContext);
|
const UIShortcutMatch shortcutMatch = m_shortcutRegistry.Match(event, shortcutContext);
|
||||||
if (shortcutMatch.matched) {
|
if (shortcutMatch.matched) {
|
||||||
summary.shortcutHandled = true;
|
summary.shortcutMatched = true;
|
||||||
summary.commandId = shortcutMatch.binding.commandId;
|
summary.commandId = shortcutMatch.binding.commandId;
|
||||||
return FinalizeDispatch(event, std::move(summary));
|
summary.shortcutScope = shortcutMatch.binding.scope;
|
||||||
|
summary.shortcutOwnerId = shortcutMatch.binding.ownerId;
|
||||||
|
if (ShouldSuppressShortcutMatch(event, shortcutMatch, shortcutContext)) {
|
||||||
|
summary.shortcutSuppressed = true;
|
||||||
|
} else {
|
||||||
|
summary.shortcutHandled = true;
|
||||||
|
return FinalizeDispatch(event, std::move(summary));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UIInputRouteContext routeContext = {};
|
UIInputRouteContext routeContext = {};
|
||||||
@@ -97,6 +117,20 @@ private:
|
|||||||
const UIInputEvent& event,
|
const UIInputEvent& event,
|
||||||
const UIInputPath& hoveredPath) const;
|
const UIInputPath& hoveredPath) const;
|
||||||
|
|
||||||
|
bool ShouldStartActivePathOnKeyDown(
|
||||||
|
const UIInputEvent& event) const;
|
||||||
|
|
||||||
|
bool ShouldClearActivePathOnKeyUp(
|
||||||
|
const UIInputEvent& event) const;
|
||||||
|
|
||||||
|
UIShortcutContext BuildEffectiveShortcutContext(
|
||||||
|
const UIInputPath& hoveredPath) const;
|
||||||
|
|
||||||
|
bool ShouldSuppressShortcutMatch(
|
||||||
|
const UIInputEvent& event,
|
||||||
|
const UIShortcutMatch& shortcutMatch,
|
||||||
|
const UIShortcutContext& shortcutContext) const;
|
||||||
|
|
||||||
UIInputDispatchSummary FinalizeDispatch(
|
UIInputDispatchSummary FinalizeDispatch(
|
||||||
const UIInputEvent& event,
|
const UIInputEvent& event,
|
||||||
UIInputDispatchSummary&& summary);
|
UIInputDispatchSummary&& summary);
|
||||||
@@ -104,6 +138,7 @@ private:
|
|||||||
UIInputDispatcherOptions m_options = {};
|
UIInputDispatcherOptions m_options = {};
|
||||||
UIFocusController m_focusController = {};
|
UIFocusController m_focusController = {};
|
||||||
UIShortcutRegistry m_shortcutRegistry = {};
|
UIShortcutRegistry m_shortcutRegistry = {};
|
||||||
|
UIShortcutContext m_shortcutContext = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UI
|
} // namespace UI
|
||||||
|
|||||||
@@ -33,10 +33,19 @@ struct UIShortcutBinding {
|
|||||||
std::string commandId = {};
|
std::string commandId = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct UIShortcutScopeChain {
|
||||||
|
UIInputPath path = {};
|
||||||
|
UIElementId windowId = 0;
|
||||||
|
UIElementId panelId = 0;
|
||||||
|
UIElementId widgetId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct UIShortcutContext {
|
struct UIShortcutContext {
|
||||||
UIInputPath focusedPath = {};
|
UIInputPath focusedPath = {};
|
||||||
UIInputPath activePath = {};
|
UIInputPath activePath = {};
|
||||||
UIInputPath hoveredPath = {};
|
UIInputPath hoveredPath = {};
|
||||||
|
UIShortcutScopeChain commandScope = {};
|
||||||
|
bool textInputActive = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UIShortcutMatch {
|
struct UIShortcutMatch {
|
||||||
|
|||||||
275
engine/include/XCEngine/UI/Layout/UISplitterLayout.h
Normal file
275
engine/include/XCEngine/UI/Layout/UISplitterLayout.h
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Layout/LayoutTypes.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Layout {
|
||||||
|
|
||||||
|
struct UISplitterMetrics {
|
||||||
|
float thickness = 8.0f;
|
||||||
|
float hitThickness = 12.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UISplitterConstraints {
|
||||||
|
float primaryMin = 0.0f;
|
||||||
|
float primaryMax = GetUnboundedLayoutExtent();
|
||||||
|
float secondaryMin = 0.0f;
|
||||||
|
float secondaryMax = GetUnboundedLayoutExtent();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UISplitterLayoutOptions {
|
||||||
|
UILayoutAxis axis = UILayoutAxis::Horizontal;
|
||||||
|
float ratio = 0.5f;
|
||||||
|
float handleThickness = 8.0f;
|
||||||
|
float minPrimaryExtent = 0.0f;
|
||||||
|
float minSecondaryExtent = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UISplitterLayoutResult {
|
||||||
|
UIRect primaryRect = {};
|
||||||
|
UIRect handleRect = {};
|
||||||
|
UIRect secondaryRect = {};
|
||||||
|
float resolvedRatio = 0.5f;
|
||||||
|
float splitRatio = 0.5f;
|
||||||
|
float primaryExtent = 0.0f;
|
||||||
|
float secondaryExtent = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace SplitterDetail {
|
||||||
|
|
||||||
|
inline float ClampSplitterExtent(float value) {
|
||||||
|
return (std::max)(0.0f, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float ClampFiniteExtent(float value, float minValue, float maxValue) {
|
||||||
|
return (std::clamp)(value, minValue, maxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float GetMainExtent(const UISize& size, UILayoutAxis axis) {
|
||||||
|
return axis == UILayoutAxis::Horizontal ? size.width : size.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float GetCrossExtent(const UISize& size, UILayoutAxis axis) {
|
||||||
|
return axis == UILayoutAxis::Horizontal ? size.height : size.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float GetMainExtent(const UIRect& rect, UILayoutAxis axis) {
|
||||||
|
return axis == UILayoutAxis::Horizontal ? rect.width : rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float GetCrossExtent(const UIRect& rect, UILayoutAxis axis) {
|
||||||
|
return axis == UILayoutAxis::Horizontal ? rect.height : rect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace SplitterDetail
|
||||||
|
|
||||||
|
inline UISize MeasureSplitterDesiredSize(
|
||||||
|
UILayoutAxis axis,
|
||||||
|
const UISize& primarySize,
|
||||||
|
const UISize& secondarySize,
|
||||||
|
float handleThickness) {
|
||||||
|
const float clampedHandleThickness = SplitterDetail::ClampSplitterExtent(handleThickness);
|
||||||
|
if (axis == UILayoutAxis::Horizontal) {
|
||||||
|
return UISize(
|
||||||
|
primarySize.width + clampedHandleThickness + secondarySize.width,
|
||||||
|
(std::max)(primarySize.height, secondarySize.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
return UISize(
|
||||||
|
(std::max)(primarySize.width, secondarySize.width),
|
||||||
|
primarySize.height + clampedHandleThickness + secondarySize.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float ClampSplitterRatio(
|
||||||
|
const UISplitterLayoutOptions& options,
|
||||||
|
float totalMainExtent) {
|
||||||
|
const float mainExtent = SplitterDetail::ClampSplitterExtent(totalMainExtent);
|
||||||
|
const float handleThickness = (std::min)(
|
||||||
|
SplitterDetail::ClampSplitterExtent(options.handleThickness),
|
||||||
|
mainExtent);
|
||||||
|
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
|
||||||
|
if (usableExtent <= 0.0f) {
|
||||||
|
return 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float requestedPrimaryExtent =
|
||||||
|
usableExtent * (std::clamp)(options.ratio, 0.0f, 1.0f);
|
||||||
|
const float minPrimaryExtent = (std::min)(
|
||||||
|
SplitterDetail::ClampSplitterExtent(options.minPrimaryExtent),
|
||||||
|
usableExtent);
|
||||||
|
const float minSecondaryExtent = (std::min)(
|
||||||
|
SplitterDetail::ClampSplitterExtent(options.minSecondaryExtent),
|
||||||
|
usableExtent);
|
||||||
|
|
||||||
|
float minimumPrimaryExtent = minPrimaryExtent;
|
||||||
|
float maximumPrimaryExtent = usableExtent - minSecondaryExtent;
|
||||||
|
if (minimumPrimaryExtent > maximumPrimaryExtent) {
|
||||||
|
minimumPrimaryExtent = 0.0f;
|
||||||
|
maximumPrimaryExtent = usableExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float clampedPrimaryExtent = (std::clamp)(
|
||||||
|
requestedPrimaryExtent,
|
||||||
|
minimumPrimaryExtent,
|
||||||
|
maximumPrimaryExtent);
|
||||||
|
return clampedPrimaryExtent / usableExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float ClampSplitterRatio(
|
||||||
|
UILayoutAxis axis,
|
||||||
|
float requestedRatio,
|
||||||
|
float totalMainExtent,
|
||||||
|
const UISplitterConstraints& constraints,
|
||||||
|
const UISplitterMetrics& metrics) {
|
||||||
|
UISplitterLayoutOptions options = {};
|
||||||
|
options.axis = axis;
|
||||||
|
options.ratio = requestedRatio;
|
||||||
|
options.handleThickness = metrics.thickness;
|
||||||
|
options.minPrimaryExtent = constraints.primaryMin;
|
||||||
|
options.minSecondaryExtent = constraints.secondaryMin;
|
||||||
|
|
||||||
|
const float mainExtent = SplitterDetail::ClampSplitterExtent(totalMainExtent);
|
||||||
|
const float handleThickness = (std::min)(
|
||||||
|
SplitterDetail::ClampSplitterExtent(metrics.thickness),
|
||||||
|
mainExtent);
|
||||||
|
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
|
||||||
|
if (usableExtent <= 0.0f) {
|
||||||
|
return 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float requestedPrimaryExtent =
|
||||||
|
usableExtent * (std::clamp)(requestedRatio, 0.0f, 1.0f);
|
||||||
|
float minimumPrimaryExtent = (std::min)(
|
||||||
|
SplitterDetail::ClampSplitterExtent(constraints.primaryMin),
|
||||||
|
usableExtent);
|
||||||
|
float maximumPrimaryExtent = std::isfinite(constraints.primaryMax)
|
||||||
|
? (std::clamp)(constraints.primaryMax, minimumPrimaryExtent, usableExtent)
|
||||||
|
: usableExtent;
|
||||||
|
|
||||||
|
const float minimumFromSecondary = (std::max)(
|
||||||
|
0.0f,
|
||||||
|
usableExtent - (std::isfinite(constraints.secondaryMax)
|
||||||
|
? (std::max)(SplitterDetail::ClampSplitterExtent(constraints.secondaryMax), SplitterDetail::ClampSplitterExtent(constraints.secondaryMin))
|
||||||
|
: usableExtent));
|
||||||
|
const float maximumFromSecondary = (std::max)(
|
||||||
|
0.0f,
|
||||||
|
usableExtent - (std::min)(SplitterDetail::ClampSplitterExtent(constraints.secondaryMin), usableExtent));
|
||||||
|
|
||||||
|
minimumPrimaryExtent = (std::max)(minimumPrimaryExtent, minimumFromSecondary);
|
||||||
|
maximumPrimaryExtent = (std::min)(maximumPrimaryExtent, maximumFromSecondary);
|
||||||
|
if (minimumPrimaryExtent > maximumPrimaryExtent) {
|
||||||
|
minimumPrimaryExtent = 0.0f;
|
||||||
|
maximumPrimaryExtent = usableExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float clampedPrimaryExtent = (std::clamp)(
|
||||||
|
requestedPrimaryExtent,
|
||||||
|
minimumPrimaryExtent,
|
||||||
|
maximumPrimaryExtent);
|
||||||
|
return clampedPrimaryExtent / usableExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline UISplitterLayoutResult ArrangeSplitterLayout(
|
||||||
|
const UISplitterLayoutOptions& options,
|
||||||
|
const UIRect& bounds) {
|
||||||
|
UISplitterLayoutResult result = {};
|
||||||
|
|
||||||
|
const float mainExtent = SplitterDetail::GetMainExtent(bounds, options.axis);
|
||||||
|
const float crossExtent = SplitterDetail::GetCrossExtent(bounds, options.axis);
|
||||||
|
const float handleThickness = (std::min)(
|
||||||
|
SplitterDetail::ClampSplitterExtent(options.handleThickness),
|
||||||
|
mainExtent);
|
||||||
|
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
|
||||||
|
|
||||||
|
result.resolvedRatio = ClampSplitterRatio(options, mainExtent);
|
||||||
|
result.primaryExtent = usableExtent * result.resolvedRatio;
|
||||||
|
result.secondaryExtent = (std::max)(0.0f, usableExtent - result.primaryExtent);
|
||||||
|
|
||||||
|
if (options.axis == UILayoutAxis::Horizontal) {
|
||||||
|
result.primaryRect = UIRect(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y,
|
||||||
|
result.primaryExtent,
|
||||||
|
crossExtent);
|
||||||
|
result.handleRect = UIRect(
|
||||||
|
bounds.x + result.primaryExtent,
|
||||||
|
bounds.y,
|
||||||
|
handleThickness,
|
||||||
|
crossExtent);
|
||||||
|
result.secondaryRect = UIRect(
|
||||||
|
result.handleRect.x + handleThickness,
|
||||||
|
bounds.y,
|
||||||
|
result.secondaryExtent,
|
||||||
|
crossExtent);
|
||||||
|
} else {
|
||||||
|
result.primaryRect = UIRect(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y,
|
||||||
|
crossExtent,
|
||||||
|
result.primaryExtent);
|
||||||
|
result.handleRect = UIRect(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y + result.primaryExtent,
|
||||||
|
crossExtent,
|
||||||
|
handleThickness);
|
||||||
|
result.secondaryRect = UIRect(
|
||||||
|
bounds.x,
|
||||||
|
result.handleRect.y + handleThickness,
|
||||||
|
crossExtent,
|
||||||
|
result.secondaryExtent);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.splitRatio = result.resolvedRatio;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float ResolveSplitterRatioFromPointerPosition(
|
||||||
|
const UISplitterLayoutOptions& options,
|
||||||
|
const UIRect& bounds,
|
||||||
|
float pointerMainPosition) {
|
||||||
|
const float mainExtent = SplitterDetail::GetMainExtent(bounds, options.axis);
|
||||||
|
const float handleThickness = (std::min)(
|
||||||
|
SplitterDetail::ClampSplitterExtent(options.handleThickness),
|
||||||
|
mainExtent);
|
||||||
|
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
|
||||||
|
if (usableExtent <= 0.0f) {
|
||||||
|
return 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float origin = options.axis == UILayoutAxis::Horizontal ? bounds.x : bounds.y;
|
||||||
|
UISplitterLayoutOptions pointerOptions = options;
|
||||||
|
pointerOptions.ratio = (pointerMainPosition - origin - handleThickness * 0.5f) / usableExtent;
|
||||||
|
return ClampSplitterRatio(pointerOptions, mainExtent);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline UISplitterLayoutResult ArrangeUISplitter(
|
||||||
|
const UIRect& bounds,
|
||||||
|
UILayoutAxis axis,
|
||||||
|
float requestedRatio,
|
||||||
|
const UISplitterConstraints& constraints,
|
||||||
|
const UISplitterMetrics& metrics) {
|
||||||
|
UISplitterLayoutOptions options = {};
|
||||||
|
options.axis = axis;
|
||||||
|
options.ratio = ClampSplitterRatio(
|
||||||
|
axis,
|
||||||
|
requestedRatio,
|
||||||
|
SplitterDetail::GetMainExtent(bounds, axis),
|
||||||
|
constraints,
|
||||||
|
metrics);
|
||||||
|
options.handleThickness = metrics.thickness;
|
||||||
|
options.minPrimaryExtent = constraints.primaryMin;
|
||||||
|
options.minSecondaryExtent = constraints.secondaryMin;
|
||||||
|
|
||||||
|
UISplitterLayoutResult result = ArrangeSplitterLayout(options, bounds);
|
||||||
|
result.splitRatio = options.ratio;
|
||||||
|
result.resolvedRatio = options.ratio;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Layout
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||||||
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
|
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
|
||||||
|
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -27,13 +28,28 @@ public:
|
|||||||
std::uint64_t totalPointerEventCount = 0u;
|
std::uint64_t totalPointerEventCount = 0u;
|
||||||
UIPoint pointerPosition = {};
|
UIPoint pointerPosition = {};
|
||||||
bool pointerInsideViewport = false;
|
bool pointerInsideViewport = false;
|
||||||
|
bool textInputActive = false;
|
||||||
std::string hoveredStateKey = {};
|
std::string hoveredStateKey = {};
|
||||||
std::string focusedStateKey = {};
|
std::string focusedStateKey = {};
|
||||||
std::string activeStateKey = {};
|
std::string activeStateKey = {};
|
||||||
std::string captureStateKey = {};
|
std::string captureStateKey = {};
|
||||||
|
std::string commandScopeStateKey = {};
|
||||||
|
std::string windowScopeStateKey = {};
|
||||||
|
std::string panelScopeStateKey = {};
|
||||||
|
std::string widgetScopeStateKey = {};
|
||||||
std::string lastEventType = {};
|
std::string lastEventType = {};
|
||||||
std::string lastTargetStateKey = {};
|
std::string lastTargetStateKey = {};
|
||||||
std::string lastTargetKind = {};
|
std::string lastTargetKind = {};
|
||||||
|
std::string lastShortcutCommandId = {};
|
||||||
|
std::string lastShortcutScope = {};
|
||||||
|
std::string lastShortcutOwnerStateKey = {};
|
||||||
|
bool lastShortcutHandled = false;
|
||||||
|
bool lastShortcutSuppressed = false;
|
||||||
|
std::string recentShortcutCommandId = {};
|
||||||
|
std::string recentShortcutScope = {};
|
||||||
|
std::string recentShortcutOwnerStateKey = {};
|
||||||
|
bool recentShortcutHandled = false;
|
||||||
|
bool recentShortcutSuppressed = false;
|
||||||
std::string lastResult = {};
|
std::string lastResult = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +76,11 @@ public:
|
|||||||
const InputDebugSnapshot& GetInputDebugSnapshot() const;
|
const InputDebugSnapshot& GetInputDebugSnapshot() const;
|
||||||
const ScrollDebugSnapshot& GetScrollDebugSnapshot() const;
|
const ScrollDebugSnapshot& GetScrollDebugSnapshot() const;
|
||||||
|
|
||||||
|
struct SplitterDragRuntimeState {
|
||||||
|
std::string stateKey = {};
|
||||||
|
Widgets::UISplitterDragState drag = {};
|
||||||
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct PointerState {
|
struct PointerState {
|
||||||
UIPoint position = {};
|
UIPoint position = {};
|
||||||
@@ -69,7 +90,9 @@ private:
|
|||||||
|
|
||||||
UIInputDispatcher m_inputDispatcher;
|
UIInputDispatcher m_inputDispatcher;
|
||||||
std::unordered_map<std::string, float> m_verticalScrollOffsets = {};
|
std::unordered_map<std::string, float> m_verticalScrollOffsets = {};
|
||||||
|
std::unordered_map<std::string, float> m_splitterRatios = {};
|
||||||
PointerState m_pointerState = {};
|
PointerState m_pointerState = {};
|
||||||
|
SplitterDragRuntimeState m_splitterDragState = {};
|
||||||
InputDebugSnapshot m_inputDebugSnapshot = {};
|
InputDebugSnapshot m_inputDebugSnapshot = {};
|
||||||
ScrollDebugSnapshot m_scrollDebugSnapshot = {};
|
ScrollDebugSnapshot m_scrollDebugSnapshot = {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace XCEngine {
|
|||||||
namespace UI {
|
namespace UI {
|
||||||
|
|
||||||
enum class UITextureHandleKind : std::uint8_t {
|
enum class UITextureHandleKind : std::uint8_t {
|
||||||
ImGuiDescriptor = 0,
|
DescriptorHandle = 0,
|
||||||
ShaderResourceView
|
ShaderResourceView
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ struct UITextureHandle {
|
|||||||
std::uintptr_t nativeHandle = 0;
|
std::uintptr_t nativeHandle = 0;
|
||||||
std::uint32_t width = 0;
|
std::uint32_t width = 0;
|
||||||
std::uint32_t height = 0;
|
std::uint32_t height = 0;
|
||||||
UITextureHandleKind kind = UITextureHandleKind::ImGuiDescriptor;
|
UITextureHandleKind kind = UITextureHandleKind::DescriptorHandle;
|
||||||
|
|
||||||
constexpr bool IsValid() const {
|
constexpr bool IsValid() const {
|
||||||
return nativeHandle != 0 && width > 0 && height > 0;
|
return nativeHandle != 0 && width > 0 && height > 0;
|
||||||
|
|||||||
124
engine/include/XCEngine/UI/Widgets/UISplitterInteraction.h
Normal file
124
engine/include/XCEngine/UI/Widgets/UISplitterInteraction.h
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Core/UIInvalidation.h>
|
||||||
|
#include <XCEngine/UI/Layout/UISplitterLayout.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Widgets {
|
||||||
|
|
||||||
|
struct UISplitterDragState {
|
||||||
|
bool active = false;
|
||||||
|
UIElementId ownerId = 0u;
|
||||||
|
Layout::UILayoutAxis axis = Layout::UILayoutAxis::Horizontal;
|
||||||
|
UIRect bounds = {};
|
||||||
|
Layout::UISplitterConstraints constraints = {};
|
||||||
|
Layout::UISplitterMetrics metrics = {};
|
||||||
|
float splitRatio = 0.5f;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline UIRect ExpandUISplitterHandleHitRect(
|
||||||
|
const UIRect& handleRect,
|
||||||
|
Layout::UILayoutAxis axis,
|
||||||
|
float crossAxisPadding = 3.0f) {
|
||||||
|
const float padding = (std::max)(0.0f, crossAxisPadding);
|
||||||
|
if (axis == Layout::UILayoutAxis::Horizontal) {
|
||||||
|
return UIRect(
|
||||||
|
handleRect.x - padding,
|
||||||
|
handleRect.y,
|
||||||
|
handleRect.width + padding * 2.0f,
|
||||||
|
handleRect.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIRect(
|
||||||
|
handleRect.x,
|
||||||
|
handleRect.y - padding,
|
||||||
|
handleRect.width,
|
||||||
|
handleRect.height + padding * 2.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool HitTestUISplitterHandle(
|
||||||
|
const UIRect& handleRect,
|
||||||
|
Layout::UILayoutAxis axis,
|
||||||
|
const UIPoint& point,
|
||||||
|
float crossAxisPadding = 3.0f) {
|
||||||
|
const UIRect hitRect = ExpandUISplitterHandleHitRect(handleRect, axis, crossAxisPadding);
|
||||||
|
return point.x >= hitRect.x &&
|
||||||
|
point.x <= hitRect.x + hitRect.width &&
|
||||||
|
point.y >= hitRect.y &&
|
||||||
|
point.y <= hitRect.y + hitRect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool BeginUISplitterDrag(
|
||||||
|
UIElementId ownerId,
|
||||||
|
Layout::UILayoutAxis axis,
|
||||||
|
const UIRect& bounds,
|
||||||
|
const Layout::UISplitterLayoutResult& currentLayout,
|
||||||
|
const Layout::UISplitterConstraints& constraints,
|
||||||
|
const Layout::UISplitterMetrics& metrics,
|
||||||
|
const UIPoint& pointerPosition,
|
||||||
|
UISplitterDragState& outState) {
|
||||||
|
const float extraHitPadding = (std::max)(
|
||||||
|
0.0f,
|
||||||
|
(metrics.hitThickness - metrics.thickness) * 0.5f);
|
||||||
|
if (!HitTestUISplitterHandle(
|
||||||
|
currentLayout.handleRect,
|
||||||
|
axis,
|
||||||
|
pointerPosition,
|
||||||
|
extraHitPadding)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outState = {};
|
||||||
|
outState.active = true;
|
||||||
|
outState.ownerId = ownerId;
|
||||||
|
outState.axis = axis;
|
||||||
|
outState.bounds = bounds;
|
||||||
|
outState.constraints = constraints;
|
||||||
|
outState.metrics = metrics;
|
||||||
|
outState.splitRatio = currentLayout.splitRatio;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool UpdateUISplitterDrag(
|
||||||
|
UISplitterDragState& state,
|
||||||
|
const UIPoint& pointerPosition,
|
||||||
|
Layout::UISplitterLayoutResult& outLayout) {
|
||||||
|
if (!state.active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float pointerMainPosition = state.axis == Layout::UILayoutAxis::Horizontal
|
||||||
|
? pointerPosition.x
|
||||||
|
: pointerPosition.y;
|
||||||
|
const float requestedRatio = Layout::ResolveSplitterRatioFromPointerPosition(
|
||||||
|
Layout::UISplitterLayoutOptions {
|
||||||
|
state.axis,
|
||||||
|
state.splitRatio,
|
||||||
|
state.metrics.thickness,
|
||||||
|
state.constraints.primaryMin,
|
||||||
|
state.constraints.secondaryMin
|
||||||
|
},
|
||||||
|
state.bounds,
|
||||||
|
pointerMainPosition);
|
||||||
|
|
||||||
|
outLayout = Layout::ArrangeUISplitter(
|
||||||
|
state.bounds,
|
||||||
|
state.axis,
|
||||||
|
requestedRatio,
|
||||||
|
state.constraints,
|
||||||
|
state.metrics);
|
||||||
|
const bool changed = std::fabs(outLayout.splitRatio - state.splitRatio) > 0.0001f;
|
||||||
|
state.splitRatio = outLayout.splitRatio;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void EndUISplitterDrag(UISplitterDragState& state) {
|
||||||
|
state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Widgets
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace UI {
|
namespace UI {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool IsKeyboardActivationKey(std::int32_t keyCode) {
|
||||||
|
return keyCode == static_cast<std::int32_t>(Input::KeyCode::Enter) ||
|
||||||
|
keyCode == static_cast<std::int32_t>(Input::KeyCode::Space);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool UIInputDispatcher::ShouldTransferFocusOnPointerDown(
|
bool UIInputDispatcher::ShouldTransferFocusOnPointerDown(
|
||||||
const UIInputEvent& event,
|
const UIInputEvent& event,
|
||||||
const UIInputPath& hoveredPath) const {
|
const UIInputPath& hoveredPath) const {
|
||||||
@@ -20,10 +31,52 @@ bool UIInputDispatcher::ShouldStartActivePathOnPointerDown(
|
|||||||
!hoveredPath.Empty();
|
!hoveredPath.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UIInputDispatcher::ShouldStartActivePathOnKeyDown(
|
||||||
|
const UIInputEvent& event) const {
|
||||||
|
return event.type == UIInputEventType::KeyDown &&
|
||||||
|
!m_focusController.GetFocusedPath().Empty() &&
|
||||||
|
IsKeyboardActivationKey(event.keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UIInputDispatcher::ShouldClearActivePathOnKeyUp(
|
||||||
|
const UIInputEvent& event) const {
|
||||||
|
return event.type == UIInputEventType::KeyUp &&
|
||||||
|
IsKeyboardActivationKey(event.keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
UIShortcutContext UIInputDispatcher::BuildEffectiveShortcutContext(
|
||||||
|
const UIInputPath& hoveredPath) const {
|
||||||
|
UIShortcutContext context = m_shortcutContext;
|
||||||
|
context.focusedPath = m_focusController.GetFocusedPath();
|
||||||
|
context.activePath = m_focusController.GetActivePath();
|
||||||
|
context.hoveredPath = hoveredPath;
|
||||||
|
|
||||||
|
if (context.commandScope.path.Empty()) {
|
||||||
|
context.commandScope.path = !context.focusedPath.Empty()
|
||||||
|
? context.focusedPath
|
||||||
|
: context.activePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.commandScope.widgetId == 0 &&
|
||||||
|
!context.commandScope.path.Empty()) {
|
||||||
|
context.commandScope.widgetId = context.commandScope.path.Target();
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UIInputDispatcher::ShouldSuppressShortcutMatch(
|
||||||
|
const UIInputEvent&,
|
||||||
|
const UIShortcutMatch& shortcutMatch,
|
||||||
|
const UIShortcutContext& shortcutContext) const {
|
||||||
|
return shortcutMatch.matched && shortcutContext.textInputActive;
|
||||||
|
}
|
||||||
|
|
||||||
UIInputDispatchSummary UIInputDispatcher::FinalizeDispatch(
|
UIInputDispatchSummary UIInputDispatcher::FinalizeDispatch(
|
||||||
const UIInputEvent& event,
|
const UIInputEvent& event,
|
||||||
UIInputDispatchSummary&& summary) {
|
UIInputDispatchSummary&& summary) {
|
||||||
if (event.type == UIInputEventType::PointerButtonUp) {
|
if (event.type == UIInputEventType::PointerButtonUp ||
|
||||||
|
ShouldClearActivePathOnKeyUp(event)) {
|
||||||
m_focusController.ClearActivePath();
|
m_focusController.ClearActivePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,18 @@ namespace UI {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
const UIInputPath& ResolvePrimaryShortcutPath(const UIShortcutContext& context) {
|
const UIInputPath& ResolvePrimaryShortcutPath(const UIShortcutContext& context) {
|
||||||
if (!context.activePath.Empty()) {
|
if (!context.commandScope.path.Empty()) {
|
||||||
return context.activePath;
|
return context.commandScope.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.focusedPath.Empty()) {
|
if (!context.focusedPath.Empty()) {
|
||||||
return context.focusedPath;
|
return context.focusedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!context.activePath.Empty()) {
|
||||||
|
return context.activePath;
|
||||||
|
}
|
||||||
|
|
||||||
return context.hoveredPath;
|
return context.hoveredPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +30,28 @@ bool ModifiersEqual(
|
|||||||
lhs.super == rhs.super;
|
lhs.super == rhs.super;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UIElementId ResolveScopedOwnerId(
|
||||||
|
UIShortcutScope scope,
|
||||||
|
const UIShortcutContext& context) {
|
||||||
|
switch (scope) {
|
||||||
|
case UIShortcutScope::Window:
|
||||||
|
return context.commandScope.windowId;
|
||||||
|
case UIShortcutScope::Panel:
|
||||||
|
return context.commandScope.panelId;
|
||||||
|
case UIShortcutScope::Widget:
|
||||||
|
if (context.commandScope.widgetId != 0) {
|
||||||
|
return context.commandScope.widgetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.commandScope.path.Empty()
|
||||||
|
? 0
|
||||||
|
: context.commandScope.path.Target();
|
||||||
|
case UIShortcutScope::Global:
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool IsBindingActive(
|
bool IsBindingActive(
|
||||||
const UIShortcutBinding& binding,
|
const UIShortcutBinding& binding,
|
||||||
const UIShortcutContext& context) {
|
const UIShortcutContext& context) {
|
||||||
@@ -37,6 +63,11 @@ bool IsBindingActive(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UIElementId resolvedOwnerId = ResolveScopedOwnerId(binding.scope, context);
|
||||||
|
if (resolvedOwnerId != 0) {
|
||||||
|
return binding.ownerId == resolvedOwnerId;
|
||||||
|
}
|
||||||
|
|
||||||
return ResolvePrimaryShortcutPath(context).Contains(binding.ownerId);
|
return ResolvePrimaryShortcutPath(context).Contains(binding.ownerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +122,7 @@ bool UIShortcutRegistry::UnregisterBinding(std::uint64_t bindingId) {
|
|||||||
|
|
||||||
void UIShortcutRegistry::Clear() {
|
void UIShortcutRegistry::Clear() {
|
||||||
m_bindings.clear();
|
m_bindings.clear();
|
||||||
|
m_nextBindingId = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
UIShortcutMatch UIShortcutRegistry::Match(
|
UIShortcutMatch UIShortcutRegistry::Match(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,15 +9,16 @@ file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCNEWEDITOR_REPO_ROOT_PATH)
|
|||||||
set(NEW_EDITOR_RESOURCE_FILES
|
set(NEW_EDITOR_RESOURCE_FILES
|
||||||
ui/views/editor_shell.xcui
|
ui/views/editor_shell.xcui
|
||||||
ui/themes/editor_shell.xctheme
|
ui/themes/editor_shell.xctheme
|
||||||
ui/schemas/editor_inspector_shell.xcschema
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(XCNewEditorLib STATIC
|
add_library(XCNewEditorLib STATIC
|
||||||
src/SandboxFrameBuilder.cpp
|
src/editor/EditorShellAsset.cpp
|
||||||
|
src/Widgets/UIEditorCollectionPrimitives.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(XCNewEditorLib
|
target_include_directories(XCNewEditorLib
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
${CMAKE_SOURCE_DIR}/engine/include
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,7 @@ target_include_directories(XCNewEditorLib
|
|||||||
target_compile_definitions(XCNewEditorLib PUBLIC
|
target_compile_definitions(XCNewEditorLib PUBLIC
|
||||||
UNICODE
|
UNICODE
|
||||||
_UNICODE
|
_UNICODE
|
||||||
|
XCNEWEDITOR_REPO_ROOT="${XCNEWEDITOR_REPO_ROOT_PATH}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -37,41 +39,70 @@ target_link_libraries(XCNewEditorLib PUBLIC
|
|||||||
XCEngine
|
XCEngine
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(XCNewEditorApp WIN32
|
add_library(XCNewEditorHost STATIC
|
||||||
src/main.cpp
|
src/Host/AutoScreenshot.cpp
|
||||||
src/Application.cpp
|
src/Host/NativeRenderer.cpp
|
||||||
src/AutoScreenshot.cpp
|
|
||||||
src/NativeRenderer.cpp
|
|
||||||
${NEW_EDITOR_RESOURCE_FILES}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(XCNewEditorApp PRIVATE
|
target_include_directories(XCNewEditorHost
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
PUBLIC
|
||||||
${CMAKE_SOURCE_DIR}/engine/include
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(XCNewEditorApp PRIVATE
|
target_compile_definitions(XCNewEditorHost PUBLIC
|
||||||
UNICODE
|
UNICODE
|
||||||
_UNICODE
|
_UNICODE
|
||||||
XCNEWEDITOR_REPO_ROOT="${XCNEWEDITOR_REPO_ROOT_PATH}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(XCNewEditorApp PRIVATE /utf-8 /FS)
|
target_compile_options(XCNewEditorHost PRIVATE /utf-8 /FS)
|
||||||
set_property(TARGET XCNewEditorApp PROPERTY
|
set_property(TARGET XCNewEditorHost PROPERTY
|
||||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(XCNewEditorApp PRIVATE
|
target_link_libraries(XCNewEditorHost PUBLIC
|
||||||
XCNewEditorLib
|
XCEngine
|
||||||
d2d1.lib
|
d2d1.lib
|
||||||
dwrite.lib
|
dwrite.lib
|
||||||
windowscodecs.lib
|
windowscodecs.lib
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(XCNewEditorApp PROPERTIES
|
if(XCENGINE_BUILD_NEW_EDITOR)
|
||||||
OUTPUT_NAME "XCNewEditor"
|
add_executable(XCNewEditorApp WIN32
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin"
|
src/main.cpp
|
||||||
)
|
src/Host/Application.cpp
|
||||||
|
${NEW_EDITOR_RESOURCE_FILES}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(XCNewEditorApp PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(XCNewEditorApp PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
XCNEWEDITOR_REPO_ROOT="${XCNEWEDITOR_REPO_ROOT_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(XCNewEditorApp PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET XCNewEditorApp PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(XCNewEditorApp PRIVATE
|
||||||
|
XCNewEditorLib
|
||||||
|
XCNewEditorHost
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(XCNewEditorApp PROPERTIES
|
||||||
|
OUTPUT_NAME "XCNewEditor"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${NEW_EDITOR_RESOURCE_FILES})
|
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${NEW_EDITOR_RESOURCE_FILES})
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine::XCUI::Host {
|
||||||
namespace NewEditor {
|
|
||||||
|
|
||||||
class NativeRenderer;
|
class NativeRenderer;
|
||||||
|
|
||||||
@@ -50,5 +49,4 @@ private:
|
|||||||
bool m_capturePending = false;
|
bool m_capturePending = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace NewEditor
|
} // namespace XCEngine::XCUI::Host
|
||||||
} // namespace XCEngine
|
|
||||||
173
new_editor/include/XCNewEditor/Host/InputModifierTracker.h
Normal file
173
new_editor/include/XCNewEditor/Host/InputModifierTracker.h
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Types.h>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace XCEngine::XCUI::Host {
|
||||||
|
|
||||||
|
class InputModifierTracker {
|
||||||
|
public:
|
||||||
|
void Reset() {
|
||||||
|
m_leftShift = false;
|
||||||
|
m_rightShift = false;
|
||||||
|
m_leftControl = false;
|
||||||
|
m_rightControl = false;
|
||||||
|
m_leftAlt = false;
|
||||||
|
m_rightAlt = false;
|
||||||
|
m_leftSuper = false;
|
||||||
|
m_rightSuper = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncFromSystemState() {
|
||||||
|
m_leftShift = (GetKeyState(VK_LSHIFT) & 0x8000) != 0;
|
||||||
|
m_rightShift = (GetKeyState(VK_RSHIFT) & 0x8000) != 0;
|
||||||
|
m_leftControl = (GetKeyState(VK_LCONTROL) & 0x8000) != 0;
|
||||||
|
m_rightControl = (GetKeyState(VK_RCONTROL) & 0x8000) != 0;
|
||||||
|
m_leftAlt = (GetKeyState(VK_LMENU) & 0x8000) != 0;
|
||||||
|
m_rightAlt = (GetKeyState(VK_RMENU) & 0x8000) != 0;
|
||||||
|
m_leftSuper = (GetKeyState(VK_LWIN) & 0x8000) != 0;
|
||||||
|
m_rightSuper = (GetKeyState(VK_RWIN) & 0x8000) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::XCEngine::UI::UIInputModifiers GetCurrentModifiers() const {
|
||||||
|
return BuildModifiers();
|
||||||
|
}
|
||||||
|
|
||||||
|
::XCEngine::UI::UIInputModifiers BuildPointerModifiers(std::size_t wParam) const {
|
||||||
|
::XCEngine::UI::UIInputModifiers modifiers = BuildModifiers();
|
||||||
|
modifiers.shift = modifiers.shift || (wParam & MK_SHIFT) != 0;
|
||||||
|
modifiers.control = modifiers.control || (wParam & MK_CONTROL) != 0;
|
||||||
|
return modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
::XCEngine::UI::UIInputModifiers ApplyKeyMessage(
|
||||||
|
::XCEngine::UI::UIInputEventType type,
|
||||||
|
WPARAM wParam,
|
||||||
|
LPARAM lParam) {
|
||||||
|
if (type == ::XCEngine::UI::UIInputEventType::KeyDown) {
|
||||||
|
SetModifierState(ResolveModifierKey(wParam, lParam), true);
|
||||||
|
} else if (type == ::XCEngine::UI::UIInputEventType::KeyUp) {
|
||||||
|
SetModifierState(ResolveModifierKey(wParam, lParam), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildModifiers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class ModifierKey : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
LeftShift,
|
||||||
|
RightShift,
|
||||||
|
LeftControl,
|
||||||
|
RightControl,
|
||||||
|
LeftAlt,
|
||||||
|
RightAlt,
|
||||||
|
LeftSuper,
|
||||||
|
RightSuper
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool IsExtendedKey(LPARAM lParam) {
|
||||||
|
return (static_cast<std::uint32_t>(lParam) & 0x01000000u) != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint32_t ExtractScanCode(LPARAM lParam) {
|
||||||
|
return (static_cast<std::uint32_t>(lParam) >> 16u) & 0xffu;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ModifierKey ResolveModifierKey(WPARAM wParam, LPARAM lParam) {
|
||||||
|
switch (static_cast<std::uint32_t>(wParam)) {
|
||||||
|
case VK_SHIFT: {
|
||||||
|
const UINT shiftVirtualKey = MapVirtualKeyW(ExtractScanCode(lParam), MAPVK_VSC_TO_VK_EX);
|
||||||
|
return shiftVirtualKey == VK_RSHIFT
|
||||||
|
? ModifierKey::RightShift
|
||||||
|
: ModifierKey::LeftShift;
|
||||||
|
}
|
||||||
|
case VK_LSHIFT:
|
||||||
|
return ModifierKey::LeftShift;
|
||||||
|
case VK_RSHIFT:
|
||||||
|
return ModifierKey::RightShift;
|
||||||
|
case VK_CONTROL:
|
||||||
|
return IsExtendedKey(lParam)
|
||||||
|
? ModifierKey::RightControl
|
||||||
|
: ModifierKey::LeftControl;
|
||||||
|
case VK_LCONTROL:
|
||||||
|
return ModifierKey::LeftControl;
|
||||||
|
case VK_RCONTROL:
|
||||||
|
return ModifierKey::RightControl;
|
||||||
|
case VK_MENU:
|
||||||
|
return IsExtendedKey(lParam)
|
||||||
|
? ModifierKey::RightAlt
|
||||||
|
: ModifierKey::LeftAlt;
|
||||||
|
case VK_LMENU:
|
||||||
|
return ModifierKey::LeftAlt;
|
||||||
|
case VK_RMENU:
|
||||||
|
return ModifierKey::RightAlt;
|
||||||
|
case VK_LWIN:
|
||||||
|
return ModifierKey::LeftSuper;
|
||||||
|
case VK_RWIN:
|
||||||
|
return ModifierKey::RightSuper;
|
||||||
|
default:
|
||||||
|
return ModifierKey::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetModifierState(ModifierKey key, bool pressed) {
|
||||||
|
switch (key) {
|
||||||
|
case ModifierKey::LeftShift:
|
||||||
|
m_leftShift = pressed;
|
||||||
|
break;
|
||||||
|
case ModifierKey::RightShift:
|
||||||
|
m_rightShift = pressed;
|
||||||
|
break;
|
||||||
|
case ModifierKey::LeftControl:
|
||||||
|
m_leftControl = pressed;
|
||||||
|
break;
|
||||||
|
case ModifierKey::RightControl:
|
||||||
|
m_rightControl = pressed;
|
||||||
|
break;
|
||||||
|
case ModifierKey::LeftAlt:
|
||||||
|
m_leftAlt = pressed;
|
||||||
|
break;
|
||||||
|
case ModifierKey::RightAlt:
|
||||||
|
m_rightAlt = pressed;
|
||||||
|
break;
|
||||||
|
case ModifierKey::LeftSuper:
|
||||||
|
m_leftSuper = pressed;
|
||||||
|
break;
|
||||||
|
case ModifierKey::RightSuper:
|
||||||
|
m_rightSuper = pressed;
|
||||||
|
break;
|
||||||
|
case ModifierKey::None:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::XCEngine::UI::UIInputModifiers BuildModifiers() const {
|
||||||
|
::XCEngine::UI::UIInputModifiers modifiers = {};
|
||||||
|
modifiers.shift = m_leftShift || m_rightShift;
|
||||||
|
modifiers.control = m_leftControl || m_rightControl;
|
||||||
|
modifiers.alt = m_leftAlt || m_rightAlt;
|
||||||
|
modifiers.super = m_leftSuper || m_rightSuper;
|
||||||
|
return modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool m_leftShift = false;
|
||||||
|
bool m_rightShift = false;
|
||||||
|
bool m_leftControl = false;
|
||||||
|
bool m_rightControl = false;
|
||||||
|
bool m_leftAlt = false;
|
||||||
|
bool m_rightAlt = false;
|
||||||
|
bool m_leftSuper = false;
|
||||||
|
bool m_rightSuper = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace XCEngine::XCUI::Host
|
||||||
@@ -18,8 +18,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine::XCUI::Host {
|
||||||
namespace NewEditor {
|
|
||||||
|
|
||||||
class NativeRenderer {
|
class NativeRenderer {
|
||||||
public:
|
public:
|
||||||
@@ -63,5 +62,4 @@ private:
|
|||||||
bool m_wicComInitialized = false;
|
bool m_wicComInitialized = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace NewEditor
|
} // namespace XCEngine::XCUI::Host
|
||||||
} // namespace XCEngine
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
|
||||||
#include "SandboxFrameBuilder.h"
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -15,8 +15,7 @@
|
|||||||
#define XCNEWEDITOR_REPO_ROOT "."
|
#define XCNEWEDITOR_REPO_ROOT "."
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine::NewEditor {
|
||||||
namespace NewEditor {
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -25,14 +24,14 @@ using ::XCEngine::UI::UIDrawData;
|
|||||||
using ::XCEngine::UI::UIDrawList;
|
using ::XCEngine::UI::UIDrawList;
|
||||||
using ::XCEngine::UI::UIInputEvent;
|
using ::XCEngine::UI::UIInputEvent;
|
||||||
using ::XCEngine::UI::UIInputEventType;
|
using ::XCEngine::UI::UIInputEventType;
|
||||||
using ::XCEngine::UI::UIInputModifiers;
|
|
||||||
using ::XCEngine::UI::UIPoint;
|
using ::XCEngine::UI::UIPoint;
|
||||||
using ::XCEngine::UI::UIPointerButton;
|
using ::XCEngine::UI::UIPointerButton;
|
||||||
using ::XCEngine::UI::UIRect;
|
using ::XCEngine::UI::UIRect;
|
||||||
using ::XCEngine::UI::Runtime::UIScreenFrameInput;
|
using ::XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||||
|
using ::XCEngine::Input::KeyCode;
|
||||||
|
|
||||||
constexpr const wchar_t* kWindowClassName = L"XCNewEditorNativeSandbox";
|
constexpr const wchar_t* kWindowClassName = L"XCNewEditorShellHost";
|
||||||
constexpr const wchar_t* kWindowTitle = L"XCNewEditor Native Sandbox";
|
constexpr const wchar_t* kWindowTitle = L"XCUI New Editor";
|
||||||
constexpr auto kReloadPollInterval = std::chrono::milliseconds(150);
|
constexpr auto kReloadPollInterval = std::chrono::milliseconds(150);
|
||||||
|
|
||||||
constexpr UIColor kOverlayBgColor(0.10f, 0.10f, 0.10f, 0.95f);
|
constexpr UIColor kOverlayBgColor(0.10f, 0.10f, 0.10f, 0.95f);
|
||||||
@@ -83,21 +82,79 @@ std::string FormatPoint(const UIPoint& point) {
|
|||||||
return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")";
|
return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string FormatRect(const UIRect& rect) {
|
std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) {
|
||||||
return "(" + FormatFloat(rect.x) +
|
switch (wParam) {
|
||||||
", " + FormatFloat(rect.y) +
|
case 'A': return static_cast<std::int32_t>(KeyCode::A);
|
||||||
", " + FormatFloat(rect.width) +
|
case 'B': return static_cast<std::int32_t>(KeyCode::B);
|
||||||
", " + FormatFloat(rect.height) +
|
case 'C': return static_cast<std::int32_t>(KeyCode::C);
|
||||||
")";
|
case 'D': return static_cast<std::int32_t>(KeyCode::D);
|
||||||
|
case 'E': return static_cast<std::int32_t>(KeyCode::E);
|
||||||
|
case 'F': return static_cast<std::int32_t>(KeyCode::F);
|
||||||
|
case 'G': return static_cast<std::int32_t>(KeyCode::G);
|
||||||
|
case 'H': return static_cast<std::int32_t>(KeyCode::H);
|
||||||
|
case 'I': return static_cast<std::int32_t>(KeyCode::I);
|
||||||
|
case 'J': return static_cast<std::int32_t>(KeyCode::J);
|
||||||
|
case 'K': return static_cast<std::int32_t>(KeyCode::K);
|
||||||
|
case 'L': return static_cast<std::int32_t>(KeyCode::L);
|
||||||
|
case 'M': return static_cast<std::int32_t>(KeyCode::M);
|
||||||
|
case 'N': return static_cast<std::int32_t>(KeyCode::N);
|
||||||
|
case 'O': return static_cast<std::int32_t>(KeyCode::O);
|
||||||
|
case 'P': return static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
case 'Q': return static_cast<std::int32_t>(KeyCode::Q);
|
||||||
|
case 'R': return static_cast<std::int32_t>(KeyCode::R);
|
||||||
|
case 'S': return static_cast<std::int32_t>(KeyCode::S);
|
||||||
|
case 'T': return static_cast<std::int32_t>(KeyCode::T);
|
||||||
|
case 'U': return static_cast<std::int32_t>(KeyCode::U);
|
||||||
|
case 'V': return static_cast<std::int32_t>(KeyCode::V);
|
||||||
|
case 'W': return static_cast<std::int32_t>(KeyCode::W);
|
||||||
|
case 'X': return static_cast<std::int32_t>(KeyCode::X);
|
||||||
|
case 'Y': return static_cast<std::int32_t>(KeyCode::Y);
|
||||||
|
case 'Z': return static_cast<std::int32_t>(KeyCode::Z);
|
||||||
|
case '0': return static_cast<std::int32_t>(KeyCode::Zero);
|
||||||
|
case '1': return static_cast<std::int32_t>(KeyCode::One);
|
||||||
|
case '2': return static_cast<std::int32_t>(KeyCode::Two);
|
||||||
|
case '3': return static_cast<std::int32_t>(KeyCode::Three);
|
||||||
|
case '4': return static_cast<std::int32_t>(KeyCode::Four);
|
||||||
|
case '5': return static_cast<std::int32_t>(KeyCode::Five);
|
||||||
|
case '6': return static_cast<std::int32_t>(KeyCode::Six);
|
||||||
|
case '7': return static_cast<std::int32_t>(KeyCode::Seven);
|
||||||
|
case '8': return static_cast<std::int32_t>(KeyCode::Eight);
|
||||||
|
case '9': return static_cast<std::int32_t>(KeyCode::Nine);
|
||||||
|
case VK_SPACE: return static_cast<std::int32_t>(KeyCode::Space);
|
||||||
|
case VK_TAB: return static_cast<std::int32_t>(KeyCode::Tab);
|
||||||
|
case VK_RETURN: return static_cast<std::int32_t>(KeyCode::Enter);
|
||||||
|
case VK_ESCAPE: return static_cast<std::int32_t>(KeyCode::Escape);
|
||||||
|
case VK_SHIFT: return static_cast<std::int32_t>(KeyCode::LeftShift);
|
||||||
|
case VK_CONTROL: return static_cast<std::int32_t>(KeyCode::LeftCtrl);
|
||||||
|
case VK_MENU: return static_cast<std::int32_t>(KeyCode::LeftAlt);
|
||||||
|
case VK_UP: return static_cast<std::int32_t>(KeyCode::Up);
|
||||||
|
case VK_DOWN: return static_cast<std::int32_t>(KeyCode::Down);
|
||||||
|
case VK_LEFT: return static_cast<std::int32_t>(KeyCode::Left);
|
||||||
|
case VK_RIGHT: return static_cast<std::int32_t>(KeyCode::Right);
|
||||||
|
case VK_HOME: return static_cast<std::int32_t>(KeyCode::Home);
|
||||||
|
case VK_END: return static_cast<std::int32_t>(KeyCode::End);
|
||||||
|
case VK_PRIOR: return static_cast<std::int32_t>(KeyCode::PageUp);
|
||||||
|
case VK_NEXT: return static_cast<std::int32_t>(KeyCode::PageDown);
|
||||||
|
case VK_DELETE: return static_cast<std::int32_t>(KeyCode::Delete);
|
||||||
|
case VK_BACK: return static_cast<std::int32_t>(KeyCode::Backspace);
|
||||||
|
case VK_F1: return static_cast<std::int32_t>(KeyCode::F1);
|
||||||
|
case VK_F2: return static_cast<std::int32_t>(KeyCode::F2);
|
||||||
|
case VK_F3: return static_cast<std::int32_t>(KeyCode::F3);
|
||||||
|
case VK_F4: return static_cast<std::int32_t>(KeyCode::F4);
|
||||||
|
case VK_F5: return static_cast<std::int32_t>(KeyCode::F5);
|
||||||
|
case VK_F6: return static_cast<std::int32_t>(KeyCode::F6);
|
||||||
|
case VK_F7: return static_cast<std::int32_t>(KeyCode::F7);
|
||||||
|
case VK_F8: return static_cast<std::int32_t>(KeyCode::F8);
|
||||||
|
case VK_F9: return static_cast<std::int32_t>(KeyCode::F9);
|
||||||
|
case VK_F10: return static_cast<std::int32_t>(KeyCode::F10);
|
||||||
|
case VK_F11: return static_cast<std::int32_t>(KeyCode::F11);
|
||||||
|
case VK_F12: return static_cast<std::int32_t>(KeyCode::F12);
|
||||||
|
default: return static_cast<std::int32_t>(KeyCode::None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UIInputModifiers BuildInputModifiers(size_t wParam) {
|
bool IsRepeatKeyMessage(LPARAM lParam) {
|
||||||
UIInputModifiers modifiers = {};
|
return (static_cast<unsigned long>(lParam) & (1ul << 30)) != 0ul;
|
||||||
modifiers.shift = (wParam & MK_SHIFT) != 0;
|
|
||||||
modifiers.control = (wParam & MK_CONTROL) != 0;
|
|
||||||
modifiers.alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
|
|
||||||
modifiers.super = (GetKeyState(VK_LWIN) & 0x8000) != 0 || (GetKeyState(VK_RWIN) & 0x8000) != 0;
|
|
||||||
return modifiers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -130,6 +187,7 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
|||||||
|
|
||||||
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||||
m_hInstance = hInstance;
|
m_hInstance = hInstance;
|
||||||
|
m_shellAssetDefinition = BuildDefaultEditorShellAsset(ResolveRepoRootPath());
|
||||||
|
|
||||||
WNDCLASSEXW windowClass = {};
|
WNDCLASSEXW windowClass = {};
|
||||||
windowClass.cbSize = sizeof(windowClass);
|
windowClass.cbSize = sizeof(windowClass);
|
||||||
@@ -170,7 +228,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
|||||||
|
|
||||||
m_startTime = std::chrono::steady_clock::now();
|
m_startTime = std::chrono::steady_clock::now();
|
||||||
m_lastFrameTime = m_startTime;
|
m_lastFrameTime = m_startTime;
|
||||||
m_autoScreenshot.Initialize(ResolveRepoRelativePath("new_editor/captures"));
|
m_autoScreenshot.Initialize(m_shellAssetDefinition.captureRootPath);
|
||||||
LoadStructuredScreen("startup");
|
LoadStructuredScreen("startup");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -209,7 +267,6 @@ void Application::RenderFrame() {
|
|||||||
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||||
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
const double timeSeconds = std::chrono::duration<double>(now - m_startTime).count();
|
|
||||||
double deltaTimeSeconds = std::chrono::duration<double>(now - m_lastFrameTime).count();
|
double deltaTimeSeconds = std::chrono::duration<double>(now - m_lastFrameTime).count();
|
||||||
if (deltaTimeSeconds <= 0.0) {
|
if (deltaTimeSeconds <= 0.0) {
|
||||||
deltaTimeSeconds = 1.0 / 60.0;
|
deltaTimeSeconds = 1.0 / 60.0;
|
||||||
@@ -234,17 +291,10 @@ void Application::RenderFrame() {
|
|||||||
drawData.AddDrawList(drawList);
|
drawData.AddDrawList(drawList);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_runtimeStatus = "Authored XCUI";
|
m_runtimeStatus = "XCUI Editor Shell";
|
||||||
m_runtimeError = frame.errorMessage;
|
m_runtimeError = frame.errorMessage;
|
||||||
}
|
} else {
|
||||||
|
m_runtimeStatus = "Editor Shell | Load Error";
|
||||||
if (drawData.Empty()) {
|
|
||||||
SandboxFrameOptions options = {};
|
|
||||||
options.width = width;
|
|
||||||
options.height = height;
|
|
||||||
options.timeSeconds = timeSeconds;
|
|
||||||
drawData = BuildSandboxFrame(options);
|
|
||||||
m_runtimeStatus = "Fallback Sandbox";
|
|
||||||
if (m_runtimeError.empty() && !m_screenPlayer.IsLoaded()) {
|
if (m_runtimeError.empty() && !m_screenPlayer.IsLoaded()) {
|
||||||
m_runtimeError = m_screenPlayer.GetLastError();
|
m_runtimeError = m_screenPlayer.GetLastError();
|
||||||
}
|
}
|
||||||
@@ -276,7 +326,7 @@ void Application::QueuePointerEvent(UIInputEventType type, UIPointerButton butto
|
|||||||
event.position = UIPoint(
|
event.position = UIPoint(
|
||||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
event.modifiers = BuildInputModifiers(static_cast<size_t>(wParam));
|
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
||||||
m_pendingInputEvents.push_back(event);
|
m_pendingInputEvents.push_back(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,19 +357,43 @@ void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM
|
|||||||
event.type = UIInputEventType::PointerWheel;
|
event.type = UIInputEventType::PointerWheel;
|
||||||
event.position = UIPoint(static_cast<float>(screenPoint.x), static_cast<float>(screenPoint.y));
|
event.position = UIPoint(static_cast<float>(screenPoint.x), static_cast<float>(screenPoint.y));
|
||||||
event.wheelDelta = static_cast<float>(wheelDelta);
|
event.wheelDelta = static_cast<float>(wheelDelta);
|
||||||
event.modifiers = BuildInputModifiers(static_cast<size_t>(wParam));
|
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueueKeyEvent(UIInputEventType type, WPARAM wParam, LPARAM lParam) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.keyCode = MapVirtualKeyToUIKeyCode(wParam);
|
||||||
|
event.modifiers = m_inputModifierTracker.ApplyKeyMessage(type, wParam, lParam);
|
||||||
|
event.repeat = IsRepeatKeyMessage(lParam);
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueueCharacterEvent(WPARAM wParam, LPARAM) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::Character;
|
||||||
|
event.character = static_cast<std::uint32_t>(wParam);
|
||||||
|
event.modifiers = m_inputModifierTracker.GetCurrentModifiers();
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueueWindowFocusEvent(UIInputEventType type) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
m_pendingInputEvents.push_back(event);
|
m_pendingInputEvents.push_back(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Application::LoadStructuredScreen(const char* triggerReason) {
|
bool Application::LoadStructuredScreen(const char* triggerReason) {
|
||||||
|
(void)triggerReason;
|
||||||
m_screenAsset = {};
|
m_screenAsset = {};
|
||||||
m_screenAsset.screenId = "new_editor.editor_shell";
|
m_screenAsset.screenId = m_shellAssetDefinition.screenId;
|
||||||
m_screenAsset.documentPath = ResolveRepoRelativePath("new_editor/ui/views/editor_shell.xcui").string();
|
m_screenAsset.documentPath = m_shellAssetDefinition.documentPath.string();
|
||||||
m_screenAsset.themePath = ResolveRepoRelativePath("new_editor/ui/themes/editor_shell.xctheme").string();
|
m_screenAsset.themePath = m_shellAssetDefinition.themePath.string();
|
||||||
|
|
||||||
const bool loaded = m_screenPlayer.Load(m_screenAsset);
|
const bool loaded = m_screenPlayer.Load(m_screenAsset);
|
||||||
m_useStructuredScreen = loaded;
|
m_useStructuredScreen = loaded;
|
||||||
m_runtimeStatus = loaded ? "Authored XCUI" : "Fallback Sandbox";
|
m_runtimeStatus = loaded ? "XCUI Editor Shell" : "Editor Shell | Load Error";
|
||||||
m_runtimeError = loaded ? std::string() : m_screenPlayer.GetLastError();
|
m_runtimeError = loaded ? std::string() : m_screenPlayer.GetLastError();
|
||||||
RebuildTrackedFileStates();
|
RebuildTrackedFileStates();
|
||||||
return loaded;
|
return loaded;
|
||||||
@@ -404,12 +478,13 @@ bool Application::DetectTrackedFileChange() const {
|
|||||||
|
|
||||||
void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const {
|
void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const {
|
||||||
const bool authoredMode = m_useStructuredScreen && m_screenPlayer.IsLoaded();
|
const bool authoredMode = m_useStructuredScreen && m_screenPlayer.IsLoaded();
|
||||||
const float panelWidth = authoredMode ? 420.0f : 360.0f;
|
const float panelWidth = authoredMode ? 430.0f : 380.0f;
|
||||||
std::vector<std::string> detailLines = {};
|
std::vector<std::string> detailLines = {};
|
||||||
detailLines.push_back(
|
detailLines.push_back(
|
||||||
authoredMode
|
authoredMode
|
||||||
? "Hot reload watches authored UI resources."
|
? "Hot reload watches editor shell resources."
|
||||||
: "Using native fallback while authored UI is invalid.");
|
: "Authored editor shell failed to load.");
|
||||||
|
detailLines.push_back("Document: editor_shell.xcui");
|
||||||
|
|
||||||
if (authoredMode) {
|
if (authoredMode) {
|
||||||
const auto& inputDebug = m_documentHost.GetInputDebugSnapshot();
|
const auto& inputDebug = m_documentHost.GetInputDebugSnapshot();
|
||||||
@@ -424,18 +499,24 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
|
|||||||
" | " +
|
" | " +
|
||||||
ExtractStateKeyTail(inputDebug.captureStateKey));
|
ExtractStateKeyTail(inputDebug.captureStateKey));
|
||||||
if (!inputDebug.lastEventType.empty()) {
|
if (!inputDebug.lastEventType.empty()) {
|
||||||
|
const std::string eventPosition = inputDebug.lastEventType == "KeyDown" ||
|
||||||
|
inputDebug.lastEventType == "KeyUp" ||
|
||||||
|
inputDebug.lastEventType == "Character" ||
|
||||||
|
inputDebug.lastEventType == "FocusGained" ||
|
||||||
|
inputDebug.lastEventType == "FocusLost"
|
||||||
|
? std::string()
|
||||||
|
: " at " + FormatPoint(inputDebug.pointerPosition);
|
||||||
detailLines.push_back(
|
detailLines.push_back(
|
||||||
"Last input: " +
|
"Last input: " +
|
||||||
inputDebug.lastEventType +
|
inputDebug.lastEventType +
|
||||||
" at " +
|
eventPosition);
|
||||||
FormatPoint(inputDebug.pointerPosition));
|
|
||||||
detailLines.push_back(
|
detailLines.push_back(
|
||||||
"Route: " +
|
"Route: " +
|
||||||
inputDebug.lastTargetKind +
|
inputDebug.lastTargetKind +
|
||||||
" -> " +
|
" -> " +
|
||||||
ExtractStateKeyTail(inputDebug.lastTargetStateKey));
|
ExtractStateKeyTail(inputDebug.lastTargetStateKey));
|
||||||
detailLines.push_back(
|
detailLines.push_back(
|
||||||
"Result: " +
|
"Last event result: " +
|
||||||
(inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult));
|
(inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,13 +526,15 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
|
|||||||
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
|
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
|
||||||
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u));
|
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u));
|
||||||
} else {
|
} else {
|
||||||
detailLines.push_back("Screenshots: manual only (F12)");
|
detailLines.push_back("Screenshots: F12 -> new_editor/captures/");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_runtimeError.empty()) {
|
if (!m_runtimeError.empty()) {
|
||||||
detailLines.push_back(TruncateText(m_runtimeError, 78u));
|
detailLines.push_back(TruncateText(m_runtimeError, 78u));
|
||||||
} else if (!m_autoScreenshot.GetLastCaptureError().empty()) {
|
} else if (!m_autoScreenshot.GetLastCaptureError().empty()) {
|
||||||
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureError(), 78u));
|
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureError(), 78u));
|
||||||
|
} else if (!authoredMode) {
|
||||||
|
detailLines.push_back("No fallback sandbox is rendered in this host.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const float panelHeight = 38.0f + static_cast<float>(detailLines.size()) * 18.0f;
|
const float panelHeight = 38.0f + static_cast<float>(detailLines.size()) * 18.0f;
|
||||||
@@ -466,7 +549,7 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
|
|||||||
4.0f);
|
4.0f);
|
||||||
overlay.AddText(
|
overlay.AddText(
|
||||||
UIPoint(panelRect.x + 28.0f, panelRect.y + 10.0f),
|
UIPoint(panelRect.x + 28.0f, panelRect.y + 10.0f),
|
||||||
m_runtimeStatus.empty() ? "Runtime State" : m_runtimeStatus,
|
m_runtimeStatus.empty() ? "XCUI Editor Shell" : m_runtimeStatus,
|
||||||
kOverlayTextPrimary,
|
kOverlayTextPrimary,
|
||||||
14.0f);
|
14.0f);
|
||||||
|
|
||||||
@@ -484,8 +567,12 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path Application::ResolveRepoRelativePath(const char* relativePath) {
|
std::filesystem::path Application::ResolveRepoRootPath() {
|
||||||
return (std::filesystem::path(XCNEWEDITOR_REPO_ROOT) / relativePath).lexically_normal();
|
std::string root = XCNEWEDITOR_REPO_ROOT;
|
||||||
|
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||||
|
root = root.substr(1u, root.size() - 2u);
|
||||||
|
}
|
||||||
|
return std::filesystem::path(root).lexically_normal();
|
||||||
}
|
}
|
||||||
|
|
||||||
LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||||
@@ -557,9 +644,40 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case WM_SETFOCUS:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->m_inputModifierTracker.SyncFromSystemState();
|
||||||
|
application->QueueWindowFocusEvent(UIInputEventType::FocusGained);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_KILLFOCUS:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->m_inputModifierTracker.Reset();
|
||||||
|
application->QueueWindowFocusEvent(UIInputEventType::FocusLost);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case WM_KEYDOWN:
|
case WM_KEYDOWN:
|
||||||
if (application != nullptr && wParam == VK_F12) {
|
case WM_SYSKEYDOWN:
|
||||||
application->m_autoScreenshot.RequestCapture("manual_f12");
|
if (application != nullptr) {
|
||||||
|
if (wParam == VK_F12) {
|
||||||
|
application->m_autoScreenshot.RequestCapture("manual_f12");
|
||||||
|
}
|
||||||
|
application->QueueKeyEvent(UIInputEventType::KeyDown, wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_KEYUP:
|
||||||
|
case WM_SYSKEYUP:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->QueueKeyEvent(UIInputEventType::KeyUp, wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_CHAR:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->QueueCharacterEvent(wParam, lParam);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -579,9 +697,8 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
|
|||||||
}
|
}
|
||||||
|
|
||||||
int RunNewEditor(HINSTANCE hInstance, int nCmdShow) {
|
int RunNewEditor(HINSTANCE hInstance, int nCmdShow) {
|
||||||
Application application = {};
|
Application application;
|
||||||
return application.Run(hInstance, nCmdShow);
|
return application.Run(hInstance, nCmdShow);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace NewEditor
|
} // namespace XCEngine::NewEditor
|
||||||
} // namespace XCEngine
|
|
||||||
@@ -4,8 +4,11 @@
|
|||||||
#define NOMINMAX
|
#define NOMINMAX
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "AutoScreenshot.h"
|
#include <XCNewEditor/Host/AutoScreenshot.h>
|
||||||
#include "NativeRenderer.h"
|
#include <XCNewEditor/Host/InputModifierTracker.h>
|
||||||
|
#include <XCNewEditor/Host/NativeRenderer.h>
|
||||||
|
|
||||||
|
#include "editor/EditorShellAsset.h"
|
||||||
|
|
||||||
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||||
@@ -19,8 +22,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine::NewEditor {
|
||||||
namespace NewEditor {
|
|
||||||
|
|
||||||
class Application {
|
class Application {
|
||||||
public:
|
public:
|
||||||
@@ -44,27 +46,32 @@ private:
|
|||||||
void QueuePointerEvent(::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, WPARAM wParam, LPARAM lParam);
|
void QueuePointerEvent(::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, WPARAM wParam, LPARAM lParam);
|
||||||
void QueuePointerLeaveEvent();
|
void QueuePointerLeaveEvent();
|
||||||
void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam);
|
void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam);
|
||||||
|
void QueueKeyEvent(::XCEngine::UI::UIInputEventType type, WPARAM wParam, LPARAM lParam);
|
||||||
|
void QueueCharacterEvent(WPARAM wParam, LPARAM lParam);
|
||||||
|
void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type);
|
||||||
bool LoadStructuredScreen(const char* triggerReason);
|
bool LoadStructuredScreen(const char* triggerReason);
|
||||||
void RefreshStructuredScreen();
|
void RefreshStructuredScreen();
|
||||||
void RebuildTrackedFileStates();
|
void RebuildTrackedFileStates();
|
||||||
bool DetectTrackedFileChange() const;
|
bool DetectTrackedFileChange() const;
|
||||||
void AppendRuntimeOverlay(::XCEngine::UI::UIDrawData& drawData, float width, float height) const;
|
void AppendRuntimeOverlay(::XCEngine::UI::UIDrawData& drawData, float width, float height) const;
|
||||||
static std::filesystem::path ResolveRepoRelativePath(const char* relativePath);
|
static std::filesystem::path ResolveRepoRootPath();
|
||||||
|
|
||||||
HWND m_hwnd = nullptr;
|
HWND m_hwnd = nullptr;
|
||||||
HINSTANCE m_hInstance = nullptr;
|
HINSTANCE m_hInstance = nullptr;
|
||||||
ATOM m_windowClassAtom = 0;
|
ATOM m_windowClassAtom = 0;
|
||||||
NativeRenderer m_renderer;
|
::XCEngine::XCUI::Host::NativeRenderer m_renderer;
|
||||||
AutoScreenshotController m_autoScreenshot;
|
::XCEngine::XCUI::Host::AutoScreenshotController m_autoScreenshot;
|
||||||
::XCEngine::UI::Runtime::UIDocumentScreenHost m_documentHost;
|
::XCEngine::UI::Runtime::UIDocumentScreenHost m_documentHost;
|
||||||
::XCEngine::UI::Runtime::UIScreenPlayer m_screenPlayer;
|
::XCEngine::UI::Runtime::UIScreenPlayer m_screenPlayer;
|
||||||
::XCEngine::UI::Runtime::UIScreenAsset m_screenAsset = {};
|
::XCEngine::UI::Runtime::UIScreenAsset m_screenAsset = {};
|
||||||
|
EditorShellAsset m_shellAssetDefinition = {};
|
||||||
std::vector<TrackedFileState> m_trackedFiles = {};
|
std::vector<TrackedFileState> m_trackedFiles = {};
|
||||||
std::chrono::steady_clock::time_point m_startTime = {};
|
std::chrono::steady_clock::time_point m_startTime = {};
|
||||||
std::chrono::steady_clock::time_point m_lastFrameTime = {};
|
std::chrono::steady_clock::time_point m_lastFrameTime = {};
|
||||||
std::chrono::steady_clock::time_point m_lastReloadPollTime = {};
|
std::chrono::steady_clock::time_point m_lastReloadPollTime = {};
|
||||||
std::uint64_t m_frameIndex = 0;
|
std::uint64_t m_frameIndex = 0;
|
||||||
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
|
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
|
||||||
|
::XCEngine::XCUI::Host::InputModifierTracker m_inputModifierTracker = {};
|
||||||
bool m_trackingMouseLeave = false;
|
bool m_trackingMouseLeave = false;
|
||||||
bool m_useStructuredScreen = false;
|
bool m_useStructuredScreen = false;
|
||||||
std::string m_runtimeStatus = {};
|
std::string m_runtimeStatus = {};
|
||||||
@@ -73,5 +80,4 @@ private:
|
|||||||
|
|
||||||
int RunNewEditor(HINSTANCE hInstance, int nCmdShow);
|
int RunNewEditor(HINSTANCE hInstance, int nCmdShow);
|
||||||
|
|
||||||
} // namespace NewEditor
|
} // namespace XCEngine::NewEditor
|
||||||
} // namespace XCEngine
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "AutoScreenshot.h"
|
#include <XCNewEditor/Host/AutoScreenshot.h>
|
||||||
|
|
||||||
#include "NativeRenderer.h"
|
#include <XCNewEditor/Host/NativeRenderer.h>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -8,8 +8,7 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine::XCUI::Host {
|
||||||
namespace NewEditor {
|
|
||||||
|
|
||||||
void AutoScreenshotController::Initialize(const std::filesystem::path& captureRoot) {
|
void AutoScreenshotController::Initialize(const std::filesystem::path& captureRoot) {
|
||||||
m_captureRoot = captureRoot.lexically_normal();
|
m_captureRoot = captureRoot.lexically_normal();
|
||||||
@@ -144,5 +143,4 @@ std::string AutoScreenshotController::SanitizeReason(std::string_view reason) {
|
|||||||
return sanitized.empty() ? "capture" : sanitized;
|
return sanitized.empty() ? "capture" : sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace NewEditor
|
} // namespace XCEngine::XCUI::Host
|
||||||
} // namespace XCEngine
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
#include "NativeRenderer.h"
|
#include <XCNewEditor/Host/NativeRenderer.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine::XCUI::Host {
|
||||||
namespace NewEditor {
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -483,5 +482,4 @@ std::wstring NativeRenderer::Utf8ToWide(std::string_view text) {
|
|||||||
return wideText;
|
return wideText;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace NewEditor
|
} // namespace XCEngine::XCUI::Host
|
||||||
} // namespace XCEngine
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
#include "SandboxFrameBuilder.h"
|
|
||||||
|
|
||||||
#include <XCEngine/UI/Widgets/UIEditorPanelChrome.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
|
|
||||||
namespace XCEngine {
|
|
||||||
namespace NewEditor {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
using ::XCEngine::UI::UIColor;
|
|
||||||
using ::XCEngine::UI::UIDrawData;
|
|
||||||
using ::XCEngine::UI::UIDrawList;
|
|
||||||
using ::XCEngine::UI::UIPoint;
|
|
||||||
using ::XCEngine::UI::UIRect;
|
|
||||||
using ::XCEngine::UI::Widgets::AppendUIEditorPanelChromeBackground;
|
|
||||||
using ::XCEngine::UI::Widgets::AppendUIEditorPanelChromeForeground;
|
|
||||||
using ::XCEngine::UI::Widgets::UIEditorPanelChromeMetrics;
|
|
||||||
using ::XCEngine::UI::Widgets::UIEditorPanelChromeState;
|
|
||||||
using ::XCEngine::UI::Widgets::UIEditorPanelChromeText;
|
|
||||||
|
|
||||||
constexpr UIColor kWorkspaceColor(0.05f, 0.07f, 0.09f, 1.0f);
|
|
||||||
constexpr UIColor kShellColor(0.08f, 0.10f, 0.13f, 1.0f);
|
|
||||||
constexpr UIColor kSubtleLineColor(0.18f, 0.23f, 0.29f, 1.0f);
|
|
||||||
constexpr UIColor kTextPrimary(0.92f, 0.95f, 0.98f, 1.0f);
|
|
||||||
constexpr UIColor kTextSecondary(0.67f, 0.74f, 0.81f, 1.0f);
|
|
||||||
constexpr UIColor kTextMuted(0.49f, 0.56f, 0.64f, 1.0f);
|
|
||||||
constexpr UIColor kSelectionColor(0.16f, 0.37f, 0.64f, 0.90f);
|
|
||||||
constexpr UIColor kAccentColor(0.19f, 0.76f, 0.57f, 1.0f);
|
|
||||||
constexpr UIColor kWarningColor(0.96f, 0.72f, 0.22f, 1.0f);
|
|
||||||
constexpr UIColor kErrorColor(0.88f, 0.29f, 0.30f, 1.0f);
|
|
||||||
|
|
||||||
float ClampPositive(float value, float fallback) {
|
|
||||||
return value > 1.0f ? value : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
float Pulse01(double timeSeconds, double speed) {
|
|
||||||
const double phase = std::sin(timeSeconds * speed);
|
|
||||||
return static_cast<float>(0.5 + phase * 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
UIRect InsetRect(const UIRect& rect, float insetX, float insetY) {
|
|
||||||
return UIRect(
|
|
||||||
rect.x + insetX,
|
|
||||||
rect.y + insetY,
|
|
||||||
(std::max)(0.0f, rect.width - insetX * 2.0f),
|
|
||||||
(std::max)(0.0f, rect.height - insetY * 2.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddSectionTitle(
|
|
||||||
UIDrawList& drawList,
|
|
||||||
float x,
|
|
||||||
float y,
|
|
||||||
std::string_view title,
|
|
||||||
std::string_view subtitle = {}) {
|
|
||||||
drawList.AddText(UIPoint(x, y), std::string(title), kTextPrimary, 17.0f);
|
|
||||||
if (!subtitle.empty()) {
|
|
||||||
drawList.AddText(UIPoint(x, y + 20.0f), std::string(subtitle), kTextMuted, 13.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddRow(
|
|
||||||
UIDrawList& drawList,
|
|
||||||
const UIRect& rect,
|
|
||||||
std::string_view label,
|
|
||||||
bool selected = false,
|
|
||||||
float indent = 0.0f,
|
|
||||||
const UIColor& dotColor = kAccentColor) {
|
|
||||||
if (selected) {
|
|
||||||
drawList.AddFilledRect(rect, kSelectionColor, 6.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawList.AddFilledRect(
|
|
||||||
UIRect(rect.x + 10.0f + indent, rect.y + 10.0f, 8.0f, 8.0f),
|
|
||||||
dotColor,
|
|
||||||
4.0f);
|
|
||||||
drawList.AddText(
|
|
||||||
UIPoint(rect.x + 26.0f + indent, rect.y + 6.0f),
|
|
||||||
std::string(label),
|
|
||||||
selected ? kTextPrimary : kTextSecondary,
|
|
||||||
14.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddPropertyRow(
|
|
||||||
UIDrawList& drawList,
|
|
||||||
const UIRect& rect,
|
|
||||||
std::string_view name,
|
|
||||||
std::string_view value) {
|
|
||||||
drawList.AddText(UIPoint(rect.x, rect.y + 6.0f), std::string(name), kTextSecondary, 14.0f);
|
|
||||||
|
|
||||||
const UIRect fieldRect(rect.x + rect.width * 0.42f, rect.y, rect.width * 0.58f, rect.height);
|
|
||||||
drawList.AddFilledRect(fieldRect, UIColor(0.11f, 0.15f, 0.20f, 1.0f), 6.0f);
|
|
||||||
drawList.AddRectOutline(fieldRect, kSubtleLineColor, 1.0f, 6.0f);
|
|
||||||
drawList.AddText(
|
|
||||||
UIPoint(fieldRect.x + 10.0f, fieldRect.y + 6.0f),
|
|
||||||
std::string(value),
|
|
||||||
kTextPrimary,
|
|
||||||
14.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddLogRow(
|
|
||||||
UIDrawList& drawList,
|
|
||||||
const UIRect& rect,
|
|
||||||
const UIColor& levelColor,
|
|
||||||
std::string_view level,
|
|
||||||
std::string_view message) {
|
|
||||||
drawList.AddFilledRect(
|
|
||||||
UIRect(rect.x, rect.y + 7.0f, 6.0f, rect.height - 14.0f),
|
|
||||||
levelColor,
|
|
||||||
3.0f);
|
|
||||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 4.0f), std::string(level), levelColor, 13.0f);
|
|
||||||
drawList.AddText(UIPoint(rect.x + 82.0f, rect.y + 4.0f), std::string(message), kTextSecondary, 13.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendPanel(
|
|
||||||
UIDrawList& drawList,
|
|
||||||
const UIRect& panelRect,
|
|
||||||
std::string_view title,
|
|
||||||
std::string_view subtitle,
|
|
||||||
std::string_view footer,
|
|
||||||
bool active) {
|
|
||||||
UIEditorPanelChromeState state = {};
|
|
||||||
state.active = active;
|
|
||||||
state.hovered = active;
|
|
||||||
|
|
||||||
UIEditorPanelChromeText text = {};
|
|
||||||
text.title = title;
|
|
||||||
text.subtitle = subtitle;
|
|
||||||
text.footer = footer;
|
|
||||||
|
|
||||||
UIEditorPanelChromeMetrics metrics = {};
|
|
||||||
metrics.headerHeight = 46.0f;
|
|
||||||
|
|
||||||
AppendUIEditorPanelChromeBackground(drawList, panelRect, state, {}, metrics);
|
|
||||||
AppendUIEditorPanelChromeForeground(drawList, panelRect, text, {}, metrics);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendTopBar(UIDrawList& drawList, const UIRect& rect, double timeSeconds) {
|
|
||||||
drawList.AddFilledRect(rect, kShellColor, 0.0f);
|
|
||||||
drawList.AddRectOutline(rect, kSubtleLineColor, 1.0f, 0.0f);
|
|
||||||
drawList.AddText(
|
|
||||||
UIPoint(rect.x + 20.0f, rect.y + 14.0f),
|
|
||||||
"XCUI Native Sandbox",
|
|
||||||
kTextPrimary,
|
|
||||||
20.0f);
|
|
||||||
drawList.AddText(
|
|
||||||
UIPoint(rect.x + 228.0f, rect.y + 17.0f),
|
|
||||||
"editor ui proving ground",
|
|
||||||
kTextMuted,
|
|
||||||
13.0f);
|
|
||||||
|
|
||||||
const float pulse = Pulse01(timeSeconds, 2.2);
|
|
||||||
const UIColor liveColor(
|
|
||||||
0.18f + pulse * 0.16f,
|
|
||||||
0.74f,
|
|
||||||
0.58f,
|
|
||||||
1.0f);
|
|
||||||
drawList.AddFilledRect(
|
|
||||||
UIRect(rect.x + rect.width - 170.0f, rect.y + 12.0f, 108.0f, 24.0f),
|
|
||||||
UIColor(0.10f, 0.16f, 0.12f, 1.0f),
|
|
||||||
12.0f);
|
|
||||||
drawList.AddFilledRect(
|
|
||||||
UIRect(rect.x + rect.width - 160.0f, rect.y + 20.0f, 8.0f, 8.0f),
|
|
||||||
liveColor,
|
|
||||||
4.0f);
|
|
||||||
drawList.AddText(
|
|
||||||
UIPoint(rect.x + rect.width - 146.0f, rect.y + 14.0f),
|
|
||||||
"Native Render",
|
|
||||||
kTextPrimary,
|
|
||||||
13.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendStatusBar(UIDrawList& drawList, const UIRect& rect, const SandboxFrameOptions& options) {
|
|
||||||
drawList.AddFilledRect(rect, kShellColor, 0.0f);
|
|
||||||
drawList.AddRectOutline(rect, kSubtleLineColor, 1.0f, 0.0f);
|
|
||||||
|
|
||||||
const std::string leftText =
|
|
||||||
"Direct renderer | size " +
|
|
||||||
std::to_string(static_cast<int>(options.width)) +
|
|
||||||
"x" +
|
|
||||||
std::to_string(static_cast<int>(options.height));
|
|
||||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 8.0f), leftText, kTextSecondary, 13.0f);
|
|
||||||
drawList.AddText(
|
|
||||||
UIPoint(rect.x + rect.width - 176.0f, rect.y + 8.0f),
|
|
||||||
"No ImGui Host",
|
|
||||||
kAccentColor,
|
|
||||||
13.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendHierarchyPanel(UIDrawList& drawList, const UIRect& panelRect) {
|
|
||||||
AppendPanel(drawList, panelRect, "Hierarchy", "scene graph", "12 items", false);
|
|
||||||
|
|
||||||
const UIRect body = InsetRect(panelRect, 16.0f, 60.0f);
|
|
||||||
AddSectionTitle(drawList, body.x, body.y, "Active Scene", "Sandbox_City.xcscene");
|
|
||||||
|
|
||||||
float rowY = body.y + 42.0f;
|
|
||||||
const float rowHeight = 28.0f;
|
|
||||||
AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "World", false, 0.0f, kWarningColor);
|
|
||||||
rowY += rowHeight + 4.0f;
|
|
||||||
AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Main Camera", false, 18.0f);
|
|
||||||
rowY += rowHeight + 4.0f;
|
|
||||||
AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Directional Light", false, 18.0f, kWarningColor);
|
|
||||||
rowY += rowHeight + 4.0f;
|
|
||||||
AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Player", true, 18.0f);
|
|
||||||
rowY += rowHeight + 4.0f;
|
|
||||||
AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "WeaponSocket", false, 36.0f, UIColor(0.58f, 0.70f, 0.88f, 1.0f));
|
|
||||||
rowY += rowHeight + 4.0f;
|
|
||||||
AddRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "FX_Trail", false, 36.0f, UIColor(0.76f, 0.43f, 0.93f, 1.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendInspectorPanel(UIDrawList& drawList, const UIRect& panelRect) {
|
|
||||||
AppendPanel(drawList, panelRect, "Inspector", "Player", "schema-first draft", false);
|
|
||||||
|
|
||||||
const UIRect body = InsetRect(panelRect, 16.0f, 60.0f);
|
|
||||||
AddSectionTitle(drawList, body.x, body.y, "Transform", "shared widget experiment");
|
|
||||||
|
|
||||||
float rowY = body.y + 42.0f;
|
|
||||||
const float rowHeight = 30.0f;
|
|
||||||
AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Position", "12.0, 1.8, -4.0");
|
|
||||||
rowY += rowHeight + 6.0f;
|
|
||||||
AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Rotation", "0.0, 36.5, 0.0");
|
|
||||||
rowY += rowHeight + 6.0f;
|
|
||||||
AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Scale", "1.0, 1.0, 1.0");
|
|
||||||
rowY += 48.0f;
|
|
||||||
|
|
||||||
AddSectionTitle(drawList, body.x, rowY, "Character", "future AutoForm target");
|
|
||||||
rowY += 42.0f;
|
|
||||||
AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "State", "Locomotion");
|
|
||||||
rowY += rowHeight + 6.0f;
|
|
||||||
AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Move Speed", "6.4");
|
|
||||||
rowY += rowHeight + 6.0f;
|
|
||||||
AddPropertyRow(drawList, UIRect(body.x, rowY, body.width, rowHeight), "Jump Height", "1.3");
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendScenePanel(UIDrawList& drawList, const UIRect& panelRect, double timeSeconds) {
|
|
||||||
AppendPanel(drawList, panelRect, "Scene", "native shell draft", "ViewportSlot comes later", true);
|
|
||||||
|
|
||||||
const UIRect body = InsetRect(panelRect, 16.0f, 60.0f);
|
|
||||||
drawList.AddFilledRect(body, UIColor(0.07f, 0.09f, 0.11f, 1.0f), 12.0f);
|
|
||||||
drawList.AddRectOutline(body, kSubtleLineColor, 1.0f, 12.0f);
|
|
||||||
|
|
||||||
const float gridStep = 34.0f;
|
|
||||||
for (float x = body.x; x < body.x + body.width; x += gridStep) {
|
|
||||||
drawList.AddRectOutline(UIRect(x, body.y, 1.0f, body.height), UIColor(0.10f, 0.13f, 0.16f, 0.6f));
|
|
||||||
}
|
|
||||||
for (float y = body.y; y < body.y + body.height; y += gridStep) {
|
|
||||||
drawList.AddRectOutline(UIRect(body.x, y, body.width, 1.0f), UIColor(0.10f, 0.13f, 0.16f, 0.6f));
|
|
||||||
}
|
|
||||||
|
|
||||||
const float pulse = Pulse01(timeSeconds, 1.8);
|
|
||||||
const UIRect selectionRect(
|
|
||||||
body.x + body.width * 0.32f,
|
|
||||||
body.y + body.height * 0.24f,
|
|
||||||
body.width * 0.22f,
|
|
||||||
body.height * 0.34f);
|
|
||||||
drawList.AddRectOutline(
|
|
||||||
selectionRect,
|
|
||||||
UIColor(0.24f + pulse * 0.12f, 0.56f, 0.95f, 1.0f),
|
|
||||||
2.0f,
|
|
||||||
8.0f);
|
|
||||||
drawList.AddText(
|
|
||||||
UIPoint(body.x + 18.0f, body.y + 18.0f),
|
|
||||||
"This area is intentionally native-rendered, not hosted by ImGui.",
|
|
||||||
kTextSecondary,
|
|
||||||
15.0f);
|
|
||||||
drawList.AddText(
|
|
||||||
UIPoint(body.x + 18.0f, body.y + 40.0f),
|
|
||||||
"Use this sandbox to iterate shell chrome, panel composition, and draw packets.",
|
|
||||||
kTextMuted,
|
|
||||||
13.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendConsolePanel(UIDrawList& drawList, const UIRect& panelRect) {
|
|
||||||
AppendPanel(drawList, panelRect, "Console", "native renderer smoke log", "6 records", false);
|
|
||||||
|
|
||||||
const UIRect body = InsetRect(panelRect, 16.0f, 60.0f);
|
|
||||||
AddLogRow(drawList, UIRect(body.x, body.y, body.width, 22.0f), kAccentColor, "Info", "XCUI native sandbox frame submitted.");
|
|
||||||
AddLogRow(drawList, UIRect(body.x, body.y + 26.0f, body.width, 22.0f), kWarningColor, "Warn", "Viewport host not connected in this phase.");
|
|
||||||
AddLogRow(drawList, UIRect(body.x, body.y + 52.0f, body.width, 22.0f), kTextSecondary, "Info", "Hierarchy / Inspector are draw-packet prototypes.");
|
|
||||||
AddLogRow(drawList, UIRect(body.x, body.y + 78.0f, body.width, 22.0f), kErrorColor, "Todo", "Replace bespoke scene panel with authored shell document.");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
UIDrawData BuildSandboxFrame(const SandboxFrameOptions& options) {
|
|
||||||
const float width = ClampPositive(options.width, 1280.0f);
|
|
||||||
const float height = ClampPositive(options.height, 720.0f);
|
|
||||||
|
|
||||||
const float margin = 18.0f;
|
|
||||||
const float gap = 14.0f;
|
|
||||||
const float topBarHeight = 48.0f;
|
|
||||||
const float statusBarHeight = 30.0f;
|
|
||||||
const float leftPanelWidth = 260.0f;
|
|
||||||
const float rightPanelWidth = 320.0f;
|
|
||||||
const float bottomPanelHeight = 210.0f;
|
|
||||||
|
|
||||||
const UIRect fullRect(0.0f, 0.0f, width, height);
|
|
||||||
const UIRect topBarRect(0.0f, 0.0f, width, topBarHeight);
|
|
||||||
const UIRect statusRect(0.0f, height - statusBarHeight, width, statusBarHeight);
|
|
||||||
|
|
||||||
const float workspaceTop = topBarHeight + margin;
|
|
||||||
const float workspaceBottom = height - statusBarHeight - margin;
|
|
||||||
const float workspaceHeight = (std::max)(0.0f, workspaceBottom - workspaceTop);
|
|
||||||
const float centerX = margin + leftPanelWidth + gap;
|
|
||||||
const float centerWidth = (std::max)(240.0f, width - margin * 2.0f - leftPanelWidth - rightPanelWidth - gap * 2.0f);
|
|
||||||
const float sceneHeight = (std::max)(220.0f, workspaceHeight - bottomPanelHeight - gap);
|
|
||||||
|
|
||||||
const UIRect hierarchyRect(margin, workspaceTop, leftPanelWidth, workspaceHeight);
|
|
||||||
const UIRect sceneRect(centerX, workspaceTop, centerWidth, sceneHeight);
|
|
||||||
const UIRect consoleRect(centerX, workspaceTop + sceneHeight + gap, centerWidth, bottomPanelHeight);
|
|
||||||
const UIRect inspectorRect(centerX + centerWidth + gap, workspaceTop, rightPanelWidth, workspaceHeight);
|
|
||||||
|
|
||||||
UIDrawData drawData = {};
|
|
||||||
UIDrawList& drawList = drawData.EmplaceDrawList("XCUI Native Editor Sandbox");
|
|
||||||
drawList.PushClipRect(fullRect);
|
|
||||||
drawList.AddFilledRect(fullRect, kWorkspaceColor, 0.0f);
|
|
||||||
|
|
||||||
AppendTopBar(drawList, topBarRect, options.timeSeconds);
|
|
||||||
AppendHierarchyPanel(drawList, hierarchyRect);
|
|
||||||
AppendScenePanel(drawList, sceneRect, options.timeSeconds);
|
|
||||||
AppendConsolePanel(drawList, consoleRect);
|
|
||||||
AppendInspectorPanel(drawList, inspectorRect);
|
|
||||||
AppendStatusBar(drawList, statusRect, options);
|
|
||||||
|
|
||||||
drawList.PopClipRect();
|
|
||||||
return drawData;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace NewEditor
|
|
||||||
} // namespace XCEngine
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <XCEngine/UI/DrawData.h>
|
|
||||||
|
|
||||||
namespace XCEngine {
|
|
||||||
namespace NewEditor {
|
|
||||||
|
|
||||||
struct SandboxFrameOptions {
|
|
||||||
float width = 1280.0f;
|
|
||||||
float height = 720.0f;
|
|
||||||
double timeSeconds = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
::XCEngine::UI::UIDrawData BuildSandboxFrame(const SandboxFrameOptions& options);
|
|
||||||
|
|
||||||
} // namespace NewEditor
|
|
||||||
} // namespace XCEngine
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
|
#include <XCNewEditor/Widgets/UIEditorCollectionPrimitives.h>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace UI {
|
namespace UI {
|
||||||
13
new_editor/src/editor/EditorShellAsset.cpp
Normal file
13
new_editor/src/editor/EditorShellAsset.cpp
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#include "EditorShellAsset.h"
|
||||||
|
|
||||||
|
namespace XCEngine::NewEditor {
|
||||||
|
|
||||||
|
EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot) {
|
||||||
|
EditorShellAsset asset = {};
|
||||||
|
asset.documentPath = (repoRoot / "new_editor/ui/views/editor_shell.xcui").lexically_normal();
|
||||||
|
asset.themePath = (repoRoot / "new_editor/ui/themes/editor_shell.xctheme").lexically_normal();
|
||||||
|
asset.captureRootPath = (repoRoot / "new_editor/captures").lexically_normal();
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::NewEditor
|
||||||
17
new_editor/src/editor/EditorShellAsset.h
Normal file
17
new_editor/src/editor/EditorShellAsset.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace XCEngine::NewEditor {
|
||||||
|
|
||||||
|
struct EditorShellAsset {
|
||||||
|
std::string screenId = "new_editor.shell";
|
||||||
|
std::filesystem::path documentPath = {};
|
||||||
|
std::filesystem::path themePath = {};
|
||||||
|
std::filesystem::path captureRootPath = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot);
|
||||||
|
|
||||||
|
} // namespace XCEngine::NewEditor
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "Application.h"
|
#include "Host/Application.h"
|
||||||
|
|
||||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||||
return XCEngine::NewEditor::RunNewEditor(hInstance, nCmdShow);
|
return XCEngine::NewEditor::RunNewEditor(hInstance, nCmdShow);
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<Schema name="EditorInspectorShell">
|
|
||||||
<Element tag="InspectorPanel" allowUnknownAttributes="true">
|
|
||||||
<Attribute name="vm" type="string" />
|
|
||||||
<Attribute name="schema" type="document" documentKind="schema" />
|
|
||||||
|
|
||||||
<Element tag="Section" allowUnknownAttributes="true">
|
|
||||||
<Attribute name="title" type="string" />
|
|
||||||
<Attribute name="subtitle" type="string" />
|
|
||||||
|
|
||||||
<Element tag="Field" allowUnknownAttributes="true">
|
|
||||||
<Attribute name="label" type="string" />
|
|
||||||
<Attribute name="binding" type="string" />
|
|
||||||
<Attribute
|
|
||||||
name="widget"
|
|
||||||
type="enum"
|
|
||||||
values="Vector3Field,FloatField,EnumField,AssetField" />
|
|
||||||
<Attribute name="step" type="number" />
|
|
||||||
</Element>
|
|
||||||
</Element>
|
|
||||||
</Element>
|
|
||||||
</Schema>
|
|
||||||
@@ -1,30 +1,8 @@
|
|||||||
<View
|
<View
|
||||||
name="NewEditorShell"
|
name="NewEditorShell"
|
||||||
theme="../themes/editor_shell.xctheme">
|
theme="../themes/editor_shell.xctheme">
|
||||||
<Column padding="24" gap="16">
|
<Column
|
||||||
<Card
|
id="editor-shell-root"
|
||||||
title="XCUI Core Validation"
|
width="fill"
|
||||||
subtitle="Current batch: input state routing | minimal sandbox | native renderer"
|
height="fill" />
|
||||||
tone="accent"
|
|
||||||
height="90">
|
|
||||||
<Column gap="8">
|
|
||||||
<Text text="这个试验面板只保留当前批次需要检查的控件。" />
|
|
||||||
<Text text="无关的 editor 壳层面板暂时不放进来,避免干扰检查。" />
|
|
||||||
</Column>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="Input Core" subtitle="hover focus active capture" height="196">
|
|
||||||
<Column gap="12">
|
|
||||||
<Text text="这一轮只需要检查下面这三个按钮。" />
|
|
||||||
<Row gap="12">
|
|
||||||
<Button id="input-hover" text="Hover / Focus" />
|
|
||||||
<Button id="input-capture" text="Pointer Capture" capturePointer="true" />
|
|
||||||
<Button id="input-route" text="Route Target" />
|
|
||||||
</Row>
|
|
||||||
<Text text="1. 鼠标移到左侧按钮:hover 应该变化,focus 应该保持空。" />
|
|
||||||
<Text text="2. 按住中间按钮不放:focus、active、capture 都应该停在中间按钮。" />
|
|
||||||
<Text text="3. 按住中间按钮拖到右侧再松开:hover 移到右侧,capture 清空,focus 仍留在中间。" />
|
|
||||||
</Column>
|
|
||||||
</Card>
|
|
||||||
</Column>
|
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ enable_testing()
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
add_subdirectory(Core)
|
add_subdirectory(Core)
|
||||||
|
add_subdirectory(UI)
|
||||||
add_subdirectory(Memory)
|
add_subdirectory(Memory)
|
||||||
add_subdirectory(Threading)
|
add_subdirectory(Threading)
|
||||||
add_subdirectory(Debug)
|
add_subdirectory(Debug)
|
||||||
@@ -43,9 +44,6 @@ add_subdirectory(RHI)
|
|||||||
add_subdirectory(Resources)
|
add_subdirectory(Resources)
|
||||||
add_subdirectory(Input)
|
add_subdirectory(Input)
|
||||||
add_subdirectory(Editor)
|
add_subdirectory(Editor)
|
||||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/NewEditor")
|
|
||||||
add_subdirectory(NewEditor)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
find_program(XCENGINE_POWERSHELL_EXECUTABLE NAMES powershell pwsh REQUIRED)
|
find_program(XCENGINE_POWERSHELL_EXECUTABLE NAMES powershell pwsh REQUIRED)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
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_panel_chrome.cpp
|
|
||||||
test_ui_expansion_model.cpp
|
test_ui_expansion_model.cpp
|
||||||
test_ui_flat_hierarchy_helpers.cpp
|
test_ui_flat_hierarchy_helpers.cpp
|
||||||
test_ui_input_dispatcher.cpp
|
test_ui_input_dispatcher.cpp
|
||||||
|
|||||||
7
tests/UI/CMakeLists.txt
Normal file
7
tests/UI/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
|
||||||
|
project(XCEngine_UITests)
|
||||||
|
|
||||||
|
add_subdirectory(Core)
|
||||||
|
add_subdirectory(Runtime)
|
||||||
|
add_subdirectory(Editor)
|
||||||
17
tests/UI/Core/CMakeLists.txt
Normal file
17
tests/UI/Core/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
|
||||||
|
project(XCEngine_CoreUITests)
|
||||||
|
|
||||||
|
add_subdirectory(unit)
|
||||||
|
add_subdirectory(integration)
|
||||||
|
|
||||||
|
add_custom_target(core_ui_unit_tests
|
||||||
|
DEPENDS
|
||||||
|
core_ui_tests
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(core_ui_all_tests
|
||||||
|
DEPENDS
|
||||||
|
core_ui_unit_tests
|
||||||
|
core_ui_integration_tests
|
||||||
|
)
|
||||||
1
tests/UI/Core/integration/CMakeLists.txt
Normal file
1
tests/UI/Core/integration/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
add_custom_target(core_ui_integration_tests)
|
||||||
8
tests/UI/Core/integration/README.md
Normal file
8
tests/UI/Core/integration/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Core UI Integration Notes
|
||||||
|
|
||||||
|
The core XCUI lane currently validates shared primitives through automated unit tests.
|
||||||
|
|
||||||
|
Interactive validation belongs to:
|
||||||
|
|
||||||
|
- `tests/UI/Runtime/integration/` for game/runtime UI
|
||||||
|
- `tests/UI/Editor/integration/` for editor UI
|
||||||
40
tests/UI/Core/unit/CMakeLists.txt
Normal file
40
tests/UI/Core/unit/CMakeLists.txt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
set(CORE_UI_TEST_SOURCES
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_shortcut_scope.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_splitter_layout.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_splitter_interaction.cpp
|
||||||
|
# Migration bridge: legacy XCUI unit coverage still lives under tests/Core/UI
|
||||||
|
# until it is moved into tests/UI/Core/unit without changing behavior.
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_core.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_expansion_model.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_flat_hierarchy_helpers.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_input_dispatcher.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_keyboard_navigation_model.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_property_edit_model.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_layout_engine.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_selection_model.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_text_editing.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_text_input_controller.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(core_ui_tests ${CORE_UI_TEST_SOURCES})
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
set_target_properties(core_ui_tests PROPERTIES
|
||||||
|
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(core_ui_tests
|
||||||
|
PRIVATE
|
||||||
|
XCEngine
|
||||||
|
GTest::gtest
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(core_ui_tests PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Fixtures
|
||||||
|
)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(core_ui_tests)
|
||||||
171
tests/UI/Core/unit/test_ui_shortcut_scope.cpp
Normal file
171
tests/UI/Core/unit/test_ui_shortcut_scope.cpp
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||||||
|
#include <XCEngine/UI/Input/UIShortcutRegistry.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIInputDispatchDecision;
|
||||||
|
using XCEngine::UI::UIInputDispatcher;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIInputPath;
|
||||||
|
using XCEngine::UI::UIShortcutBinding;
|
||||||
|
using XCEngine::UI::UIShortcutContext;
|
||||||
|
using XCEngine::UI::UIShortcutRegistry;
|
||||||
|
using XCEngine::UI::UIShortcutScope;
|
||||||
|
|
||||||
|
UIInputEvent MakeCtrlPEvent() {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
event.modifiers.control = true;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UIShortcutScopeTest, RegistryUsesCommandScopeInsteadOfActivePathForShortcutResolution) {
|
||||||
|
UIShortcutRegistry registry = {};
|
||||||
|
|
||||||
|
UIShortcutBinding dragWidgetBinding = {};
|
||||||
|
dragWidgetBinding.scope = UIShortcutScope::Widget;
|
||||||
|
dragWidgetBinding.ownerId = 91u;
|
||||||
|
dragWidgetBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
dragWidgetBinding.chord.modifiers.control = true;
|
||||||
|
dragWidgetBinding.commandId = "drag.widget.command";
|
||||||
|
registry.RegisterBinding(dragWidgetBinding);
|
||||||
|
|
||||||
|
UIShortcutBinding panelBinding = {};
|
||||||
|
panelBinding.scope = UIShortcutScope::Panel;
|
||||||
|
panelBinding.ownerId = 20u;
|
||||||
|
panelBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
panelBinding.chord.modifiers.control = true;
|
||||||
|
panelBinding.commandId = "panel.command";
|
||||||
|
registry.RegisterBinding(panelBinding);
|
||||||
|
|
||||||
|
UIShortcutContext context = {};
|
||||||
|
context.focusedPath = { 10u, 20u, 30u };
|
||||||
|
context.activePath = { 90u, 91u };
|
||||||
|
context.commandScope.path = context.focusedPath;
|
||||||
|
context.commandScope.windowId = 10u;
|
||||||
|
context.commandScope.panelId = 20u;
|
||||||
|
context.commandScope.widgetId = 30u;
|
||||||
|
|
||||||
|
const auto match = registry.Match(MakeCtrlPEvent(), context);
|
||||||
|
ASSERT_TRUE(match.matched);
|
||||||
|
EXPECT_EQ(match.binding.commandId, "panel.command");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIShortcutScopeTest, RegistryPrefersPanelThenWindowThenGlobalWithinFocusedCommandScope) {
|
||||||
|
UIShortcutRegistry registry = {};
|
||||||
|
|
||||||
|
UIShortcutBinding globalBinding = {};
|
||||||
|
globalBinding.scope = UIShortcutScope::Global;
|
||||||
|
globalBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
globalBinding.chord.modifiers.control = true;
|
||||||
|
globalBinding.commandId = "global.command";
|
||||||
|
registry.RegisterBinding(globalBinding);
|
||||||
|
|
||||||
|
UIShortcutBinding windowBinding = {};
|
||||||
|
windowBinding.scope = UIShortcutScope::Window;
|
||||||
|
windowBinding.ownerId = 10u;
|
||||||
|
windowBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
windowBinding.chord.modifiers.control = true;
|
||||||
|
windowBinding.commandId = "window.command";
|
||||||
|
registry.RegisterBinding(windowBinding);
|
||||||
|
|
||||||
|
UIShortcutBinding panelBinding = {};
|
||||||
|
panelBinding.scope = UIShortcutScope::Panel;
|
||||||
|
panelBinding.ownerId = 20u;
|
||||||
|
panelBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
panelBinding.chord.modifiers.control = true;
|
||||||
|
panelBinding.commandId = "panel.command";
|
||||||
|
registry.RegisterBinding(panelBinding);
|
||||||
|
|
||||||
|
UIShortcutContext context = {};
|
||||||
|
context.focusedPath = { 10u, 20u, 30u };
|
||||||
|
context.commandScope.path = context.focusedPath;
|
||||||
|
context.commandScope.windowId = 10u;
|
||||||
|
context.commandScope.panelId = 20u;
|
||||||
|
context.commandScope.widgetId = 30u;
|
||||||
|
|
||||||
|
const auto match = registry.Match(MakeCtrlPEvent(), context);
|
||||||
|
ASSERT_TRUE(match.matched);
|
||||||
|
EXPECT_EQ(match.binding.commandId, "panel.command");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIShortcutScopeTest, InputDispatcherConsumesShortcutBeforeRoutingWhenCommandScopeMatches) {
|
||||||
|
UIInputDispatcher dispatcher{};
|
||||||
|
dispatcher.GetFocusController().SetFocusedPath({ 10u, 20u, 30u });
|
||||||
|
|
||||||
|
UIShortcutContext shortcutContext = {};
|
||||||
|
shortcutContext.commandScope.path = { 10u, 20u, 30u };
|
||||||
|
shortcutContext.commandScope.windowId = 10u;
|
||||||
|
shortcutContext.commandScope.panelId = 20u;
|
||||||
|
shortcutContext.commandScope.widgetId = 30u;
|
||||||
|
dispatcher.SetShortcutContext(shortcutContext);
|
||||||
|
|
||||||
|
UIShortcutBinding binding = {};
|
||||||
|
binding.scope = UIShortcutScope::Panel;
|
||||||
|
binding.ownerId = 20u;
|
||||||
|
binding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
binding.chord.modifiers.control = true;
|
||||||
|
binding.commandId = "panel.command";
|
||||||
|
dispatcher.GetShortcutRegistry().RegisterBinding(binding);
|
||||||
|
|
||||||
|
bool handlerCalled = false;
|
||||||
|
const auto summary = dispatcher.Dispatch(
|
||||||
|
MakeCtrlPEvent(),
|
||||||
|
{},
|
||||||
|
[&handlerCalled](const auto&) {
|
||||||
|
handlerCalled = true;
|
||||||
|
return UIInputDispatchDecision{};
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_TRUE(summary.shortcutMatched);
|
||||||
|
EXPECT_TRUE(summary.shortcutHandled);
|
||||||
|
EXPECT_FALSE(summary.shortcutSuppressed);
|
||||||
|
EXPECT_EQ(summary.commandId, "panel.command");
|
||||||
|
EXPECT_EQ(summary.shortcutScope, UIShortcutScope::Panel);
|
||||||
|
EXPECT_EQ(summary.shortcutOwnerId, 20u);
|
||||||
|
EXPECT_FALSE(handlerCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIShortcutScopeTest, InputDispatcherSuppressesShortcutWhileTextInputIsActiveButStillRoutesEvent) {
|
||||||
|
UIInputDispatcher dispatcher{};
|
||||||
|
dispatcher.GetFocusController().SetFocusedPath({ 10u, 20u, 30u });
|
||||||
|
|
||||||
|
UIShortcutContext shortcutContext = {};
|
||||||
|
shortcutContext.commandScope.path = { 10u, 20u, 30u };
|
||||||
|
shortcutContext.commandScope.windowId = 10u;
|
||||||
|
shortcutContext.commandScope.panelId = 20u;
|
||||||
|
shortcutContext.commandScope.widgetId = 30u;
|
||||||
|
shortcutContext.textInputActive = true;
|
||||||
|
dispatcher.SetShortcutContext(shortcutContext);
|
||||||
|
|
||||||
|
UIShortcutBinding binding = {};
|
||||||
|
binding.scope = UIShortcutScope::Panel;
|
||||||
|
binding.ownerId = 20u;
|
||||||
|
binding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
binding.chord.modifiers.control = true;
|
||||||
|
binding.commandId = "panel.command";
|
||||||
|
dispatcher.GetShortcutRegistry().RegisterBinding(binding);
|
||||||
|
|
||||||
|
bool handlerCalled = false;
|
||||||
|
const auto summary = dispatcher.Dispatch(
|
||||||
|
MakeCtrlPEvent(),
|
||||||
|
{},
|
||||||
|
[&handlerCalled](const auto&) {
|
||||||
|
handlerCalled = true;
|
||||||
|
return UIInputDispatchDecision{};
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_TRUE(summary.shortcutMatched);
|
||||||
|
EXPECT_FALSE(summary.shortcutHandled);
|
||||||
|
EXPECT_TRUE(summary.shortcutSuppressed);
|
||||||
|
EXPECT_EQ(summary.commandId, "panel.command");
|
||||||
|
EXPECT_TRUE(handlerCalled);
|
||||||
|
}
|
||||||
112
tests/UI/Core/unit/test_ui_splitter_interaction.cpp
Normal file
112
tests/UI/Core/unit/test_ui_splitter_interaction.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Layout/UISplitterLayout.h>
|
||||||
|
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Layout::UILayoutAxis;
|
||||||
|
using XCEngine::UI::Layout::ArrangeUISplitter;
|
||||||
|
using XCEngine::UI::Layout::UISplitterConstraints;
|
||||||
|
using XCEngine::UI::Layout::UISplitterLayoutResult;
|
||||||
|
using XCEngine::UI::Layout::UISplitterMetrics;
|
||||||
|
using XCEngine::UI::Widgets::BeginUISplitterDrag;
|
||||||
|
using XCEngine::UI::Widgets::EndUISplitterDrag;
|
||||||
|
using XCEngine::UI::Widgets::ExpandUISplitterHandleHitRect;
|
||||||
|
using XCEngine::UI::Widgets::HitTestUISplitterHandle;
|
||||||
|
using XCEngine::UI::Widgets::UISplitterDragState;
|
||||||
|
using XCEngine::UI::Widgets::UpdateUISplitterDrag;
|
||||||
|
|
||||||
|
void ExpectRect(
|
||||||
|
const UIRect& rect,
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
float width,
|
||||||
|
float height) {
|
||||||
|
EXPECT_FLOAT_EQ(rect.x, x);
|
||||||
|
EXPECT_FLOAT_EQ(rect.y, y);
|
||||||
|
EXPECT_FLOAT_EQ(rect.width, width);
|
||||||
|
EXPECT_FLOAT_EQ(rect.height, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UISplitterInteractionTest, HorizontalSplitterHitRectExpandsAcrossDividerWidth) {
|
||||||
|
const UIRect hitRect = ExpandUISplitterHandleHitRect(
|
||||||
|
UIRect(240.0f, 24.0f, 8.0f, 260.0f),
|
||||||
|
UILayoutAxis::Horizontal,
|
||||||
|
4.0f);
|
||||||
|
|
||||||
|
ExpectRect(hitRect, 236.0f, 24.0f, 16.0f, 260.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UISplitterInteractionTest, VerticalSplitterHitRectExpandsAcrossDividerHeight) {
|
||||||
|
const UIRect hitRect = ExpandUISplitterHandleHitRect(
|
||||||
|
UIRect(16.0f, 180.0f, 420.0f, 10.0f),
|
||||||
|
UILayoutAxis::Vertical,
|
||||||
|
5.0f);
|
||||||
|
|
||||||
|
ExpectRect(hitRect, 16.0f, 175.0f, 420.0f, 20.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UISplitterInteractionTest, HitTestUsesExpandedHorizontalHandleRect) {
|
||||||
|
EXPECT_TRUE(HitTestUISplitterHandle(
|
||||||
|
UIRect(240.0f, 24.0f, 8.0f, 260.0f),
|
||||||
|
UILayoutAxis::Horizontal,
|
||||||
|
UIPoint(236.5f, 80.0f),
|
||||||
|
4.0f));
|
||||||
|
EXPECT_FALSE(HitTestUISplitterHandle(
|
||||||
|
UIRect(240.0f, 24.0f, 8.0f, 260.0f),
|
||||||
|
UILayoutAxis::Horizontal,
|
||||||
|
UIPoint(235.0f, 80.0f),
|
||||||
|
4.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UISplitterInteractionTest, HitTestUsesExpandedVerticalHandleRect) {
|
||||||
|
EXPECT_TRUE(HitTestUISplitterHandle(
|
||||||
|
UIRect(16.0f, 180.0f, 420.0f, 10.0f),
|
||||||
|
UILayoutAxis::Vertical,
|
||||||
|
UIPoint(90.0f, 176.0f),
|
||||||
|
5.0f));
|
||||||
|
EXPECT_FALSE(HitTestUISplitterHandle(
|
||||||
|
UIRect(16.0f, 180.0f, 420.0f, 10.0f),
|
||||||
|
UILayoutAxis::Vertical,
|
||||||
|
UIPoint(90.0f, 174.0f),
|
||||||
|
5.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UISplitterInteractionTest, DragUpdateClampsPrimaryExtentAgainstConstraints) {
|
||||||
|
UISplitterConstraints constraints = {};
|
||||||
|
constraints.primaryMin = 120.0f;
|
||||||
|
constraints.secondaryMin = 180.0f;
|
||||||
|
const UISplitterMetrics metrics = { 10.0f, 18.0f };
|
||||||
|
const UIRect bounds(0.0f, 0.0f, 500.0f, 180.0f);
|
||||||
|
const UISplitterLayoutResult initialLayout = ArrangeUISplitter(
|
||||||
|
bounds,
|
||||||
|
UILayoutAxis::Horizontal,
|
||||||
|
0.5f,
|
||||||
|
constraints,
|
||||||
|
metrics);
|
||||||
|
|
||||||
|
UISplitterDragState dragState = {};
|
||||||
|
ASSERT_TRUE(BeginUISplitterDrag(
|
||||||
|
42u,
|
||||||
|
UILayoutAxis::Horizontal,
|
||||||
|
bounds,
|
||||||
|
initialLayout,
|
||||||
|
constraints,
|
||||||
|
metrics,
|
||||||
|
UIPoint(initialLayout.handleRect.x + 5.0f, 90.0f),
|
||||||
|
dragState));
|
||||||
|
|
||||||
|
UISplitterLayoutResult updatedLayout = {};
|
||||||
|
ASSERT_TRUE(UpdateUISplitterDrag(dragState, UIPoint(80.0f, 90.0f), updatedLayout));
|
||||||
|
EXPECT_FLOAT_EQ(updatedLayout.primaryExtent, 120.0f);
|
||||||
|
EXPECT_FLOAT_EQ(updatedLayout.secondaryExtent, 370.0f);
|
||||||
|
|
||||||
|
EndUISplitterDrag(dragState);
|
||||||
|
EXPECT_FALSE(dragState.active);
|
||||||
|
EXPECT_EQ(dragState.ownerId, 0u);
|
||||||
|
}
|
||||||
76
tests/UI/Core/unit/test_ui_splitter_layout.cpp
Normal file
76
tests/UI/Core/unit/test_ui_splitter_layout.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Layout/UISplitterLayout.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Layout::ArrangeUISplitter;
|
||||||
|
using XCEngine::UI::Layout::UILayoutAxis;
|
||||||
|
using XCEngine::UI::Layout::UISplitterConstraints;
|
||||||
|
using XCEngine::UI::Layout::UISplitterMetrics;
|
||||||
|
|
||||||
|
void ExpectRect(
|
||||||
|
const UIRect& rect,
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
float width,
|
||||||
|
float height) {
|
||||||
|
EXPECT_FLOAT_EQ(rect.x, x);
|
||||||
|
EXPECT_FLOAT_EQ(rect.y, y);
|
||||||
|
EXPECT_FLOAT_EQ(rect.width, width);
|
||||||
|
EXPECT_FLOAT_EQ(rect.height, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UISplitterLayoutTest, ArrangeHorizontalSplitterClampsPrimaryExtentToMinimum) {
|
||||||
|
UISplitterConstraints constraints = {};
|
||||||
|
constraints.primaryMin = 200.0f;
|
||||||
|
constraints.secondaryMin = 120.0f;
|
||||||
|
|
||||||
|
const auto result = ArrangeUISplitter(
|
||||||
|
UIRect(0.0f, 0.0f, 600.0f, 300.0f),
|
||||||
|
UILayoutAxis::Horizontal,
|
||||||
|
0.1f,
|
||||||
|
constraints,
|
||||||
|
UISplitterMetrics{ 10.0f, 18.0f });
|
||||||
|
|
||||||
|
ExpectRect(result.primaryRect, 0.0f, 0.0f, 200.0f, 300.0f);
|
||||||
|
ExpectRect(result.handleRect, 200.0f, 0.0f, 10.0f, 300.0f);
|
||||||
|
ExpectRect(result.secondaryRect, 210.0f, 0.0f, 390.0f, 300.0f);
|
||||||
|
EXPECT_NEAR(result.splitRatio, 200.0f / 590.0f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UISplitterLayoutTest, ArrangeVerticalSplitterClampsSecondaryMinimumAgainstRequestedRatio) {
|
||||||
|
UISplitterConstraints constraints = {};
|
||||||
|
constraints.primaryMin = 100.0f;
|
||||||
|
constraints.secondaryMin = 120.0f;
|
||||||
|
|
||||||
|
const auto result = ArrangeUISplitter(
|
||||||
|
UIRect(0.0f, 0.0f, 500.0f, 400.0f),
|
||||||
|
UILayoutAxis::Vertical,
|
||||||
|
0.9f,
|
||||||
|
constraints,
|
||||||
|
UISplitterMetrics{ 8.0f, 16.0f });
|
||||||
|
|
||||||
|
ExpectRect(result.primaryRect, 0.0f, 0.0f, 500.0f, 272.0f);
|
||||||
|
ExpectRect(result.handleRect, 0.0f, 272.0f, 500.0f, 8.0f);
|
||||||
|
ExpectRect(result.secondaryRect, 0.0f, 280.0f, 500.0f, 120.0f);
|
||||||
|
EXPECT_NEAR(result.splitRatio, 272.0f / 392.0f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UISplitterLayoutTest, HorizontalArrangementSplitsAvailableExtentAroundHandle) {
|
||||||
|
const auto result = ArrangeUISplitter(
|
||||||
|
UIRect(10.0f, 20.0f, 400.0f, 120.0f),
|
||||||
|
UILayoutAxis::Horizontal,
|
||||||
|
0.5f,
|
||||||
|
{},
|
||||||
|
UISplitterMetrics{ 8.0f, 14.0f });
|
||||||
|
|
||||||
|
EXPECT_FLOAT_EQ(result.primaryExtent, 196.0f);
|
||||||
|
EXPECT_FLOAT_EQ(result.secondaryExtent, 196.0f);
|
||||||
|
ExpectRect(result.primaryRect, 10.0f, 20.0f, 196.0f, 120.0f);
|
||||||
|
ExpectRect(result.handleRect, 206.0f, 20.0f, 8.0f, 120.0f);
|
||||||
|
ExpectRect(result.secondaryRect, 214.0f, 20.0f, 196.0f, 120.0f);
|
||||||
|
}
|
||||||
18
tests/UI/Editor/CMakeLists.txt
Normal file
18
tests/UI/Editor/CMakeLists.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
|
||||||
|
project(XCEngine_EditorUITests)
|
||||||
|
|
||||||
|
add_subdirectory(integration/shared)
|
||||||
|
add_subdirectory(unit)
|
||||||
|
add_subdirectory(integration)
|
||||||
|
|
||||||
|
add_custom_target(editor_ui_unit_tests
|
||||||
|
DEPENDS
|
||||||
|
editor_ui_tests
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(editor_ui_all_tests
|
||||||
|
DEPENDS
|
||||||
|
editor_ui_unit_tests
|
||||||
|
editor_ui_integration_tests
|
||||||
|
)
|
||||||
8
tests/UI/Editor/integration/CMakeLists.txt
Normal file
8
tests/UI/Editor/integration/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
add_subdirectory(input)
|
||||||
|
add_subdirectory(layout)
|
||||||
|
|
||||||
|
add_custom_target(editor_ui_integration_tests
|
||||||
|
DEPENDS
|
||||||
|
editor_ui_input_integration_tests
|
||||||
|
editor_ui_layout_integration_tests
|
||||||
|
)
|
||||||
22
tests/UI/Editor/integration/README.md
Normal file
22
tests/UI/Editor/integration/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Editor UI Integration Validation
|
||||||
|
|
||||||
|
This directory contains the manual XCUI validation system for editor-facing scenarios.
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
|
||||||
|
- `shared/`: shared host, native renderer, screenshot helper, scenario registry
|
||||||
|
- `input/`: input-related validation category
|
||||||
|
- `layout/`: layout and shell-foundation validation category
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- One scenario directory maps to one executable.
|
||||||
|
- Do not accumulate unrelated checks into one monolithic app.
|
||||||
|
- Shared infrastructure belongs in `shared/`, not duplicated per scenario.
|
||||||
|
- Screenshots are stored per scenario inside that scenario's `captures/` folder.
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --build build --config Debug --target editor_ui_integration_tests
|
||||||
|
```
|
||||||
10
tests/UI/Editor/integration/input/CMakeLists.txt
Normal file
10
tests/UI/Editor/integration/input/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
add_subdirectory(keyboard_focus)
|
||||||
|
add_subdirectory(pointer_states)
|
||||||
|
add_subdirectory(shortcut_scope)
|
||||||
|
|
||||||
|
add_custom_target(editor_ui_input_integration_tests
|
||||||
|
DEPENDS
|
||||||
|
editor_ui_input_keyboard_focus_validation
|
||||||
|
editor_ui_input_pointer_states_validation
|
||||||
|
editor_ui_input_shortcut_scope_validation
|
||||||
|
)
|
||||||
9
tests/UI/Editor/integration/input/README.md
Normal file
9
tests/UI/Editor/integration/input/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Editor Input Integration
|
||||||
|
|
||||||
|
这个分类只放 editor 输入相关的手工验证场景。
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- 一个场景目录对应一个独立 exe
|
||||||
|
- 共享宿主层只放在 `integration/shared/`
|
||||||
|
- 不允许把多个无关检查点塞进同一个 exe
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
set(EDITOR_UI_INPUT_KEYBOARD_FOCUS_RESOURCES
|
||||||
|
View.xcui
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(editor_ui_input_keyboard_focus_validation WIN32
|
||||||
|
main.cpp
|
||||||
|
${EDITOR_UI_INPUT_KEYBOARD_FOCUS_RESOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_input_keyboard_focus_validation PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_input_keyboard_focus_validation PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_input_keyboard_focus_validation PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_input_keyboard_focus_validation PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_input_keyboard_focus_validation PRIVATE
|
||||||
|
editor_ui_integration_host
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(editor_ui_input_keyboard_focus_validation PROPERTIES
|
||||||
|
OUTPUT_NAME "XCUIEditorInputKeyboardFocusValidation"
|
||||||
|
)
|
||||||
|
|
||||||
|
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES View.xcui)
|
||||||
18
tests/UI/Editor/integration/input/keyboard_focus/README.md
Normal file
18
tests/UI/Editor/integration/input/keyboard_focus/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Keyboard Focus Validation
|
||||||
|
|
||||||
|
可执行 target:
|
||||||
|
|
||||||
|
- `editor_ui_input_keyboard_focus_validation`
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build\tests\UI\Editor\integration\input\keyboard_focus\Debug\XCUIEditorInputKeyboardFocusValidation.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
检查点:
|
||||||
|
|
||||||
|
1. 按 `Tab`,焦点依次切换三个按钮
|
||||||
|
2. 按 `Shift+Tab`,焦点反向切换
|
||||||
|
3. 按 `Enter` 或 `Space`,当前 `focus` 按钮进入 `active`
|
||||||
|
4. 松开按键后,`active` 清空
|
||||||
30
tests/UI/Editor/integration/input/keyboard_focus/View.xcui
Normal file
30
tests/UI/Editor/integration/input/keyboard_focus/View.xcui
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<View
|
||||||
|
name="EditorInputKeyboardFocus"
|
||||||
|
theme="../../shared/themes/editor_validation.xctheme">
|
||||||
|
<Column padding="24" gap="16">
|
||||||
|
<Card
|
||||||
|
title="Editor Validation | Keyboard Focus"
|
||||||
|
subtitle="当前批次:Tab 焦点遍历 | Enter / Space 激活"
|
||||||
|
tone="accent"
|
||||||
|
height="90">
|
||||||
|
<Column gap="8">
|
||||||
|
<Text text="这是 editor 侧验证场景,不承载 runtime 游戏 UI。" />
|
||||||
|
<Text text="这一轮只检查键盘焦点和激活,不混入复杂 editor 面板。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Keyboard Focus" subtitle="tab focus active" height="214">
|
||||||
|
<Column gap="12">
|
||||||
|
<Text text="只检查下面三个可聚焦按钮和右下角状态叠层。" />
|
||||||
|
<Row gap="12">
|
||||||
|
<Button id="focus-first" text="First Focus" />
|
||||||
|
<Button id="focus-second" text="Second Focus" />
|
||||||
|
<Button id="focus-third" text="Third Focus" />
|
||||||
|
</Row>
|
||||||
|
<Text text="1. 按 Tab:focus 应依次切到 First / Second / Third。" />
|
||||||
|
<Text text="2. 按 Shift+Tab:focus 应反向切换。" />
|
||||||
|
<Text text="3. focus 停在任一按钮后,按 Enter 或 Space:active 应出现;松开后 active 清空。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
</Column>
|
||||||
|
</View>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include "Application.h"
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||||
|
return XCEngine::Tests::EditorUI::RunEditorUIValidationApp(
|
||||||
|
hInstance,
|
||||||
|
nCmdShow,
|
||||||
|
"editor.input.keyboard_focus");
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
set(EDITOR_UI_INPUT_POINTER_STATES_RESOURCES
|
||||||
|
View.xcui
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(editor_ui_input_pointer_states_validation WIN32
|
||||||
|
main.cpp
|
||||||
|
${EDITOR_UI_INPUT_POINTER_STATES_RESOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_input_pointer_states_validation PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_input_pointer_states_validation PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_input_pointer_states_validation PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_input_pointer_states_validation PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_input_pointer_states_validation PRIVATE
|
||||||
|
editor_ui_integration_host
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(editor_ui_input_pointer_states_validation PROPERTIES
|
||||||
|
OUTPUT_NAME "XCUIEditorInputPointerStatesValidation"
|
||||||
|
)
|
||||||
|
|
||||||
|
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES View.xcui)
|
||||||
17
tests/UI/Editor/integration/input/pointer_states/README.md
Normal file
17
tests/UI/Editor/integration/input/pointer_states/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Pointer States Validation
|
||||||
|
|
||||||
|
可执行 target:
|
||||||
|
|
||||||
|
- `editor_ui_input_pointer_states_validation`
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build\tests\UI\Editor\integration\input\pointer_states\Debug\XCUIEditorInputPointerStatesValidation.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
检查点:
|
||||||
|
|
||||||
|
1. hover 左侧按钮,只应变化 `hover`
|
||||||
|
2. 按住中间按钮,应看到 `focus`、`active`、`capture`
|
||||||
|
3. 拖到右侧再松开,应看到 `capture` 清空,route 转到新的目标
|
||||||
30
tests/UI/Editor/integration/input/pointer_states/View.xcui
Normal file
30
tests/UI/Editor/integration/input/pointer_states/View.xcui
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<View
|
||||||
|
name="EditorInputPointerStates"
|
||||||
|
theme="../../shared/themes/editor_validation.xctheme">
|
||||||
|
<Column padding="24" gap="16">
|
||||||
|
<Card
|
||||||
|
title="Editor Validation | Pointer States"
|
||||||
|
subtitle="当前批次:鼠标 hover / focus / active / capture"
|
||||||
|
tone="accent"
|
||||||
|
height="90">
|
||||||
|
<Column gap="8">
|
||||||
|
<Text text="这是 editor 侧验证场景,不承载 runtime 游戏 UI。" />
|
||||||
|
<Text text="这一轮只检查鼠标输入状态,不混入别的控件实验。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Pointer Input" subtitle="hover focus active capture" height="196">
|
||||||
|
<Column gap="12">
|
||||||
|
<Text text="这一轮只需要检查下面这三个按钮。" />
|
||||||
|
<Row gap="12">
|
||||||
|
<Button id="input-hover" text="Hover / Focus" />
|
||||||
|
<Button id="input-capture" text="Pointer Capture" capturePointer="true" />
|
||||||
|
<Button id="input-route" text="Route Target" />
|
||||||
|
</Row>
|
||||||
|
<Text text="1. 鼠标移到左侧按钮:hover 应变化,focus 保持空。" />
|
||||||
|
<Text text="2. 按住中间按钮:focus、active、capture 都应留在中间。" />
|
||||||
|
<Text text="3. 拖到右侧再松开:hover 移到右侧,capture 清空,focus 仍留中间。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
</Column>
|
||||||
|
</View>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include "Application.h"
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||||
|
return XCEngine::Tests::EditorUI::RunEditorUIValidationApp(
|
||||||
|
hInstance,
|
||||||
|
nCmdShow,
|
||||||
|
"editor.input.pointer_states");
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
set(EDITOR_UI_INPUT_SHORTCUT_SCOPE_RESOURCES
|
||||||
|
View.xcui
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(editor_ui_input_shortcut_scope_validation WIN32
|
||||||
|
main.cpp
|
||||||
|
${EDITOR_UI_INPUT_SHORTCUT_SCOPE_RESOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_input_shortcut_scope_validation PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_input_shortcut_scope_validation PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_input_shortcut_scope_validation PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_input_shortcut_scope_validation PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_input_shortcut_scope_validation PRIVATE
|
||||||
|
editor_ui_integration_host
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(editor_ui_input_shortcut_scope_validation PROPERTIES
|
||||||
|
OUTPUT_NAME "XCUIEditorInputShortcutScopeValidation"
|
||||||
|
)
|
||||||
|
|
||||||
|
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES View.xcui)
|
||||||
69
tests/UI/Editor/integration/input/shortcut_scope/View.xcui
Normal file
69
tests/UI/Editor/integration/input/shortcut_scope/View.xcui
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<View
|
||||||
|
name="EditorInputShortcutScope"
|
||||||
|
theme="../../shared/themes/editor_validation.xctheme"
|
||||||
|
shortcut="Ctrl+P"
|
||||||
|
shortcutCommand="global.command"
|
||||||
|
shortcutScope="global">
|
||||||
|
<Column padding="20" gap="12">
|
||||||
|
<Card
|
||||||
|
title="Editor Validation | Shortcut Scope"
|
||||||
|
subtitle="验证功能:Editor shortcut scope 路由与 text input suppression"
|
||||||
|
tone="accent"
|
||||||
|
height="100">
|
||||||
|
<Column gap="6">
|
||||||
|
<Text text="功能 1:验证 Ctrl+P 在 Widget / Panel / Window / Global 间按优先级命中 shortcut。" />
|
||||||
|
<Text text="功能 2:验证 Text Input Proxy 会抑制 Ctrl+P 和 Tab 焦点遍历。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Button id="global-focus" text="Global Focus" />
|
||||||
|
|
||||||
|
<Card
|
||||||
|
id="window-shell"
|
||||||
|
title="Window Scope"
|
||||||
|
subtitle="Ctrl+P -> window.command"
|
||||||
|
shortcutScopeRoot="window"
|
||||||
|
shortcut="Ctrl+P"
|
||||||
|
shortcutCommand="window.command"
|
||||||
|
shortcutScope="window">
|
||||||
|
<Column gap="10">
|
||||||
|
<Text text="先检查优先级:widget > panel > window > global。" />
|
||||||
|
<Button id="window-focus" text="Window Focus" />
|
||||||
|
|
||||||
|
<Card
|
||||||
|
id="panel-shell"
|
||||||
|
title="Panel Scope"
|
||||||
|
subtitle="Ctrl+P -> panel.command"
|
||||||
|
shortcutScopeRoot="panel"
|
||||||
|
shortcut="Ctrl+P"
|
||||||
|
shortcutCommand="panel.command"
|
||||||
|
shortcutScope="panel">
|
||||||
|
<Column gap="10">
|
||||||
|
<Button id="panel-focus" text="Panel Focus" />
|
||||||
|
|
||||||
|
<Card
|
||||||
|
id="widget-shell"
|
||||||
|
title="Widget Scope"
|
||||||
|
subtitle="Ctrl+P -> widget.command"
|
||||||
|
tone="accent-alt"
|
||||||
|
shortcutScopeRoot="widget"
|
||||||
|
shortcut="Ctrl+P"
|
||||||
|
shortcutCommand="widget.command"
|
||||||
|
shortcutScope="widget">
|
||||||
|
<Column gap="10">
|
||||||
|
<Button id="widget-focus" text="Widget Focus" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Button id="text-input" text="Text Input Proxy" textInput="true" />
|
||||||
|
<Text text="操作指引:" />
|
||||||
|
<Text text="1. 依次点 Widget / Panel / Window / Global Focus,再按 Ctrl+P。" />
|
||||||
|
<Text text="2. 右下角 Recent shortcut 应分别显示 widget / panel / window / global,且状态为 handled。" />
|
||||||
|
<Text text="3. 点 Text Input Proxy 再按 Ctrl+P,Recent shortcut 状态应变为 suppressed。" />
|
||||||
|
<Text text="4. 保持 Text Input Proxy focus 再按 Tab,Result 应显示 focus traversal suppressed,focus 不应跳走。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
</Column>
|
||||||
|
</View>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include "Application.h"
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||||
|
return XCEngine::Tests::EditorUI::RunEditorUIValidationApp(
|
||||||
|
hInstance,
|
||||||
|
nCmdShow,
|
||||||
|
"editor.input.shortcut_scope");
|
||||||
|
}
|
||||||
6
tests/UI/Editor/integration/layout/CMakeLists.txt
Normal file
6
tests/UI/Editor/integration/layout/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
add_subdirectory(splitter_resize)
|
||||||
|
|
||||||
|
add_custom_target(editor_ui_layout_integration_tests
|
||||||
|
DEPENDS
|
||||||
|
editor_ui_layout_splitter_resize_validation
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
set(EDITOR_UI_LAYOUT_SPLITTER_RESIZE_RESOURCES
|
||||||
|
View.xcui
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(editor_ui_layout_splitter_resize_validation WIN32
|
||||||
|
main.cpp
|
||||||
|
${EDITOR_UI_LAYOUT_SPLITTER_RESIZE_RESOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_layout_splitter_resize_validation PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_layout_splitter_resize_validation PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_layout_splitter_resize_validation PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_layout_splitter_resize_validation PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_layout_splitter_resize_validation PRIVATE
|
||||||
|
editor_ui_integration_host
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(editor_ui_layout_splitter_resize_validation PROPERTIES
|
||||||
|
OUTPUT_NAME "XCUIEditorLayoutSplitterResizeValidation"
|
||||||
|
)
|
||||||
|
|
||||||
|
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES View.xcui)
|
||||||
39
tests/UI/Editor/integration/layout/splitter_resize/View.xcui
Normal file
39
tests/UI/Editor/integration/layout/splitter_resize/View.xcui
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<View
|
||||||
|
name="EditorSplitterResizeValidation"
|
||||||
|
theme="../../shared/themes/editor_validation.xctheme">
|
||||||
|
<Column width="fill" height="fill" padding="20" gap="12">
|
||||||
|
<Card
|
||||||
|
title="功能:Splitter / pane resize"
|
||||||
|
subtitle="这一轮只检查分割条拖拽和最小尺寸 clamp"
|
||||||
|
tone="accent"
|
||||||
|
height="128">
|
||||||
|
<Column gap="6">
|
||||||
|
<Text text="1. 鼠标移到中间 divider:右下角 Hover 应落到 workspace-splitter。" />
|
||||||
|
<Text text="2. 按住左键拖拽:左右 pane 宽度应实时变化,Result 应出现 Splitter drag started / Splitter resized。" />
|
||||||
|
<Text text="3. 向左右极限拖拽:布局应被 primaryMin / secondaryMin clamp 住,不应穿透。" />
|
||||||
|
<Text text="4. 松开左键:Result 应显示 Splitter drag finished。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Splitter
|
||||||
|
id="workspace-splitter"
|
||||||
|
axis="horizontal"
|
||||||
|
splitRatio="0.38"
|
||||||
|
splitterSize="10"
|
||||||
|
splitterHitSize="18"
|
||||||
|
primaryMin="180"
|
||||||
|
secondaryMin="220"
|
||||||
|
height="fill">
|
||||||
|
<Card id="left-pane" title="Left Empty Pane" subtitle="min 180" height="fill">
|
||||||
|
<Column gap="8">
|
||||||
|
<Text text="这里只保留空 pane,用来观察 resize。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
<Card id="right-pane" title="Right Empty Pane" subtitle="min 220" height="fill">
|
||||||
|
<Column gap="8">
|
||||||
|
<Text text="拖拽过程中不应出现翻转、穿透或抖动。" />
|
||||||
|
</Column>
|
||||||
|
</Card>
|
||||||
|
</Splitter>
|
||||||
|
</Column>
|
||||||
|
</View>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include "Application.h"
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||||
|
return XCEngine::Tests::EditorUI::RunEditorUIValidationApp(
|
||||||
|
hInstance,
|
||||||
|
nCmdShow,
|
||||||
|
"editor.layout.splitter_resize");
|
||||||
|
}
|
||||||
57
tests/UI/Editor/integration/shared/CMakeLists.txt
Normal file
57
tests/UI/Editor/integration/shared/CMakeLists.txt
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH)
|
||||||
|
|
||||||
|
add_library(editor_ui_validation_registry STATIC
|
||||||
|
src/EditorValidationScenario.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_validation_registry
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_validation_registry
|
||||||
|
PUBLIC
|
||||||
|
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_validation_registry PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_validation_registry PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_validation_registry
|
||||||
|
PUBLIC
|
||||||
|
XCEngine
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(editor_ui_integration_host STATIC
|
||||||
|
src/Application.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_integration_host
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/include
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_integration_host
|
||||||
|
PUBLIC
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_integration_host PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_integration_host PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_integration_host
|
||||||
|
PUBLIC
|
||||||
|
editor_ui_validation_registry
|
||||||
|
XCNewEditorHost
|
||||||
|
)
|
||||||
782
tests/UI/Editor/integration/shared/src/Application.cpp
Normal file
782
tests/UI/Editor/integration/shared/src/Application.cpp
Normal file
@@ -0,0 +1,782 @@
|
|||||||
|
#include "Application.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||||
|
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace XCEngine::Tests::EditorUI {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::XCEngine::UI::UIColor;
|
||||||
|
using ::XCEngine::UI::UIDrawData;
|
||||||
|
using ::XCEngine::UI::UIDrawList;
|
||||||
|
using ::XCEngine::UI::UIInputEvent;
|
||||||
|
using ::XCEngine::UI::UIInputEventType;
|
||||||
|
using ::XCEngine::UI::UIPoint;
|
||||||
|
using ::XCEngine::UI::UIPointerButton;
|
||||||
|
using ::XCEngine::UI::UIRect;
|
||||||
|
using ::XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||||
|
using ::XCEngine::Input::KeyCode;
|
||||||
|
|
||||||
|
constexpr const wchar_t* kWindowClassName = L"XCUIEditorValidationHost";
|
||||||
|
constexpr const wchar_t* kWindowTitle = L"XCUI Editor Validation";
|
||||||
|
constexpr auto kReloadPollInterval = std::chrono::milliseconds(150);
|
||||||
|
|
||||||
|
constexpr UIColor kOverlayBgColor(0.10f, 0.10f, 0.10f, 0.95f);
|
||||||
|
constexpr UIColor kOverlayBorderColor(0.25f, 0.25f, 0.25f, 1.0f);
|
||||||
|
constexpr UIColor kOverlayTextPrimary(0.93f, 0.93f, 0.93f, 1.0f);
|
||||||
|
constexpr UIColor kOverlayTextMuted(0.70f, 0.70f, 0.70f, 1.0f);
|
||||||
|
constexpr UIColor kOverlaySuccess(0.82f, 0.82f, 0.82f, 1.0f);
|
||||||
|
constexpr UIColor kOverlayFallback(0.56f, 0.56f, 0.56f, 1.0f);
|
||||||
|
|
||||||
|
Application* GetApplicationFromWindow(HWND hwnd) {
|
||||||
|
return reinterpret_cast<Application*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path GetRepoRootPath() {
|
||||||
|
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||||
|
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||||
|
root = root.substr(1u, root.size() - 2u);
|
||||||
|
}
|
||||||
|
return std::filesystem::path(root).lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TruncateText(const std::string& text, std::size_t maxLength) {
|
||||||
|
if (text.size() <= maxLength) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxLength <= 3u) {
|
||||||
|
return text.substr(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.substr(0, maxLength - 3u) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ExtractStateKeyTail(const std::string& stateKey) {
|
||||||
|
if (stateKey.empty()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t separator = stateKey.find_last_of('/');
|
||||||
|
if (separator == std::string::npos || separator + 1u >= stateKey.size()) {
|
||||||
|
return stateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateKey.substr(separator + 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatFloat(float value) {
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream.setf(std::ios::fixed, std::ios::floatfield);
|
||||||
|
stream.precision(1);
|
||||||
|
stream << value;
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatPoint(const UIPoint& point) {
|
||||||
|
return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatRect(const UIRect& rect) {
|
||||||
|
return "(" + FormatFloat(rect.x) +
|
||||||
|
", " + FormatFloat(rect.y) +
|
||||||
|
", " + FormatFloat(rect.width) +
|
||||||
|
", " + FormatFloat(rect.height) +
|
||||||
|
")";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) {
|
||||||
|
switch (wParam) {
|
||||||
|
case 'A': return static_cast<std::int32_t>(KeyCode::A);
|
||||||
|
case 'B': return static_cast<std::int32_t>(KeyCode::B);
|
||||||
|
case 'C': return static_cast<std::int32_t>(KeyCode::C);
|
||||||
|
case 'D': return static_cast<std::int32_t>(KeyCode::D);
|
||||||
|
case 'E': return static_cast<std::int32_t>(KeyCode::E);
|
||||||
|
case 'F': return static_cast<std::int32_t>(KeyCode::F);
|
||||||
|
case 'G': return static_cast<std::int32_t>(KeyCode::G);
|
||||||
|
case 'H': return static_cast<std::int32_t>(KeyCode::H);
|
||||||
|
case 'I': return static_cast<std::int32_t>(KeyCode::I);
|
||||||
|
case 'J': return static_cast<std::int32_t>(KeyCode::J);
|
||||||
|
case 'K': return static_cast<std::int32_t>(KeyCode::K);
|
||||||
|
case 'L': return static_cast<std::int32_t>(KeyCode::L);
|
||||||
|
case 'M': return static_cast<std::int32_t>(KeyCode::M);
|
||||||
|
case 'N': return static_cast<std::int32_t>(KeyCode::N);
|
||||||
|
case 'O': return static_cast<std::int32_t>(KeyCode::O);
|
||||||
|
case 'P': return static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
case 'Q': return static_cast<std::int32_t>(KeyCode::Q);
|
||||||
|
case 'R': return static_cast<std::int32_t>(KeyCode::R);
|
||||||
|
case 'S': return static_cast<std::int32_t>(KeyCode::S);
|
||||||
|
case 'T': return static_cast<std::int32_t>(KeyCode::T);
|
||||||
|
case 'U': return static_cast<std::int32_t>(KeyCode::U);
|
||||||
|
case 'V': return static_cast<std::int32_t>(KeyCode::V);
|
||||||
|
case 'W': return static_cast<std::int32_t>(KeyCode::W);
|
||||||
|
case 'X': return static_cast<std::int32_t>(KeyCode::X);
|
||||||
|
case 'Y': return static_cast<std::int32_t>(KeyCode::Y);
|
||||||
|
case 'Z': return static_cast<std::int32_t>(KeyCode::Z);
|
||||||
|
case '0': return static_cast<std::int32_t>(KeyCode::Zero);
|
||||||
|
case '1': return static_cast<std::int32_t>(KeyCode::One);
|
||||||
|
case '2': return static_cast<std::int32_t>(KeyCode::Two);
|
||||||
|
case '3': return static_cast<std::int32_t>(KeyCode::Three);
|
||||||
|
case '4': return static_cast<std::int32_t>(KeyCode::Four);
|
||||||
|
case '5': return static_cast<std::int32_t>(KeyCode::Five);
|
||||||
|
case '6': return static_cast<std::int32_t>(KeyCode::Six);
|
||||||
|
case '7': return static_cast<std::int32_t>(KeyCode::Seven);
|
||||||
|
case '8': return static_cast<std::int32_t>(KeyCode::Eight);
|
||||||
|
case '9': return static_cast<std::int32_t>(KeyCode::Nine);
|
||||||
|
case VK_SPACE: return static_cast<std::int32_t>(KeyCode::Space);
|
||||||
|
case VK_TAB: return static_cast<std::int32_t>(KeyCode::Tab);
|
||||||
|
case VK_RETURN: return static_cast<std::int32_t>(KeyCode::Enter);
|
||||||
|
case VK_ESCAPE: return static_cast<std::int32_t>(KeyCode::Escape);
|
||||||
|
case VK_SHIFT: return static_cast<std::int32_t>(KeyCode::LeftShift);
|
||||||
|
case VK_CONTROL: return static_cast<std::int32_t>(KeyCode::LeftCtrl);
|
||||||
|
case VK_MENU: return static_cast<std::int32_t>(KeyCode::LeftAlt);
|
||||||
|
case VK_UP: return static_cast<std::int32_t>(KeyCode::Up);
|
||||||
|
case VK_DOWN: return static_cast<std::int32_t>(KeyCode::Down);
|
||||||
|
case VK_LEFT: return static_cast<std::int32_t>(KeyCode::Left);
|
||||||
|
case VK_RIGHT: return static_cast<std::int32_t>(KeyCode::Right);
|
||||||
|
case VK_HOME: return static_cast<std::int32_t>(KeyCode::Home);
|
||||||
|
case VK_END: return static_cast<std::int32_t>(KeyCode::End);
|
||||||
|
case VK_PRIOR: return static_cast<std::int32_t>(KeyCode::PageUp);
|
||||||
|
case VK_NEXT: return static_cast<std::int32_t>(KeyCode::PageDown);
|
||||||
|
case VK_DELETE: return static_cast<std::int32_t>(KeyCode::Delete);
|
||||||
|
case VK_BACK: return static_cast<std::int32_t>(KeyCode::Backspace);
|
||||||
|
case VK_F1: return static_cast<std::int32_t>(KeyCode::F1);
|
||||||
|
case VK_F2: return static_cast<std::int32_t>(KeyCode::F2);
|
||||||
|
case VK_F3: return static_cast<std::int32_t>(KeyCode::F3);
|
||||||
|
case VK_F4: return static_cast<std::int32_t>(KeyCode::F4);
|
||||||
|
case VK_F5: return static_cast<std::int32_t>(KeyCode::F5);
|
||||||
|
case VK_F6: return static_cast<std::int32_t>(KeyCode::F6);
|
||||||
|
case VK_F7: return static_cast<std::int32_t>(KeyCode::F7);
|
||||||
|
case VK_F8: return static_cast<std::int32_t>(KeyCode::F8);
|
||||||
|
case VK_F9: return static_cast<std::int32_t>(KeyCode::F9);
|
||||||
|
case VK_F10: return static_cast<std::int32_t>(KeyCode::F10);
|
||||||
|
case VK_F11: return static_cast<std::int32_t>(KeyCode::F11);
|
||||||
|
case VK_F12: return static_cast<std::int32_t>(KeyCode::F12);
|
||||||
|
default: return static_cast<std::int32_t>(KeyCode::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsRepeatKeyMessage(LPARAM lParam) {
|
||||||
|
return (static_cast<unsigned long>(lParam) & (1ul << 30)) != 0ul;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Application::Application(std::string requestedScenarioId)
|
||||||
|
: m_screenPlayer(m_documentHost)
|
||||||
|
, m_requestedScenarioId(std::move(requestedScenarioId)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
||||||
|
if (!Initialize(hInstance, nCmdShow)) {
|
||||||
|
Shutdown();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
MSG message = {};
|
||||||
|
while (message.message != WM_QUIT) {
|
||||||
|
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||||
|
TranslateMessage(&message);
|
||||||
|
DispatchMessageW(&message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderFrame();
|
||||||
|
Sleep(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Shutdown();
|
||||||
|
return static_cast<int>(message.wParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||||
|
m_hInstance = hInstance;
|
||||||
|
|
||||||
|
WNDCLASSEXW windowClass = {};
|
||||||
|
windowClass.cbSize = sizeof(windowClass);
|
||||||
|
windowClass.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
windowClass.lpfnWndProc = &Application::WndProc;
|
||||||
|
windowClass.hInstance = hInstance;
|
||||||
|
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||||
|
windowClass.lpszClassName = kWindowClassName;
|
||||||
|
|
||||||
|
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||||
|
if (m_windowClassAtom == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hwnd = CreateWindowExW(
|
||||||
|
0,
|
||||||
|
kWindowClassName,
|
||||||
|
kWindowTitle,
|
||||||
|
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
1440,
|
||||||
|
900,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
hInstance,
|
||||||
|
this);
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowWindow(m_hwnd, nCmdShow);
|
||||||
|
UpdateWindow(m_hwnd);
|
||||||
|
|
||||||
|
if (!m_renderer.Initialize(m_hwnd)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_startTime = std::chrono::steady_clock::now();
|
||||||
|
m_lastFrameTime = m_startTime;
|
||||||
|
const EditorValidationScenario* initialScenario = m_requestedScenarioId.empty()
|
||||||
|
? &GetDefaultEditorValidationScenario()
|
||||||
|
: FindEditorValidationScenario(m_requestedScenarioId);
|
||||||
|
if (initialScenario == nullptr) {
|
||||||
|
initialScenario = &GetDefaultEditorValidationScenario();
|
||||||
|
}
|
||||||
|
m_autoScreenshot.Initialize(initialScenario->captureRootPath);
|
||||||
|
LoadStructuredScreen("startup");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::Shutdown() {
|
||||||
|
m_autoScreenshot.Shutdown();
|
||||||
|
m_screenPlayer.Unload();
|
||||||
|
m_trackedFiles.clear();
|
||||||
|
m_screenAsset = {};
|
||||||
|
m_useStructuredScreen = false;
|
||||||
|
m_runtimeStatus.clear();
|
||||||
|
m_runtimeError.clear();
|
||||||
|
m_frameIndex = 0;
|
||||||
|
|
||||||
|
m_renderer.Shutdown();
|
||||||
|
|
||||||
|
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||||
|
DestroyWindow(m_hwnd);
|
||||||
|
}
|
||||||
|
m_hwnd = nullptr;
|
||||||
|
|
||||||
|
if (m_windowClassAtom != 0 && m_hInstance != nullptr) {
|
||||||
|
UnregisterClassW(kWindowClassName, m_hInstance);
|
||||||
|
m_windowClassAtom = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::RenderFrame() {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT clientRect = {};
|
||||||
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
|
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
|
||||||
|
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
double deltaTimeSeconds = std::chrono::duration<double>(now - m_lastFrameTime).count();
|
||||||
|
if (deltaTimeSeconds <= 0.0) {
|
||||||
|
deltaTimeSeconds = 1.0 / 60.0;
|
||||||
|
}
|
||||||
|
m_lastFrameTime = now;
|
||||||
|
|
||||||
|
RefreshStructuredScreen();
|
||||||
|
std::vector<UIInputEvent> frameEvents = std::move(m_pendingInputEvents);
|
||||||
|
m_pendingInputEvents.clear();
|
||||||
|
|
||||||
|
UIDrawData drawData = {};
|
||||||
|
if (m_useStructuredScreen && m_screenPlayer.IsLoaded()) {
|
||||||
|
UIScreenFrameInput input = {};
|
||||||
|
input.viewportRect = UIRect(0.0f, 0.0f, width, height);
|
||||||
|
input.events = std::move(frameEvents);
|
||||||
|
input.deltaTimeSeconds = deltaTimeSeconds;
|
||||||
|
input.frameIndex = ++m_frameIndex;
|
||||||
|
input.focused = GetForegroundWindow() == m_hwnd;
|
||||||
|
|
||||||
|
const auto& frame = m_screenPlayer.Update(input);
|
||||||
|
for (const auto& drawList : frame.drawData.GetDrawLists()) {
|
||||||
|
drawData.AddDrawList(drawList);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_runtimeStatus = m_activeScenario != nullptr
|
||||||
|
? m_activeScenario->displayName
|
||||||
|
: "Editor UI Validation";
|
||||||
|
m_runtimeError = frame.errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawData.Empty()) {
|
||||||
|
m_runtimeStatus = "Editor UI Validation | Load Error";
|
||||||
|
if (m_runtimeError.empty() && !m_screenPlayer.IsLoaded()) {
|
||||||
|
m_runtimeError = m_screenPlayer.GetLastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendRuntimeOverlay(drawData, width, height);
|
||||||
|
|
||||||
|
const bool framePresented = m_renderer.Render(drawData);
|
||||||
|
m_autoScreenshot.CaptureIfRequested(
|
||||||
|
m_renderer,
|
||||||
|
drawData,
|
||||||
|
static_cast<unsigned int>(width),
|
||||||
|
static_cast<unsigned int>(height),
|
||||||
|
framePresented);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::OnResize(UINT width, UINT height) {
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_renderer.Resize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.pointerButton = button;
|
||||||
|
event.position = UIPoint(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<size_t>(wParam));
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueuePointerLeaveEvent() {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::PointerLeave;
|
||||||
|
if (m_hwnd != nullptr) {
|
||||||
|
POINT clientPoint = {};
|
||||||
|
GetCursorPos(&clientPoint);
|
||||||
|
ScreenToClient(m_hwnd, &clientPoint);
|
||||||
|
event.position = UIPoint(static_cast<float>(clientPoint.x), static_cast<float>(clientPoint.y));
|
||||||
|
}
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT screenPoint = {
|
||||||
|
GET_X_LPARAM(lParam),
|
||||||
|
GET_Y_LPARAM(lParam)
|
||||||
|
};
|
||||||
|
ScreenToClient(m_hwnd, &screenPoint);
|
||||||
|
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::PointerWheel;
|
||||||
|
event.position = UIPoint(static_cast<float>(screenPoint.x), static_cast<float>(screenPoint.y));
|
||||||
|
event.wheelDelta = static_cast<float>(wheelDelta);
|
||||||
|
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<size_t>(wParam));
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueueKeyEvent(UIInputEventType type, WPARAM wParam, LPARAM lParam) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.keyCode = MapVirtualKeyToUIKeyCode(wParam);
|
||||||
|
event.modifiers = m_inputModifierTracker.ApplyKeyMessage(type, wParam, lParam);
|
||||||
|
event.repeat = IsRepeatKeyMessage(lParam);
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueueCharacterEvent(WPARAM wParam, LPARAM) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::Character;
|
||||||
|
event.character = static_cast<std::uint32_t>(wParam);
|
||||||
|
event.modifiers = m_inputModifierTracker.GetCurrentModifiers();
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::QueueWindowFocusEvent(UIInputEventType type) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Application::LoadStructuredScreen(const char* triggerReason) {
|
||||||
|
(void)triggerReason;
|
||||||
|
std::string scenarioLoadWarning = {};
|
||||||
|
const EditorValidationScenario* scenario = m_requestedScenarioId.empty()
|
||||||
|
? &GetDefaultEditorValidationScenario()
|
||||||
|
: FindEditorValidationScenario(m_requestedScenarioId);
|
||||||
|
if (scenario == nullptr) {
|
||||||
|
scenario = &GetDefaultEditorValidationScenario();
|
||||||
|
scenarioLoadWarning = "Unknown validation scenario: " + m_requestedScenarioId;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_activeScenario = scenario;
|
||||||
|
m_screenAsset = {};
|
||||||
|
m_screenAsset.screenId = scenario->id;
|
||||||
|
m_screenAsset.documentPath = scenario->documentPath.string();
|
||||||
|
m_screenAsset.themePath = scenario->themePath.string();
|
||||||
|
|
||||||
|
const bool loaded = m_screenPlayer.Load(m_screenAsset);
|
||||||
|
m_useStructuredScreen = loaded;
|
||||||
|
m_runtimeStatus = loaded ? scenario->displayName : "Editor UI Validation | Load Error";
|
||||||
|
m_runtimeError = loaded
|
||||||
|
? scenarioLoadWarning
|
||||||
|
: (scenarioLoadWarning.empty()
|
||||||
|
? m_screenPlayer.GetLastError()
|
||||||
|
: scenarioLoadWarning + " | " + m_screenPlayer.GetLastError());
|
||||||
|
RebuildTrackedFileStates();
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::RefreshStructuredScreen() {
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (m_lastReloadPollTime.time_since_epoch().count() != 0 &&
|
||||||
|
now - m_lastReloadPollTime < kReloadPollInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lastReloadPollTime = now;
|
||||||
|
if (DetectTrackedFileChange()) {
|
||||||
|
LoadStructuredScreen("reload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::RebuildTrackedFileStates() {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
m_trackedFiles.clear();
|
||||||
|
std::unordered_set<std::string> seenPaths = {};
|
||||||
|
std::error_code errorCode = {};
|
||||||
|
|
||||||
|
auto appendTrackedPath = [&](const std::string& rawPath) {
|
||||||
|
if (rawPath.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs::path normalizedPath = fs::path(rawPath).lexically_normal();
|
||||||
|
const std::string key = normalizedPath.string();
|
||||||
|
if (!seenPaths.insert(key).second) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackedFileState state = {};
|
||||||
|
state.path = normalizedPath;
|
||||||
|
state.exists = fs::exists(normalizedPath, errorCode);
|
||||||
|
errorCode.clear();
|
||||||
|
if (state.exists) {
|
||||||
|
state.writeTime = fs::last_write_time(normalizedPath, errorCode);
|
||||||
|
errorCode.clear();
|
||||||
|
}
|
||||||
|
m_trackedFiles.push_back(std::move(state));
|
||||||
|
};
|
||||||
|
|
||||||
|
appendTrackedPath(m_screenAsset.documentPath);
|
||||||
|
appendTrackedPath(m_screenAsset.themePath);
|
||||||
|
|
||||||
|
if (const auto* document = m_screenPlayer.GetDocument(); document != nullptr) {
|
||||||
|
for (const std::string& dependency : document->dependencies) {
|
||||||
|
appendTrackedPath(dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Application::DetectTrackedFileChange() const {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
std::error_code errorCode = {};
|
||||||
|
for (const TrackedFileState& trackedFile : m_trackedFiles) {
|
||||||
|
const bool existsNow = fs::exists(trackedFile.path, errorCode);
|
||||||
|
errorCode.clear();
|
||||||
|
if (existsNow != trackedFile.exists) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsNow) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto writeTimeNow = fs::last_write_time(trackedFile.path, errorCode);
|
||||||
|
errorCode.clear();
|
||||||
|
if (writeTimeNow != trackedFile.writeTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const {
|
||||||
|
const bool authoredMode = m_useStructuredScreen && m_screenPlayer.IsLoaded();
|
||||||
|
const float panelWidth = authoredMode ? 460.0f : 360.0f;
|
||||||
|
std::vector<std::string> detailLines = {};
|
||||||
|
detailLines.push_back(
|
||||||
|
authoredMode
|
||||||
|
? "Hot reload watches authored UI resources."
|
||||||
|
: "Authored validation scene failed to load.");
|
||||||
|
if (m_activeScenario != nullptr) {
|
||||||
|
detailLines.push_back("Scenario: " + m_activeScenario->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authoredMode) {
|
||||||
|
const auto& inputDebug = m_documentHost.GetInputDebugSnapshot();
|
||||||
|
detailLines.push_back(
|
||||||
|
"Hover | Focus: " +
|
||||||
|
ExtractStateKeyTail(inputDebug.hoveredStateKey) +
|
||||||
|
" | " +
|
||||||
|
ExtractStateKeyTail(inputDebug.focusedStateKey));
|
||||||
|
detailLines.push_back(
|
||||||
|
"Active | Capture: " +
|
||||||
|
ExtractStateKeyTail(inputDebug.activeStateKey) +
|
||||||
|
" | " +
|
||||||
|
ExtractStateKeyTail(inputDebug.captureStateKey));
|
||||||
|
detailLines.push_back(
|
||||||
|
"Scope W/P/Wg: " +
|
||||||
|
ExtractStateKeyTail(inputDebug.windowScopeStateKey) +
|
||||||
|
" | " +
|
||||||
|
ExtractStateKeyTail(inputDebug.panelScopeStateKey) +
|
||||||
|
" | " +
|
||||||
|
ExtractStateKeyTail(inputDebug.widgetScopeStateKey));
|
||||||
|
detailLines.push_back(
|
||||||
|
std::string("Text input: ") +
|
||||||
|
(inputDebug.textInputActive ? "active" : "idle"));
|
||||||
|
if (!inputDebug.recentShortcutCommandId.empty()) {
|
||||||
|
detailLines.push_back(
|
||||||
|
"Recent shortcut: " +
|
||||||
|
inputDebug.recentShortcutScope +
|
||||||
|
" -> " +
|
||||||
|
inputDebug.recentShortcutCommandId);
|
||||||
|
detailLines.push_back(
|
||||||
|
std::string("Recent shortcut state: ") +
|
||||||
|
(inputDebug.recentShortcutHandled
|
||||||
|
? "handled"
|
||||||
|
: (inputDebug.recentShortcutSuppressed ? "suppressed" : "observed")) +
|
||||||
|
" @ " +
|
||||||
|
ExtractStateKeyTail(inputDebug.recentShortcutOwnerStateKey));
|
||||||
|
} else {
|
||||||
|
detailLines.push_back("Recent shortcut: none");
|
||||||
|
}
|
||||||
|
if (!inputDebug.lastEventType.empty()) {
|
||||||
|
const std::string eventPosition = inputDebug.lastEventType == "KeyDown" ||
|
||||||
|
inputDebug.lastEventType == "KeyUp" ||
|
||||||
|
inputDebug.lastEventType == "Character" ||
|
||||||
|
inputDebug.lastEventType == "FocusGained" ||
|
||||||
|
inputDebug.lastEventType == "FocusLost"
|
||||||
|
? std::string()
|
||||||
|
: " at " + FormatPoint(inputDebug.pointerPosition);
|
||||||
|
detailLines.push_back(
|
||||||
|
"Last input: " +
|
||||||
|
inputDebug.lastEventType +
|
||||||
|
eventPosition);
|
||||||
|
detailLines.push_back(
|
||||||
|
"Route: " +
|
||||||
|
inputDebug.lastTargetKind +
|
||||||
|
" -> " +
|
||||||
|
ExtractStateKeyTail(inputDebug.lastTargetStateKey));
|
||||||
|
if (!inputDebug.lastShortcutCommandId.empty()) {
|
||||||
|
detailLines.push_back(
|
||||||
|
"Shortcut: " +
|
||||||
|
inputDebug.lastShortcutScope +
|
||||||
|
" -> " +
|
||||||
|
inputDebug.lastShortcutCommandId);
|
||||||
|
detailLines.push_back(
|
||||||
|
std::string("Shortcut state: ") +
|
||||||
|
(inputDebug.lastShortcutHandled
|
||||||
|
? "handled"
|
||||||
|
: (inputDebug.lastShortcutSuppressed ? "suppressed" : "observed")) +
|
||||||
|
" @ " +
|
||||||
|
ExtractStateKeyTail(inputDebug.lastShortcutOwnerStateKey));
|
||||||
|
}
|
||||||
|
detailLines.push_back(
|
||||||
|
"Last event result: " +
|
||||||
|
(inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_autoScreenshot.HasPendingCapture()) {
|
||||||
|
detailLines.push_back("Shot pending...");
|
||||||
|
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
|
||||||
|
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u));
|
||||||
|
} else {
|
||||||
|
detailLines.push_back("Screenshots: F12 -> current scenario captures/");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_runtimeError.empty()) {
|
||||||
|
detailLines.push_back(TruncateText(m_runtimeError, 78u));
|
||||||
|
} else if (!m_autoScreenshot.GetLastCaptureError().empty()) {
|
||||||
|
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureError(), 78u));
|
||||||
|
} else if (!authoredMode) {
|
||||||
|
detailLines.push_back("No fallback sandbox is rendered in this host.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const float panelHeight = 38.0f + static_cast<float>(detailLines.size()) * 18.0f;
|
||||||
|
const UIRect panelRect(width - panelWidth - 16.0f, height - panelHeight - 42.0f, panelWidth, panelHeight);
|
||||||
|
|
||||||
|
UIDrawList& overlay = drawData.EmplaceDrawList("Editor UI Validation Overlay");
|
||||||
|
overlay.AddFilledRect(panelRect, kOverlayBgColor, 10.0f);
|
||||||
|
overlay.AddRectOutline(panelRect, kOverlayBorderColor, 1.0f, 10.0f);
|
||||||
|
overlay.AddFilledRect(
|
||||||
|
UIRect(panelRect.x + 12.0f, panelRect.y + 14.0f, 8.0f, 8.0f),
|
||||||
|
authoredMode ? kOverlaySuccess : kOverlayFallback,
|
||||||
|
4.0f);
|
||||||
|
overlay.AddText(
|
||||||
|
UIPoint(panelRect.x + 28.0f, panelRect.y + 10.0f),
|
||||||
|
m_runtimeStatus.empty() ? "Editor UI Validation" : m_runtimeStatus,
|
||||||
|
kOverlayTextPrimary,
|
||||||
|
14.0f);
|
||||||
|
|
||||||
|
float detailY = panelRect.y + 30.0f;
|
||||||
|
for (std::size_t index = 0; index < detailLines.size(); ++index) {
|
||||||
|
const bool lastLine = index + 1u == detailLines.size();
|
||||||
|
overlay.AddText(
|
||||||
|
UIPoint(panelRect.x + 28.0f, detailY),
|
||||||
|
detailLines[index],
|
||||||
|
lastLine && (!m_runtimeError.empty() || !m_autoScreenshot.GetLastCaptureError().empty())
|
||||||
|
? kOverlayFallback
|
||||||
|
: kOverlayTextMuted,
|
||||||
|
12.0f);
|
||||||
|
detailY += 18.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path Application::ResolveRepoRelativePath(const char* relativePath) {
|
||||||
|
return (GetRepoRootPath() / relativePath).lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||||
|
auto* application = reinterpret_cast<Application*>(createStruct->lpCreateParams);
|
||||||
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(application));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Application* application = GetApplicationFromWindow(hwnd);
|
||||||
|
switch (message) {
|
||||||
|
case WM_SIZE:
|
||||||
|
if (application != nullptr && wParam != SIZE_MINIMIZED) {
|
||||||
|
application->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case WM_PAINT:
|
||||||
|
if (application != nullptr) {
|
||||||
|
PAINTSTRUCT paintStruct = {};
|
||||||
|
BeginPaint(hwnd, &paintStruct);
|
||||||
|
application->RenderFrame();
|
||||||
|
EndPaint(hwnd, &paintStruct);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
if (application != nullptr) {
|
||||||
|
if (!application->m_trackingMouseLeave) {
|
||||||
|
TRACKMOUSEEVENT trackMouseEvent = {};
|
||||||
|
trackMouseEvent.cbSize = sizeof(trackMouseEvent);
|
||||||
|
trackMouseEvent.dwFlags = TME_LEAVE;
|
||||||
|
trackMouseEvent.hwndTrack = hwnd;
|
||||||
|
if (TrackMouseEvent(&trackMouseEvent)) {
|
||||||
|
application->m_trackingMouseLeave = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
application->QueuePointerEvent(UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_MOUSELEAVE:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->m_trackingMouseLeave = false;
|
||||||
|
application->QueuePointerLeaveEvent();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_LBUTTONDOWN:
|
||||||
|
if (application != nullptr) {
|
||||||
|
SetFocus(hwnd);
|
||||||
|
SetCapture(hwnd);
|
||||||
|
application->QueuePointerEvent(UIInputEventType::PointerButtonDown, UIPointerButton::Left, wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_LBUTTONUP:
|
||||||
|
if (application != nullptr) {
|
||||||
|
if (GetCapture() == hwnd) {
|
||||||
|
ReleaseCapture();
|
||||||
|
}
|
||||||
|
application->QueuePointerEvent(UIInputEventType::PointerButtonUp, UIPointerButton::Left, wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_MOUSEWHEEL:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_SETFOCUS:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->m_inputModifierTracker.SyncFromSystemState();
|
||||||
|
application->QueueWindowFocusEvent(UIInputEventType::FocusGained);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_KILLFOCUS:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->m_inputModifierTracker.Reset();
|
||||||
|
application->QueueWindowFocusEvent(UIInputEventType::FocusLost);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_KEYDOWN:
|
||||||
|
case WM_SYSKEYDOWN:
|
||||||
|
if (application != nullptr) {
|
||||||
|
if (wParam == VK_F12) {
|
||||||
|
application->m_autoScreenshot.RequestCapture("manual_f12");
|
||||||
|
}
|
||||||
|
application->QueueKeyEvent(UIInputEventType::KeyDown, wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_KEYUP:
|
||||||
|
case WM_SYSKEYUP:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->QueueKeyEvent(UIInputEventType::KeyUp, wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_CHAR:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->QueueCharacterEvent(wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_ERASEBKGND:
|
||||||
|
return 1;
|
||||||
|
case WM_DESTROY:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->m_hwnd = nullptr;
|
||||||
|
}
|
||||||
|
PostQuitMessage(0);
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
int RunEditorUIValidationApp(HINSTANCE hInstance, int nCmdShow, std::string requestedScenarioId) {
|
||||||
|
Application application(std::move(requestedScenarioId));
|
||||||
|
return application.Run(hInstance, nCmdShow);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::Tests::EditorUI
|
||||||
83
tests/UI/Editor/integration/shared/src/Application.h
Normal file
83
tests/UI/Editor/integration/shared/src/Application.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "EditorValidationScenario.h"
|
||||||
|
#include <XCNewEditor/Host/AutoScreenshot.h>
|
||||||
|
#include <XCNewEditor/Host/InputModifierTracker.h>
|
||||||
|
#include <XCNewEditor/Host/NativeRenderer.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||||
|
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine::Tests::EditorUI {
|
||||||
|
|
||||||
|
class Application {
|
||||||
|
public:
|
||||||
|
explicit Application(std::string requestedScenarioId = {});
|
||||||
|
|
||||||
|
int Run(HINSTANCE hInstance, int nCmdShow);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct TrackedFileState {
|
||||||
|
std::filesystem::path path = {};
|
||||||
|
std::filesystem::file_time_type writeTime = {};
|
||||||
|
bool exists = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
|
|
||||||
|
bool Initialize(HINSTANCE hInstance, int nCmdShow);
|
||||||
|
void Shutdown();
|
||||||
|
void RenderFrame();
|
||||||
|
void OnResize(UINT width, UINT height);
|
||||||
|
void QueuePointerEvent(::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, WPARAM wParam, LPARAM lParam);
|
||||||
|
void QueuePointerLeaveEvent();
|
||||||
|
void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam);
|
||||||
|
void QueueKeyEvent(::XCEngine::UI::UIInputEventType type, WPARAM wParam, LPARAM lParam);
|
||||||
|
void QueueCharacterEvent(WPARAM wParam, LPARAM lParam);
|
||||||
|
void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type);
|
||||||
|
bool LoadStructuredScreen(const char* triggerReason);
|
||||||
|
void RefreshStructuredScreen();
|
||||||
|
void RebuildTrackedFileStates();
|
||||||
|
bool DetectTrackedFileChange() const;
|
||||||
|
void AppendRuntimeOverlay(::XCEngine::UI::UIDrawData& drawData, float width, float height) const;
|
||||||
|
static std::filesystem::path ResolveRepoRelativePath(const char* relativePath);
|
||||||
|
|
||||||
|
HWND m_hwnd = nullptr;
|
||||||
|
HINSTANCE m_hInstance = nullptr;
|
||||||
|
ATOM m_windowClassAtom = 0;
|
||||||
|
::XCEngine::XCUI::Host::NativeRenderer m_renderer;
|
||||||
|
::XCEngine::XCUI::Host::AutoScreenshotController m_autoScreenshot;
|
||||||
|
::XCEngine::UI::Runtime::UIDocumentScreenHost m_documentHost;
|
||||||
|
::XCEngine::UI::Runtime::UIScreenPlayer m_screenPlayer;
|
||||||
|
::XCEngine::UI::Runtime::UIScreenAsset m_screenAsset = {};
|
||||||
|
const EditorValidationScenario* m_activeScenario = nullptr;
|
||||||
|
std::string m_requestedScenarioId = {};
|
||||||
|
std::vector<TrackedFileState> m_trackedFiles = {};
|
||||||
|
std::chrono::steady_clock::time_point m_startTime = {};
|
||||||
|
std::chrono::steady_clock::time_point m_lastFrameTime = {};
|
||||||
|
std::chrono::steady_clock::time_point m_lastReloadPollTime = {};
|
||||||
|
std::uint64_t m_frameIndex = 0;
|
||||||
|
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
|
||||||
|
::XCEngine::XCUI::Host::InputModifierTracker m_inputModifierTracker = {};
|
||||||
|
bool m_trackingMouseLeave = false;
|
||||||
|
bool m_useStructuredScreen = false;
|
||||||
|
std::string m_runtimeStatus = {};
|
||||||
|
std::string m_runtimeError = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
int RunEditorUIValidationApp(HINSTANCE hInstance, int nCmdShow, std::string requestedScenarioId = {});
|
||||||
|
|
||||||
|
} // namespace XCEngine::Tests::EditorUI
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "EditorValidationScenario.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||||
|
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace XCEngine::Tests::EditorUI {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
fs::path RepoRootPath() {
|
||||||
|
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||||
|
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||||
|
root = root.substr(1u, root.size() - 2u);
|
||||||
|
}
|
||||||
|
return fs::path(root).lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path RepoRelative(const char* relativePath) {
|
||||||
|
return (RepoRootPath() / relativePath).lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array<EditorValidationScenario, 4>& GetEditorValidationScenarios() {
|
||||||
|
static const std::array<EditorValidationScenario, 4> scenarios = { {
|
||||||
|
{
|
||||||
|
"editor.input.keyboard_focus",
|
||||||
|
UIValidationDomain::Editor,
|
||||||
|
"input",
|
||||||
|
"Editor Input | Keyboard Focus",
|
||||||
|
RepoRelative("tests/UI/Editor/integration/input/keyboard_focus/View.xcui"),
|
||||||
|
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
|
||||||
|
RepoRelative("tests/UI/Editor/integration/input/keyboard_focus/captures")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editor.input.pointer_states",
|
||||||
|
UIValidationDomain::Editor,
|
||||||
|
"input",
|
||||||
|
"Editor Input | Pointer States",
|
||||||
|
RepoRelative("tests/UI/Editor/integration/input/pointer_states/View.xcui"),
|
||||||
|
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
|
||||||
|
RepoRelative("tests/UI/Editor/integration/input/pointer_states/captures")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editor.input.shortcut_scope",
|
||||||
|
UIValidationDomain::Editor,
|
||||||
|
"input",
|
||||||
|
"Editor Input | Shortcut Scope",
|
||||||
|
RepoRelative("tests/UI/Editor/integration/input/shortcut_scope/View.xcui"),
|
||||||
|
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
|
||||||
|
RepoRelative("tests/UI/Editor/integration/input/shortcut_scope/captures")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editor.layout.splitter_resize",
|
||||||
|
UIValidationDomain::Editor,
|
||||||
|
"layout",
|
||||||
|
"Editor Layout | Splitter Resize",
|
||||||
|
RepoRelative("tests/UI/Editor/integration/layout/splitter_resize/View.xcui"),
|
||||||
|
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
|
||||||
|
RepoRelative("tests/UI/Editor/integration/layout/splitter_resize/captures")
|
||||||
|
}
|
||||||
|
} };
|
||||||
|
return scenarios;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const EditorValidationScenario& GetDefaultEditorValidationScenario() {
|
||||||
|
return GetEditorValidationScenarios().front();
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditorValidationScenario* FindEditorValidationScenario(std::string_view id) {
|
||||||
|
for (const EditorValidationScenario& scenario : GetEditorValidationScenarios()) {
|
||||||
|
if (scenario.id == id) {
|
||||||
|
return &scenario;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::Tests::EditorUI
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace XCEngine::Tests::EditorUI {
|
||||||
|
|
||||||
|
enum class UIValidationDomain : unsigned char {
|
||||||
|
Editor = 0,
|
||||||
|
Runtime
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EditorValidationScenario {
|
||||||
|
std::string id = {};
|
||||||
|
UIValidationDomain domain = UIValidationDomain::Editor;
|
||||||
|
std::string categoryId = {};
|
||||||
|
std::string displayName = {};
|
||||||
|
std::filesystem::path documentPath = {};
|
||||||
|
std::filesystem::path themePath = {};
|
||||||
|
std::filesystem::path captureRootPath = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditorValidationScenario& GetDefaultEditorValidationScenario();
|
||||||
|
const EditorValidationScenario* FindEditorValidationScenario(std::string_view id);
|
||||||
|
|
||||||
|
} // namespace XCEngine::Tests::EditorUI
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<Theme name="EditorValidationTheme">
|
||||||
|
<Tokens>
|
||||||
|
<Color name="color.bg.workspace" value="#1C1C1C" />
|
||||||
|
<Color name="color.bg.panel" value="#292929" />
|
||||||
|
<Color name="color.bg.accent" value="#3A3A3A" />
|
||||||
|
<Color name="color.bg.selection" value="#4A4A4A" />
|
||||||
|
<Color name="color.text.primary" value="#EEEEEE" />
|
||||||
|
<Color name="color.text.muted" value="#B0B0B0" />
|
||||||
|
<Spacing name="space.panel" value="12" />
|
||||||
|
<Spacing name="space.shell" value="18" />
|
||||||
|
<Radius name="radius.panel" value="10" />
|
||||||
|
<Radius name="radius.control" value="8" />
|
||||||
|
</Tokens>
|
||||||
|
|
||||||
|
<Widgets>
|
||||||
|
<Widget type="View" style="EditorWorkspace">
|
||||||
|
<Property name="background" value="color.bg.workspace" />
|
||||||
|
<Property name="padding" value="space.shell" />
|
||||||
|
</Widget>
|
||||||
|
|
||||||
|
<Widget type="Card" style="EditorPanel">
|
||||||
|
<Property name="background" value="color.bg.panel" />
|
||||||
|
<Property name="radius" value="radius.panel" />
|
||||||
|
<Property name="padding" value="space.panel" />
|
||||||
|
</Widget>
|
||||||
|
|
||||||
|
<Widget type="Button" style="EditorChip">
|
||||||
|
<Property name="background" value="color.bg.selection" />
|
||||||
|
<Property name="radius" value="radius.control" />
|
||||||
|
</Widget>
|
||||||
|
</Widgets>
|
||||||
|
</Theme>
|
||||||
35
tests/UI/Editor/unit/CMakeLists.txt
Normal file
35
tests/UI/Editor/unit/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
set(EDITOR_UI_UNIT_TEST_SOURCES
|
||||||
|
test_input_modifier_tracker.cpp
|
||||||
|
test_editor_validation_registry.cpp
|
||||||
|
test_structured_editor_shell.cpp
|
||||||
|
# Migration bridge: editor-facing XCUI primitive tests still reuse the
|
||||||
|
# legacy source location until they are relocated under tests/UI/Editor/unit.
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_editor_collection_primitives.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_editor_panel_chrome.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(editor_ui_tests ${EDITOR_UI_UNIT_TEST_SOURCES})
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_tests
|
||||||
|
PRIVATE
|
||||||
|
editor_ui_validation_registry
|
||||||
|
XCNewEditorLib
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_tests
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/include
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/src
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_tests PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_tests PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(editor_ui_tests)
|
||||||
48
tests/UI/Editor/unit/test_editor_validation_registry.cpp
Normal file
48
tests/UI/Editor/unit/test_editor_validation_registry.cpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "EditorValidationScenario.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Tests::EditorUI::FindEditorValidationScenario;
|
||||||
|
using XCEngine::Tests::EditorUI::GetDefaultEditorValidationScenario;
|
||||||
|
using XCEngine::Tests::EditorUI::UIValidationDomain;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(EditorValidationRegistryTest, KnownEditorValidationScenariosResolveToExistingResources) {
|
||||||
|
const auto* pointerScenario = FindEditorValidationScenario("editor.input.pointer_states");
|
||||||
|
const auto* keyboardScenario = FindEditorValidationScenario("editor.input.keyboard_focus");
|
||||||
|
const auto* shortcutScenario = FindEditorValidationScenario("editor.input.shortcut_scope");
|
||||||
|
const auto* splitterScenario = FindEditorValidationScenario("editor.layout.splitter_resize");
|
||||||
|
|
||||||
|
ASSERT_NE(pointerScenario, nullptr);
|
||||||
|
ASSERT_NE(keyboardScenario, nullptr);
|
||||||
|
ASSERT_NE(shortcutScenario, nullptr);
|
||||||
|
ASSERT_NE(splitterScenario, nullptr);
|
||||||
|
EXPECT_EQ(pointerScenario->domain, UIValidationDomain::Editor);
|
||||||
|
EXPECT_EQ(keyboardScenario->domain, UIValidationDomain::Editor);
|
||||||
|
EXPECT_EQ(shortcutScenario->domain, UIValidationDomain::Editor);
|
||||||
|
EXPECT_EQ(splitterScenario->domain, UIValidationDomain::Editor);
|
||||||
|
EXPECT_EQ(pointerScenario->categoryId, "input");
|
||||||
|
EXPECT_EQ(keyboardScenario->categoryId, "input");
|
||||||
|
EXPECT_EQ(shortcutScenario->categoryId, "input");
|
||||||
|
EXPECT_EQ(splitterScenario->categoryId, "layout");
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(pointerScenario->documentPath));
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(pointerScenario->themePath));
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(keyboardScenario->documentPath));
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(keyboardScenario->themePath));
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(shortcutScenario->documentPath));
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(shortcutScenario->themePath));
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(splitterScenario->documentPath));
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(splitterScenario->themePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EditorValidationRegistryTest, DefaultScenarioPointsToKeyboardFocusBatch) {
|
||||||
|
const auto& scenario = GetDefaultEditorValidationScenario();
|
||||||
|
EXPECT_EQ(scenario.id, "editor.input.keyboard_focus");
|
||||||
|
EXPECT_EQ(scenario.domain, UIValidationDomain::Editor);
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(scenario.documentPath));
|
||||||
|
}
|
||||||
90
tests/UI/Editor/unit/test_input_modifier_tracker.cpp
Normal file
90
tests/UI/Editor/unit/test_input_modifier_tracker.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCNewEditor/Host/InputModifierTracker.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Types.h>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::XCUI::Host::InputModifierTracker;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
|
||||||
|
TEST(InputModifierTrackerTest, ControlStatePersistsAcrossChordKeyDownAndClearsOnKeyUp) {
|
||||||
|
InputModifierTracker tracker = {};
|
||||||
|
|
||||||
|
const auto ctrlDown = tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyDown,
|
||||||
|
VK_CONTROL,
|
||||||
|
0x001D0001);
|
||||||
|
EXPECT_TRUE(ctrlDown.control);
|
||||||
|
EXPECT_FALSE(ctrlDown.shift);
|
||||||
|
EXPECT_FALSE(ctrlDown.alt);
|
||||||
|
|
||||||
|
const auto chordKeyDown = tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyDown,
|
||||||
|
'P',
|
||||||
|
0x00190001);
|
||||||
|
EXPECT_TRUE(chordKeyDown.control);
|
||||||
|
|
||||||
|
const auto ctrlUp = tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyUp,
|
||||||
|
VK_CONTROL,
|
||||||
|
static_cast<LPARAM>(0xC01D0001u));
|
||||||
|
EXPECT_FALSE(ctrlUp.control);
|
||||||
|
|
||||||
|
const auto nextKeyDown = tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyDown,
|
||||||
|
'P',
|
||||||
|
0x00190001);
|
||||||
|
EXPECT_FALSE(nextKeyDown.control);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InputModifierTrackerTest, PointerModifiersMergeMouseFlagsWithTrackedKeyboardState) {
|
||||||
|
InputModifierTracker tracker = {};
|
||||||
|
tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyDown,
|
||||||
|
VK_MENU,
|
||||||
|
0x00380001);
|
||||||
|
|
||||||
|
const auto modifiers = tracker.BuildPointerModifiers(MK_SHIFT);
|
||||||
|
EXPECT_TRUE(modifiers.shift);
|
||||||
|
EXPECT_TRUE(modifiers.alt);
|
||||||
|
EXPECT_FALSE(modifiers.control);
|
||||||
|
EXPECT_FALSE(modifiers.super);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InputModifierTrackerTest, RightControlIsTrackedIndependentlyFromLeftControl) {
|
||||||
|
InputModifierTracker tracker = {};
|
||||||
|
|
||||||
|
tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyDown,
|
||||||
|
VK_CONTROL,
|
||||||
|
static_cast<LPARAM>(0x011D0001u));
|
||||||
|
EXPECT_TRUE(tracker.GetCurrentModifiers().control);
|
||||||
|
|
||||||
|
tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyDown,
|
||||||
|
VK_CONTROL,
|
||||||
|
0x001D0001);
|
||||||
|
EXPECT_TRUE(tracker.GetCurrentModifiers().control);
|
||||||
|
|
||||||
|
tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyUp,
|
||||||
|
VK_CONTROL,
|
||||||
|
static_cast<LPARAM>(0xC11D0001u));
|
||||||
|
EXPECT_TRUE(tracker.GetCurrentModifiers().control);
|
||||||
|
|
||||||
|
tracker.ApplyKeyMessage(
|
||||||
|
UIInputEventType::KeyUp,
|
||||||
|
VK_CONTROL,
|
||||||
|
static_cast<LPARAM>(0xC01D0001u));
|
||||||
|
EXPECT_FALSE(tracker.GetCurrentModifiers().control);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
93
tests/UI/Editor/unit/test_structured_editor_shell.cpp
Normal file
93
tests/UI/Editor/unit/test_structured_editor_shell.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "editor/EditorShellAsset.h"
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||||
|
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef XCNEWEDITOR_REPO_ROOT
|
||||||
|
#define XCNEWEDITOR_REPO_ROOT "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::NewEditor::BuildDefaultEditorShellAsset;
|
||||||
|
using XCEngine::UI::UIDrawCommand;
|
||||||
|
using XCEngine::UI::UIDrawCommandType;
|
||||||
|
using XCEngine::UI::UIDrawData;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenAsset;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenPlayer;
|
||||||
|
using XCEngine::UI::Runtime::UIDocumentScreenHost;
|
||||||
|
|
||||||
|
std::filesystem::path RepoRootPath() {
|
||||||
|
std::string root = XCNEWEDITOR_REPO_ROOT;
|
||||||
|
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||||
|
root = root.substr(1u, root.size() - 2u);
|
||||||
|
}
|
||||||
|
return std::filesystem::path(root).lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawDataContainsText(const UIDrawData& drawData, const std::string& text) {
|
||||||
|
for (const auto& drawList : drawData.GetDrawLists()) {
|
||||||
|
for (const UIDrawCommand& command : drawList.GetCommands()) {
|
||||||
|
if (command.type == UIDrawCommandType::Text && command.text == text) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContainsPathWithFilename(
|
||||||
|
const std::vector<std::string>& paths,
|
||||||
|
const char* expectedFileName) {
|
||||||
|
for (const std::string& path : paths) {
|
||||||
|
if (std::filesystem::path(path).filename() == expectedFileName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(EditorUIStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResources) {
|
||||||
|
const auto shell = BuildDefaultEditorShellAsset(RepoRootPath());
|
||||||
|
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(shell.documentPath));
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(shell.themePath));
|
||||||
|
|
||||||
|
UIScreenAsset asset = {};
|
||||||
|
asset.screenId = shell.screenId;
|
||||||
|
asset.documentPath = shell.documentPath.string();
|
||||||
|
asset.themePath = shell.themePath.string();
|
||||||
|
|
||||||
|
UIDocumentScreenHost host = {};
|
||||||
|
UIScreenPlayer player(host);
|
||||||
|
|
||||||
|
ASSERT_TRUE(player.Load(asset)) << player.GetLastError();
|
||||||
|
ASSERT_NE(player.GetDocument(), nullptr);
|
||||||
|
EXPECT_TRUE(player.GetDocument()->hasThemeDocument);
|
||||||
|
EXPECT_TRUE(ContainsPathWithFilename(player.GetDocument()->dependencies, "editor_shell.xctheme"));
|
||||||
|
|
||||||
|
UIScreenFrameInput input = {};
|
||||||
|
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 1440.0f, 900.0f);
|
||||||
|
input.frameIndex = 1u;
|
||||||
|
input.focused = true;
|
||||||
|
|
||||||
|
const auto& frame = player.Update(input);
|
||||||
|
EXPECT_TRUE(frame.stats.documentLoaded);
|
||||||
|
EXPECT_GE(frame.stats.nodeCount, 2u);
|
||||||
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "XCUI Editor Layer"));
|
||||||
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Left Pane Host"));
|
||||||
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Primary Workspace Host"));
|
||||||
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Right Pane Host"));
|
||||||
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Bottom Pane Host"));
|
||||||
|
}
|
||||||
17
tests/UI/Runtime/CMakeLists.txt
Normal file
17
tests/UI/Runtime/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
|
||||||
|
project(XCEngine_RuntimeUITests)
|
||||||
|
|
||||||
|
add_subdirectory(unit)
|
||||||
|
add_subdirectory(integration)
|
||||||
|
|
||||||
|
add_custom_target(runtime_ui_unit_tests
|
||||||
|
DEPENDS
|
||||||
|
runtime_ui_tests
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(runtime_ui_all_tests
|
||||||
|
DEPENDS
|
||||||
|
runtime_ui_unit_tests
|
||||||
|
runtime_ui_integration_tests
|
||||||
|
)
|
||||||
1
tests/UI/Runtime/integration/CMakeLists.txt
Normal file
1
tests/UI/Runtime/integration/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
add_custom_target(runtime_ui_integration_tests)
|
||||||
11
tests/UI/Runtime/integration/README.md
Normal file
11
tests/UI/Runtime/integration/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Runtime UI Integration Validation
|
||||||
|
|
||||||
|
This directory is reserved for interactive XCUI runtime validation apps.
|
||||||
|
|
||||||
|
Planned scope:
|
||||||
|
|
||||||
|
- HUD and menu layer-stack validation
|
||||||
|
- Runtime input routing and blocking rules
|
||||||
|
- Screen stack and modal transitions
|
||||||
|
|
||||||
|
For now the runtime UI lane only has automated unit coverage in `tests/UI/Runtime/unit/`.
|
||||||
28
tests/UI/Runtime/unit/CMakeLists.txt
Normal file
28
tests/UI/Runtime/unit/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
set(RUNTIME_UI_TEST_SOURCES
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_shortcut_scope.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_splitter_validation.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_runtime.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(runtime_ui_tests ${RUNTIME_UI_TEST_SOURCES})
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
set_target_properties(runtime_ui_tests PROPERTIES
|
||||||
|
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(runtime_ui_tests
|
||||||
|
PRIVATE
|
||||||
|
XCEngine
|
||||||
|
GTest::gtest
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(runtime_ui_tests PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Fixtures
|
||||||
|
)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(runtime_ui_tests)
|
||||||
305
tests/UI/Runtime/unit/test_ui_runtime_shortcut_scope.cpp
Normal file
305
tests/UI/Runtime/unit/test_ui_runtime_shortcut_scope.cpp
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||||
|
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIDrawCommand;
|
||||||
|
using XCEngine::UI::UIDrawCommandType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenAsset;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenPlayer;
|
||||||
|
using XCEngine::UI::Runtime::UIDocumentScreenHost;
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
class TempFileScope {
|
||||||
|
public:
|
||||||
|
TempFileScope(std::string stem, std::string extension, std::string contents) {
|
||||||
|
const auto uniqueId = std::to_string(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
||||||
|
m_path = fs::temp_directory_path() / (std::move(stem) + "_" + uniqueId + std::move(extension));
|
||||||
|
std::ofstream output(m_path, std::ios::binary | std::ios::trunc);
|
||||||
|
output << contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
~TempFileScope() {
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(m_path, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs::path& Path() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
fs::path m_path = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) {
|
||||||
|
UIScreenAsset screen = {};
|
||||||
|
screen.screenId = screenId;
|
||||||
|
screen.documentPath = viewPath.string();
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
|
||||||
|
UIScreenFrameInput input = {};
|
||||||
|
input.viewportRect = UIRect(0.0f, 0.0f, 960.0f, 720.0f);
|
||||||
|
input.frameIndex = frameIndex;
|
||||||
|
input.focused = true;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIDrawCommand* FindTextCommand(
|
||||||
|
const XCEngine::UI::UIDrawData& drawData,
|
||||||
|
const std::string& text) {
|
||||||
|
for (const auto& drawList : drawData.GetDrawLists()) {
|
||||||
|
for (const UIDrawCommand& command : drawList.GetCommands()) {
|
||||||
|
if (command.type == UIDrawCommandType::Text && command.text == text) {
|
||||||
|
return &command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RectContainsPoint(
|
||||||
|
const UIRect& rect,
|
||||||
|
const UIPoint& point) {
|
||||||
|
return point.x >= rect.x &&
|
||||||
|
point.x <= rect.x + rect.width &&
|
||||||
|
point.y >= rect.y &&
|
||||||
|
point.y <= rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryFindSmallestFilledRectContainingPoint(
|
||||||
|
const XCEngine::UI::UIDrawData& drawData,
|
||||||
|
const UIPoint& point,
|
||||||
|
UIRect& outRect) {
|
||||||
|
bool found = false;
|
||||||
|
float bestArea = (std::numeric_limits<float>::max)();
|
||||||
|
|
||||||
|
for (const auto& drawList : drawData.GetDrawLists()) {
|
||||||
|
for (const UIDrawCommand& command : drawList.GetCommands()) {
|
||||||
|
if (command.type != UIDrawCommandType::FilledRect ||
|
||||||
|
!RectContainsPoint(command.rect, point)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float area = command.rect.width * command.rect.height;
|
||||||
|
if (!found || area < bestArea) {
|
||||||
|
outRect = command.rect;
|
||||||
|
bestArea = area;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryFindFilledRectForText(
|
||||||
|
const XCEngine::UI::UIDrawData& drawData,
|
||||||
|
const std::string& text,
|
||||||
|
UIRect& outRect) {
|
||||||
|
const auto* textCommand = FindTextCommand(drawData, text);
|
||||||
|
return textCommand != nullptr &&
|
||||||
|
TryFindSmallestFilledRectContainingPoint(drawData, textCommand->position, outRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
UIPoint GetRectCenter(const UIRect& rect) {
|
||||||
|
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FocusButton(
|
||||||
|
UIScreenPlayer& player,
|
||||||
|
const UIRect& viewportRect,
|
||||||
|
std::uint64_t& frameIndex,
|
||||||
|
const UIPoint& point) {
|
||||||
|
UIScreenFrameInput downInput = BuildInputState(frameIndex++);
|
||||||
|
downInput.viewportRect = viewportRect;
|
||||||
|
XCEngine::UI::UIInputEvent pointerDown = {};
|
||||||
|
pointerDown.type = XCEngine::UI::UIInputEventType::PointerButtonDown;
|
||||||
|
pointerDown.pointerButton = XCEngine::UI::UIPointerButton::Left;
|
||||||
|
pointerDown.position = point;
|
||||||
|
downInput.events.push_back(pointerDown);
|
||||||
|
player.Update(downInput);
|
||||||
|
|
||||||
|
UIScreenFrameInput upInput = BuildInputState(frameIndex++);
|
||||||
|
upInput.viewportRect = viewportRect;
|
||||||
|
XCEngine::UI::UIInputEvent pointerUp = {};
|
||||||
|
pointerUp.type = XCEngine::UI::UIInputEventType::PointerButtonUp;
|
||||||
|
pointerUp.pointerButton = XCEngine::UI::UIPointerButton::Left;
|
||||||
|
pointerUp.position = point;
|
||||||
|
upInput.events.push_back(pointerUp);
|
||||||
|
player.Update(upInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SendCtrlP(
|
||||||
|
UIScreenPlayer& player,
|
||||||
|
const UIRect& viewportRect,
|
||||||
|
std::uint64_t& frameIndex) {
|
||||||
|
UIScreenFrameInput input = BuildInputState(frameIndex++);
|
||||||
|
input.viewportRect = viewportRect;
|
||||||
|
XCEngine::UI::UIInputEvent event = {};
|
||||||
|
event.type = XCEngine::UI::UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||||
|
event.modifiers.control = true;
|
||||||
|
input.events.push_back(event);
|
||||||
|
player.Update(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SendTab(
|
||||||
|
UIScreenPlayer& player,
|
||||||
|
const UIRect& viewportRect,
|
||||||
|
std::uint64_t& frameIndex) {
|
||||||
|
UIScreenFrameInput input = BuildInputState(frameIndex++);
|
||||||
|
input.viewportRect = viewportRect;
|
||||||
|
XCEngine::UI::UIInputEvent event = {};
|
||||||
|
event.type = XCEngine::UI::UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = static_cast<std::int32_t>(KeyCode::Tab);
|
||||||
|
input.events.push_back(event);
|
||||||
|
player.Update(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UIRuntimeShortcutScopeTest, DocumentHostRoutesFocusedShortcutThroughWidgetPanelWindowAndGlobalScopes) {
|
||||||
|
TempFileScope viewFile(
|
||||||
|
"xcui_runtime_shortcut_scope",
|
||||||
|
".xcui",
|
||||||
|
"<View name=\"Shortcut Scope Test\" shortcut=\"Ctrl+P\" shortcutCommand=\"global.command\" shortcutScope=\"global\">\n"
|
||||||
|
" <Column id=\"root\" padding=\"18\" gap=\"12\">\n"
|
||||||
|
" <Button id=\"global-focus\" text=\"Global Focus\" />\n"
|
||||||
|
" <Card id=\"window-shell\" title=\"Window Scope\" shortcutScopeRoot=\"window\" shortcut=\"Ctrl+P\" shortcutCommand=\"window.command\" shortcutScope=\"window\">\n"
|
||||||
|
" <Column gap=\"10\">\n"
|
||||||
|
" <Button id=\"window-focus\" text=\"Window Focus\" />\n"
|
||||||
|
" <Card id=\"panel-shell\" title=\"Panel Scope\" shortcutScopeRoot=\"panel\" shortcut=\"Ctrl+P\" shortcutCommand=\"panel.command\" shortcutScope=\"panel\">\n"
|
||||||
|
" <Column gap=\"10\">\n"
|
||||||
|
" <Button id=\"panel-focus\" text=\"Panel Focus\" />\n"
|
||||||
|
" <Card id=\"widget-shell\" title=\"Widget Scope\" shortcutScopeRoot=\"widget\" shortcut=\"Ctrl+P\" shortcutCommand=\"widget.command\" shortcutScope=\"widget\">\n"
|
||||||
|
" <Column gap=\"10\">\n"
|
||||||
|
" <Button id=\"widget-focus\" text=\"Widget Focus\" />\n"
|
||||||
|
" </Column>\n"
|
||||||
|
" </Card>\n"
|
||||||
|
" </Column>\n"
|
||||||
|
" </Card>\n"
|
||||||
|
" </Column>\n"
|
||||||
|
" </Card>\n"
|
||||||
|
" </Column>\n"
|
||||||
|
"</View>\n");
|
||||||
|
UIDocumentScreenHost host = {};
|
||||||
|
UIScreenPlayer player(host);
|
||||||
|
|
||||||
|
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.shortcut.scope")));
|
||||||
|
|
||||||
|
UIScreenFrameInput initialInput = BuildInputState(1u);
|
||||||
|
initialInput.viewportRect = UIRect(0.0f, 0.0f, 960.0f, 720.0f);
|
||||||
|
const auto& initialFrame = player.Update(initialInput);
|
||||||
|
|
||||||
|
UIRect globalRect = {};
|
||||||
|
UIRect windowRect = {};
|
||||||
|
UIRect panelRect = {};
|
||||||
|
UIRect widgetRect = {};
|
||||||
|
ASSERT_TRUE(TryFindFilledRectForText(initialFrame.drawData, "Global Focus", globalRect));
|
||||||
|
ASSERT_TRUE(TryFindFilledRectForText(initialFrame.drawData, "Window Focus", windowRect));
|
||||||
|
ASSERT_TRUE(TryFindFilledRectForText(initialFrame.drawData, "Panel Focus", panelRect));
|
||||||
|
ASSERT_TRUE(TryFindFilledRectForText(initialFrame.drawData, "Widget Focus", widgetRect));
|
||||||
|
|
||||||
|
std::uint64_t frameIndex = 2u;
|
||||||
|
|
||||||
|
FocusButton(player, initialInput.viewportRect, frameIndex, GetRectCenter(widgetRect));
|
||||||
|
SendCtrlP(player, initialInput.viewportRect, frameIndex);
|
||||||
|
auto inputDebug = host.GetInputDebugSnapshot();
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutCommandId, "widget.command");
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutScope, "Widget");
|
||||||
|
EXPECT_TRUE(inputDebug.lastShortcutHandled);
|
||||||
|
EXPECT_NE(inputDebug.focusedStateKey.find("/widget-focus"), std::string::npos);
|
||||||
|
EXPECT_NE(inputDebug.widgetScopeStateKey.find("/widget-shell"), std::string::npos);
|
||||||
|
EXPECT_NE(inputDebug.panelScopeStateKey.find("/panel-shell"), std::string::npos);
|
||||||
|
EXPECT_NE(inputDebug.windowScopeStateKey.find("/window-shell"), std::string::npos);
|
||||||
|
|
||||||
|
FocusButton(player, initialInput.viewportRect, frameIndex, GetRectCenter(panelRect));
|
||||||
|
SendCtrlP(player, initialInput.viewportRect, frameIndex);
|
||||||
|
inputDebug = host.GetInputDebugSnapshot();
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutCommandId, "panel.command");
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutScope, "Panel");
|
||||||
|
EXPECT_TRUE(inputDebug.lastShortcutHandled);
|
||||||
|
EXPECT_NE(inputDebug.focusedStateKey.find("/panel-focus"), std::string::npos);
|
||||||
|
|
||||||
|
FocusButton(player, initialInput.viewportRect, frameIndex, GetRectCenter(windowRect));
|
||||||
|
SendCtrlP(player, initialInput.viewportRect, frameIndex);
|
||||||
|
inputDebug = host.GetInputDebugSnapshot();
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutCommandId, "window.command");
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutScope, "Window");
|
||||||
|
EXPECT_TRUE(inputDebug.lastShortcutHandled);
|
||||||
|
EXPECT_NE(inputDebug.focusedStateKey.find("/window-focus"), std::string::npos);
|
||||||
|
|
||||||
|
FocusButton(player, initialInput.viewportRect, frameIndex, GetRectCenter(globalRect));
|
||||||
|
SendCtrlP(player, initialInput.viewportRect, frameIndex);
|
||||||
|
inputDebug = host.GetInputDebugSnapshot();
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutCommandId, "global.command");
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutScope, "Global");
|
||||||
|
EXPECT_TRUE(inputDebug.lastShortcutHandled);
|
||||||
|
EXPECT_NE(inputDebug.focusedStateKey.find("/global-focus"), std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIRuntimeShortcutScopeTest, DocumentHostSuppresssShortcutAndTabTraversalWhenTextInputScopeIsFocused) {
|
||||||
|
TempFileScope viewFile(
|
||||||
|
"xcui_runtime_shortcut_suppression",
|
||||||
|
".xcui",
|
||||||
|
"<View name=\"Shortcut Suppression Test\" shortcut=\"Ctrl+P\" shortcutCommand=\"global.command\" shortcutScope=\"global\">\n"
|
||||||
|
" <Column padding=\"18\" gap=\"12\">\n"
|
||||||
|
" <Card id=\"panel-shell\" title=\"Panel Scope\" shortcutScopeRoot=\"panel\" shortcut=\"Ctrl+P\" shortcutCommand=\"panel.command\" shortcutScope=\"panel\">\n"
|
||||||
|
" <Column gap=\"10\">\n"
|
||||||
|
" <Button id=\"text-input\" text=\"Text Input Proxy\" textInput=\"true\" />\n"
|
||||||
|
" <Button id=\"after-text\" text=\"After Text\" />\n"
|
||||||
|
" </Column>\n"
|
||||||
|
" </Card>\n"
|
||||||
|
" </Column>\n"
|
||||||
|
"</View>\n");
|
||||||
|
UIDocumentScreenHost host = {};
|
||||||
|
UIScreenPlayer player(host);
|
||||||
|
|
||||||
|
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.shortcut.suppression")));
|
||||||
|
|
||||||
|
UIScreenFrameInput initialInput = BuildInputState(1u);
|
||||||
|
initialInput.viewportRect = UIRect(0.0f, 0.0f, 900.0f, 520.0f);
|
||||||
|
const auto& initialFrame = player.Update(initialInput);
|
||||||
|
|
||||||
|
UIRect textInputRect = {};
|
||||||
|
ASSERT_TRUE(TryFindFilledRectForText(initialFrame.drawData, "Text Input Proxy", textInputRect));
|
||||||
|
|
||||||
|
std::uint64_t frameIndex = 2u;
|
||||||
|
FocusButton(player, initialInput.viewportRect, frameIndex, GetRectCenter(textInputRect));
|
||||||
|
SendCtrlP(player, initialInput.viewportRect, frameIndex);
|
||||||
|
|
||||||
|
auto inputDebug = host.GetInputDebugSnapshot();
|
||||||
|
EXPECT_TRUE(inputDebug.textInputActive);
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutCommandId, "panel.command");
|
||||||
|
EXPECT_EQ(inputDebug.lastShortcutScope, "Panel");
|
||||||
|
EXPECT_FALSE(inputDebug.lastShortcutHandled);
|
||||||
|
EXPECT_TRUE(inputDebug.lastShortcutSuppressed);
|
||||||
|
EXPECT_EQ(inputDebug.lastResult, "Shortcut suppressed by text input");
|
||||||
|
EXPECT_NE(inputDebug.focusedStateKey.find("/text-input"), std::string::npos);
|
||||||
|
EXPECT_NE(inputDebug.panelScopeStateKey.find("/panel-shell"), std::string::npos);
|
||||||
|
|
||||||
|
SendTab(player, initialInput.viewportRect, frameIndex);
|
||||||
|
inputDebug = host.GetInputDebugSnapshot();
|
||||||
|
EXPECT_TRUE(inputDebug.textInputActive);
|
||||||
|
EXPECT_EQ(inputDebug.lastResult, "Focus traversal suppressed by text input");
|
||||||
|
EXPECT_NE(inputDebug.focusedStateKey.find("/text-input"), std::string::npos);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||||
|
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenAsset;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||||
|
using XCEngine::UI::Runtime::UIScreenPlayer;
|
||||||
|
using XCEngine::UI::Runtime::UIDocumentScreenHost;
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
class TempFileScope {
|
||||||
|
public:
|
||||||
|
TempFileScope(std::string stem, std::string extension, std::string contents) {
|
||||||
|
const auto uniqueId = std::to_string(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
||||||
|
m_path = fs::temp_directory_path() / (std::move(stem) + "_" + uniqueId + std::move(extension));
|
||||||
|
std::ofstream output(m_path, std::ios::binary | std::ios::trunc);
|
||||||
|
output << contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
~TempFileScope() {
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(m_path, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs::path& Path() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
fs::path m_path = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) {
|
||||||
|
UIScreenAsset screen = {};
|
||||||
|
screen.screenId = screenId;
|
||||||
|
screen.documentPath = viewPath.string();
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
|
||||||
|
UIScreenFrameInput input = {};
|
||||||
|
input.viewportRect = UIRect(0.0f, 0.0f, 960.0f, 720.0f);
|
||||||
|
input.frameIndex = frameIndex;
|
||||||
|
input.focused = true;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UIRuntimeSplitterValidationTest, InvalidSplitterArityProducesExplicitFrameError) {
|
||||||
|
TempFileScope viewFile(
|
||||||
|
"xcui_runtime_invalid_splitter",
|
||||||
|
".xcui",
|
||||||
|
"<View name=\"Invalid Splitter Test\">\n"
|
||||||
|
" <Column padding=\"16\" gap=\"10\">\n"
|
||||||
|
" <Splitter id=\"broken-splitter\" axis=\"horizontal\" splitRatio=\"0.5\">\n"
|
||||||
|
" <Card title=\"Only Child\" />\n"
|
||||||
|
" </Splitter>\n"
|
||||||
|
" </Column>\n"
|
||||||
|
"</View>\n");
|
||||||
|
|
||||||
|
UIDocumentScreenHost host = {};
|
||||||
|
UIScreenPlayer player(host);
|
||||||
|
|
||||||
|
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.invalid_splitter")));
|
||||||
|
|
||||||
|
const auto& frame = player.Update(BuildInputState());
|
||||||
|
EXPECT_FALSE(frame.errorMessage.empty());
|
||||||
|
EXPECT_NE(frame.errorMessage.find("broken-splitter"), std::string::npos);
|
||||||
|
EXPECT_NE(frame.errorMessage.find("exactly 2 child elements"), std::string::npos);
|
||||||
|
}
|
||||||
165
tests/UI/TEST_SPEC.md
Normal file
165
tests/UI/TEST_SPEC.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# XCUI TEST_SPEC
|
||||||
|
|
||||||
|
日期: `2026-04-06`
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
本规范只约束 XCUI 模块自身的测试体系,不负责复刻完整 editor 产品。
|
||||||
|
|
||||||
|
XCUI 测试体系固定为两条并行通道:
|
||||||
|
|
||||||
|
1. `unit`
|
||||||
|
2. `integration`
|
||||||
|
|
||||||
|
两者必须同时存在,但职责严格分离:
|
||||||
|
|
||||||
|
- `unit` 负责规则、状态机、路由与回归稳定性,默认可自动执行。
|
||||||
|
- `integration` 负责生成可操作 exe,让人直接检查交互、布局、焦点、shortcut、滚动与视觉状态。
|
||||||
|
|
||||||
|
## 2. 顶层目录
|
||||||
|
|
||||||
|
XCUI 的测试树统一放在:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/UI/
|
||||||
|
TEST_SPEC.md
|
||||||
|
Core/
|
||||||
|
unit/
|
||||||
|
integration/
|
||||||
|
Runtime/
|
||||||
|
unit/
|
||||||
|
integration/
|
||||||
|
Editor/
|
||||||
|
unit/
|
||||||
|
integration/
|
||||||
|
```
|
||||||
|
|
||||||
|
三层必须明确分开:
|
||||||
|
|
||||||
|
- `Core`
|
||||||
|
- 共享 UI 基础层测试。
|
||||||
|
- 例如 tree state、layout、focus、input router、shortcut、scroll、text controller。
|
||||||
|
- `Runtime`
|
||||||
|
- 面向游戏运行时 UI 的测试。
|
||||||
|
- 例如 screen stack、layer blocking、runtime 输入路由、runtime-only widget。
|
||||||
|
- `Editor`
|
||||||
|
- 面向编辑器 UI 的测试。
|
||||||
|
- 例如 editor 输入宿主、editor shell 验证场景、editor-only widget。
|
||||||
|
|
||||||
|
禁止把 `Runtime` 和 `Editor` 混在同一个测试目标里。
|
||||||
|
|
||||||
|
## 3. Unit 规范
|
||||||
|
|
||||||
|
`unit` 测试要求:
|
||||||
|
|
||||||
|
- 直接面向底层能力。
|
||||||
|
- 不依赖人工观察。
|
||||||
|
- 默认进入自动化回归。
|
||||||
|
- 新增一块共享能力时,优先先补 `unit`。
|
||||||
|
|
||||||
|
`unit` 测试不负责:
|
||||||
|
|
||||||
|
- 人工手感检查。
|
||||||
|
- 布局观感检查。
|
||||||
|
- 完整交互场景展示。
|
||||||
|
|
||||||
|
## 4. Integration 规范
|
||||||
|
|
||||||
|
`integration` 测试要求:
|
||||||
|
|
||||||
|
- 必须产出可直接运行的 exe。
|
||||||
|
- 一个 exe 只验证一个聚焦场景。
|
||||||
|
- 每次只暴露当前批次需要检查的操作区域,不做大杂烩面板。
|
||||||
|
- 界面中的操作提示默认使用中文,必要时可混用 `hover`、`focus`、`active`、`capture` 等术语。
|
||||||
|
|
||||||
|
`integration` 测试不负责:
|
||||||
|
|
||||||
|
- 模拟完整 editor 产品外壳。
|
||||||
|
- 把多个无关能力塞进同一个窗口。
|
||||||
|
- 代替底层 `unit` 回归。
|
||||||
|
|
||||||
|
## 5. Scenario 目录规范
|
||||||
|
|
||||||
|
### 5.1 Editor
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/UI/Editor/integration/
|
||||||
|
shared/
|
||||||
|
src/
|
||||||
|
<category>/
|
||||||
|
<scenario>/
|
||||||
|
CMakeLists.txt
|
||||||
|
main.cpp
|
||||||
|
View.xcui
|
||||||
|
captures/
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- `shared/` 只放宿主、渲染、截图、scenario registry 等共用基础设施。
|
||||||
|
- `<category>` 表示能力类别,例如 `input`。
|
||||||
|
- `<scenario>` 是最小验证单元。
|
||||||
|
- 一个 `<scenario>` 对应一个 exe。
|
||||||
|
|
||||||
|
### 5.2 Runtime
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/UI/Runtime/integration/
|
||||||
|
<category>/
|
||||||
|
<scenario>/
|
||||||
|
CMakeLists.txt
|
||||||
|
main.cpp
|
||||||
|
View.xcui
|
||||||
|
captures/
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime 的集成测试结构与 Editor 保持同一规范,但宿主职责必须与 Editor 分离。
|
||||||
|
|
||||||
|
## 6. 当前已有 Editor 场景
|
||||||
|
|
||||||
|
- `editor.input.keyboard_focus`
|
||||||
|
- `editor.input.pointer_states`
|
||||||
|
- `editor.input.shortcut_scope`
|
||||||
|
- `editor.layout.splitter_resize`
|
||||||
|
|
||||||
|
这些场景只用于验证 XCUI 模块能力,不代表开始复刻完整 editor 面板。
|
||||||
|
|
||||||
|
## 7. 截图规范
|
||||||
|
|
||||||
|
Editor 集成宿主支持:
|
||||||
|
|
||||||
|
- `F12` 手动截图。
|
||||||
|
- 截图只允许截当前 exe 自己的渲染结果。
|
||||||
|
- 截图输出到当前 scenario 自己的 `captures/` 目录。
|
||||||
|
|
||||||
|
输出格式:
|
||||||
|
|
||||||
|
- `captures/latest.png`
|
||||||
|
- `captures/history/<timestamp>_<index>_<reason>.png`
|
||||||
|
|
||||||
|
原则:
|
||||||
|
|
||||||
|
- 不做持续高频自动截图轰炸。
|
||||||
|
- 只在人工检查、问题复现、调试定位时触发截图。
|
||||||
|
|
||||||
|
## 8. 开发顺序
|
||||||
|
|
||||||
|
XCUI 必须坚持自底向上的建设顺序:
|
||||||
|
|
||||||
|
1. 先补共享底层能力。
|
||||||
|
2. 先补对应 `unit`。
|
||||||
|
3. 再补一个聚焦的 `integration` exe。
|
||||||
|
4. 人工检查通过后再继续向上推进。
|
||||||
|
|
||||||
|
禁止事项:
|
||||||
|
|
||||||
|
- 先堆 editor 具体面板,再回头补底层。
|
||||||
|
- 把 `new_editor` 当作 XCUI 主测试入口。
|
||||||
|
- 把一个验证 exe 做成综合试验场。
|
||||||
|
- 为了赶进度写跨层耦合的临时代码。
|
||||||
|
|
||||||
|
## 9. 当前入口约定
|
||||||
|
|
||||||
|
当前 XCUI 的正式验证入口是 `tests/UI`。
|
||||||
|
|
||||||
|
`new_editor` 不是后续 XCUI 测试体系的主入口,也不应继续承载新的测试场景扩展。
|
||||||
@@ -28,10 +28,6 @@ add_subdirectory(Containers)
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
add_subdirectory(Math)
|
add_subdirectory(Math)
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Core/UI Tests
|
|
||||||
# ============================================================
|
|
||||||
add_subdirectory(UI)
|
|
||||||
add_subdirectory(UIStyle)
|
add_subdirectory(UIStyle)
|
||||||
|
|
||||||
# Exclude all static runtime libraries to avoid conflicts
|
# Exclude all static runtime libraries to avoid conflicts
|
||||||
|
|||||||
Reference in New Issue
Block a user