@@ -0,0 +1,825 @@
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <XCEditor/Core/UIEditorCommandDispatcher.h>
# include <XCEditor/Core/UIEditorMenuModel.h>
# include <XCEditor/Core/UIEditorShellInteraction.h>
# include <XCEditor/Core/UIEditorWorkspaceController.h>
# include <XCEditor/Core/UIEditorWorkspaceModel.h>
# include "Host/AutoScreenshot.h"
# include "Host/NativeRenderer.h"
# include <XCEngine/Input/InputTypes.h>
# include <XCEngine/UI/DrawData.h>
# include <windows.h>
# include <windowsx.h>
# include <algorithm>
# include <filesystem>
# include <sstream>
# include <string>
# include <string_view>
# include <utility>
# include <vector>
# ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
# define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
# endif
namespace {
using XCEngine : : Input : : KeyCode ;
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 : : Editor : : AppendUIEditorShellInteraction ;
using XCEngine : : UI : : Editor : : BuildDefaultUIEditorWorkspaceController ;
using XCEngine : : UI : : Editor : : BuildUIEditorResolvedMenuModel ;
using XCEngine : : UI : : Editor : : BuildUIEditorWorkspacePanel ;
using XCEngine : : UI : : Editor : : BuildUIEditorWorkspaceSplit ;
using XCEngine : : UI : : Editor : : BuildUIEditorWorkspaceTabStack ;
using XCEngine : : UI : : Editor : : CollectUIEditorWorkspaceVisiblePanels ;
using XCEngine : : UI : : Editor : : GetUIEditorCommandDispatchStatusName ;
using XCEngine : : UI : : Editor : : UpdateUIEditorShellInteraction ;
using XCEngine : : UI : : Editor : : UIEditorCommandDispatchResult ;
using XCEngine : : UI : : Editor : : UIEditorCommandDispatcher ;
using XCEngine : : UI : : Editor : : UIEditorCommandPanelSource ;
using XCEngine : : UI : : Editor : : UIEditorCommandRegistry ;
using XCEngine : : UI : : Editor : : UIEditorMenuCheckedStateSource ;
using XCEngine : : UI : : Editor : : UIEditorMenuDescriptor ;
using XCEngine : : UI : : Editor : : UIEditorMenuItemDescriptor ;
using XCEngine : : UI : : Editor : : UIEditorMenuItemKind ;
using XCEngine : : UI : : Editor : : UIEditorMenuModel ;
using XCEngine : : UI : : Editor : : UIEditorPanelPresentationKind ;
using XCEngine : : UI : : Editor : : UIEditorPanelRegistry ;
using XCEngine : : UI : : Editor : : UIEditorShellInteractionFrame ;
using XCEngine : : UI : : Editor : : UIEditorShellInteractionModel ;
using XCEngine : : UI : : Editor : : UIEditorShellInteractionResult ;
using XCEngine : : UI : : Editor : : UIEditorShellInteractionState ;
using XCEngine : : UI : : Editor : : UIEditorWorkspaceCommandKind ;
using XCEngine : : UI : : Editor : : UIEditorWorkspaceController ;
using XCEngine : : UI : : Editor : : UIEditorWorkspaceModel ;
using XCEngine : : UI : : Editor : : UIEditorWorkspacePanelPresentationModel ;
using XCEngine : : UI : : Editor : : UIEditorWorkspaceSession ;
using XCEngine : : UI : : Editor : : UIEditorWorkspaceSplitAxis ;
using XCEngine : : UI : : Editor : : Widgets : : UIEditorStatusBarSegment ;
using XCEngine : : UI : : Editor : : Widgets : : UIEditorStatusBarSlot ;
using XCEngine : : UI : : Widgets : : UIPopupDismissReason ;
using XCEngine : : UI : : Editor : : Host : : AutoScreenshotController ;
using XCEngine : : UI : : Editor : : Host : : NativeRenderer ;
constexpr const wchar_t * kWindowClassName = L " XCUIEditorShellInteractionValidation " ;
constexpr const wchar_t * kWindowTitle = L " XCUI Editor | Shell Interaction " ;
constexpr UIColor kWindowBg ( 0.10f , 0.10f , 0.10f , 1.0f ) ;
constexpr UIColor kCardBg ( 0.17f , 0.17f , 0.17f , 1.0f ) ;
constexpr UIColor kCardBorder ( 0.28f , 0.28f , 0.28f , 1.0f ) ;
constexpr UIColor kTextPrimary ( 0.94f , 0.94f , 0.94f , 1.0f ) ;
constexpr UIColor kTextMuted ( 0.72f , 0.72f , 0.72f , 1.0f ) ;
constexpr UIColor kTextWeak ( 0.56f , 0.56f , 0.56f , 1.0f ) ;
constexpr UIColor kButtonBg ( 0.24f , 0.24f , 0.24f , 1.0f ) ;
constexpr UIColor kButtonHover ( 0.32f , 0.32f , 0.32f , 1.0f ) ;
constexpr UIColor kButtonBorder ( 0.46f , 0.46f , 0.46f , 1.0f ) ;
constexpr UIColor kSuccess ( 0.46f , 0.72f , 0.50f , 1.0f ) ;
constexpr UIColor kWarning ( 0.82f , 0.67f , 0.35f , 1.0f ) ;
constexpr UIColor kDanger ( 0.78f , 0.35f , 0.35f , 1.0f ) ;
enum class ActionId : unsigned char {
Reset = 0 ,
Capture
} ;
struct ButtonState {
ActionId action = ActionId : : Reset ;
std : : string label = { } ;
UIRect rect = { } ;
bool hovered = false ;
} ;
std : : filesystem : : path ResolveRepoRootPath ( ) {
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 ( ) ;
}
bool ContainsPoint ( const UIRect & rect , float x , float y ) {
return x > = rect . x & &
x < = rect . x + rect . width & &
y > = rect . y & &
y < = rect . y + rect . height ;
}
bool HasMeaningfulInteractionResult ( const UIEditorShellInteractionResult & result ) {
return result . consumed | |
result . commandTriggered | |
result . menuMutation . changed | |
! result . menuId . empty ( ) | |
! result . popupId . empty ( ) | |
! result . itemId . empty ( ) | |
! result . commandId . empty ( ) ;
}
std : : string FormatBool ( bool value ) {
return value ? " true " : " false " ;
}
std : : string FormatDismissReason ( UIPopupDismissReason reason ) {
switch ( reason ) {
case UIPopupDismissReason : : None :
return " None " ;
case UIPopupDismissReason : : Programmatic :
return " Programmatic " ;
case UIPopupDismissReason : : EscapeKey :
return " EscapeKey " ;
case UIPopupDismissReason : : PointerOutside :
return " PointerOutside " ;
case UIPopupDismissReason : : FocusLoss :
return " FocusLoss " ;
}
return " Unknown " ;
}
std : : string JoinVisiblePanelIds (
const UIEditorWorkspaceModel & workspace ,
const UIEditorWorkspaceSession & session ) {
const auto visiblePanels = CollectUIEditorWorkspaceVisiblePanels ( workspace , session ) ;
if ( visiblePanels . empty ( ) ) {
return " (none) " ;
}
std : : ostringstream stream = { } ;
for ( std : : size_t index = 0 ; index < visiblePanels . size ( ) ; + + index ) {
if ( index > 0u ) {
stream < < " , " ;
}
stream < < visiblePanels [ index ] . panelId ;
}
return stream . str ( ) ;
}
std : : string JoinPopupChainIds ( const UIEditorShellInteractionState & state ) {
const auto & popupStates = state . menuSession . GetPopupStates ( ) ;
if ( popupStates . empty ( ) ) {
return " (none) " ;
}
std : : ostringstream stream = { } ;
for ( std : : size_t index = 0 ; index < popupStates . size ( ) ; + + index ) {
if ( index > 0u ) {
stream < < " -> " ;
}
stream < < popupStates [ index ] . popupId ;
}
return stream . str ( ) ;
}
std : : string JoinSubmenuPathIds ( const UIEditorShellInteractionState & state ) {
const auto & itemIds = state . menuSession . GetOpenSubmenuItemIds ( ) ;
if ( itemIds . empty ( ) ) {
return " (none) " ;
}
std : : ostringstream stream = { } ;
for ( std : : size_t index = 0 ; index < itemIds . size ( ) ; + + index ) {
if ( index > 0u ) {
stream < < " -> " ;
}
stream < < itemIds [ index ] ;
}
return stream . str ( ) ;
}
void DrawCard (
UIDrawList & drawList ,
const UIRect & rect ,
std : : string_view title ,
std : : string_view subtitle = { } ) {
drawList . AddFilledRect ( rect , kCardBg , 10.0f ) ;
drawList . AddRectOutline ( rect , kCardBorder , 1.0f , 10.0f ) ;
drawList . AddText ( UIPoint ( rect . x + 16.0f , rect . y + 14.0f ) , std : : string ( title ) , kTextPrimary , 17.0f ) ;
if ( ! subtitle . empty ( ) ) {
drawList . AddText ( UIPoint ( rect . x + 16.0f , rect . y + 38.0f ) , std : : string ( subtitle ) , kTextMuted , 12.0f ) ;
}
}
void DrawButton ( UIDrawList & drawList , const ButtonState & button ) {
drawList . AddFilledRect ( button . rect , button . hovered ? kButtonHover : kButtonBg , 8.0f ) ;
drawList . AddRectOutline ( button . rect , kButtonBorder , 1.0f , 8.0f ) ;
drawList . AddText ( UIPoint ( button . rect . x + 14.0f , button . rect . y + 10.0f ) , button . label , kTextPrimary , 12.0f ) ;
}
std : : int32_t MapVirtualKeyToUIKeyCode ( WPARAM wParam ) {
return wParam = = VK_ESCAPE
? static_cast < std : : int32_t > ( KeyCode : : Escape )
: static_cast < std : : int32_t > ( KeyCode : : None ) ;
}
UIEditorPanelRegistry BuildPanelRegistry ( ) {
UIEditorPanelRegistry registry = { } ;
registry . panels = {
{ " hierarchy " , " Hierarchy " , UIEditorPanelPresentationKind : : Placeholder , true , true , false } ,
{ " scene " , " Scene " , UIEditorPanelPresentationKind : : ViewportShell , false , true , false } ,
{ " document " , " Document " , UIEditorPanelPresentationKind : : Placeholder , true , true , true } ,
{ " inspector " , " Inspector " , UIEditorPanelPresentationKind : : Placeholder , true , true , true }
} ;
return registry ;
}
UIEditorWorkspaceModel BuildWorkspace ( ) {
UIEditorWorkspaceModel workspace = { } ;
workspace . root = BuildUIEditorWorkspaceSplit (
" root " ,
UIEditorWorkspaceSplitAxis : : Horizontal ,
0.24f ,
BuildUIEditorWorkspacePanel ( " hierarchy-node " , " hierarchy " , " Hierarchy " , true ) ,
BuildUIEditorWorkspaceSplit (
" main " ,
UIEditorWorkspaceSplitAxis : : Horizontal ,
0.72f ,
BuildUIEditorWorkspaceTabStack (
" center-tabs " ,
{
BuildUIEditorWorkspacePanel ( " scene-node " , " scene " , " Scene " ) ,
BuildUIEditorWorkspacePanel ( " document-node " , " document " , " Document " , true )
} ,
0u ) ,
BuildUIEditorWorkspacePanel ( " inspector-node " , " inspector " , " Inspector " , true ) ) ) ;
workspace . activePanelId = " scene " ;
return workspace ;
}
UIEditorCommandRegistry BuildCommandRegistry ( ) {
UIEditorCommandRegistry registry = { } ;
registry . commands = {
{
" workspace.show_inspector " ,
" Show Inspector " ,
{ UIEditorWorkspaceCommandKind : : ShowPanel , UIEditorCommandPanelSource : : FixedPanelId , " inspector " }
} ,
{
" workspace.hide_inspector " ,
" Hide Inspector " ,
{ UIEditorWorkspaceCommandKind : : HidePanel , UIEditorCommandPanelSource : : FixedPanelId , " inspector " }
} ,
{
" workspace.activate_scene " ,
" Activate Scene " ,
{ UIEditorWorkspaceCommandKind : : ActivatePanel , UIEditorCommandPanelSource : : FixedPanelId , " scene " }
} ,
{
" workspace.activate_document " ,
" Activate Document " ,
{ UIEditorWorkspaceCommandKind : : ActivatePanel , UIEditorCommandPanelSource : : FixedPanelId , " document " }
} ,
{
" workspace.reset " ,
" Reset Workspace " ,
{ UIEditorWorkspaceCommandKind : : ResetWorkspace , UIEditorCommandPanelSource : : None , { } }
}
} ;
return registry ;
}
UIEditorMenuModel BuildMenuModel ( ) {
UIEditorMenuItemDescriptor showInspector = { } ;
showInspector . kind = UIEditorMenuItemKind : : Command ;
showInspector . itemId = " file-show-inspector " ;
showInspector . label = " Show Inspector " ;
showInspector . commandId = " workspace.show_inspector " ;
showInspector . checkedState = { UIEditorMenuCheckedStateSource : : PanelVisible , " inspector " } ;
UIEditorMenuItemDescriptor resetWorkspace = { } ;
resetWorkspace . kind = UIEditorMenuItemKind : : Command ;
resetWorkspace . itemId = " file-reset " ;
resetWorkspace . label = " Reset Workspace " ;
resetWorkspace . commandId = " workspace.reset " ;
UIEditorMenuItemDescriptor workspaceTools = { } ;
workspaceTools . kind = UIEditorMenuItemKind : : Submenu ;
workspaceTools . itemId = " file-workspace-tools " ;
workspaceTools . label = " Workspace Tools " ;
workspaceTools . children = { showInspector , resetWorkspace } ;
UIEditorMenuItemDescriptor activateScene = { } ;
activateScene . kind = UIEditorMenuItemKind : : Command ;
activateScene . itemId = " window-activate-scene " ;
activateScene . label = " Activate Scene " ;
activateScene . commandId = " workspace.activate_scene " ;
activateScene . checkedState = { UIEditorMenuCheckedStateSource : : PanelActive , " scene " } ;
UIEditorMenuItemDescriptor activateDocument = { } ;
activateDocument . kind = UIEditorMenuItemKind : : Command ;
activateDocument . itemId = " window-activate-document " ;
activateDocument . label = " Activate Document " ;
activateDocument . commandId = " workspace.activate_document " ;
activateDocument . checkedState = { UIEditorMenuCheckedStateSource : : PanelActive , " document " } ;
UIEditorMenuItemDescriptor hideInspector = { } ;
hideInspector . kind = UIEditorMenuItemKind : : Command ;
hideInspector . itemId = " window-hide-inspector " ;
hideInspector . label = " Hide Inspector " ;
hideInspector . commandId = " workspace.hide_inspector " ;
UIEditorMenuDescriptor fileMenu = { } ;
fileMenu . menuId = " file " ;
fileMenu . label = " File " ;
fileMenu . items = { workspaceTools } ;
UIEditorMenuDescriptor windowMenu = { } ;
windowMenu . menuId = " window " ;
windowMenu . label = " Window " ;
windowMenu . items = { activateScene , activateDocument , hideInspector } ;
UIEditorMenuModel model = { } ;
model . menus = { fileMenu , windowMenu } ;
return model ;
}
class ScenarioApp {
public :
int Run ( HINSTANCE hInstance , int nCmdShow ) ;
private :
static LRESULT CALLBACK WndProc ( HWND hwnd , UINT message , WPARAM wParam , LPARAM lParam ) ;
bool Initialize ( HINSTANCE hInstance , int nCmdShow ) ;
void Shutdown ( ) ;
void ResetScenario ( ) ;
void UpdateLayout ( ) ;
void HandleMouseMove ( float x , float y ) ;
void HandleLeftButtonDown ( float x , float y ) ;
void ExecuteAction ( ActionId action ) ;
UIEditorShellInteractionModel BuildInteractionModel ( ) const ;
void SetInteractionResult ( const UIEditorShellInteractionResult & result ) ;
void SetDispatchResult ( const UIEditorCommandDispatchResult & result ) ;
void RenderFrame ( ) ;
HWND m_hwnd = nullptr ;
ATOM m_windowClassAtom = 0 ;
NativeRenderer m_renderer = { } ;
AutoScreenshotController m_autoScreenshot = { } ;
std : : filesystem : : path m_captureRoot = { } ;
UIEditorWorkspaceController m_controller = { } ;
UIEditorCommandDispatcher m_commandDispatcher = { } ;
UIEditorMenuModel m_menuModel = { } ;
UIEditorShellInteractionState m_interactionState = { } ;
UIEditorShellInteractionModel m_cachedModel = { } ;
UIEditorShellInteractionFrame m_cachedFrame = { } ;
std : : vector < UIInputEvent > m_pendingInputEvents = { } ;
std : : vector < ButtonState > m_buttons = { } ;
UIRect m_introRect = { } ;
UIRect m_controlsRect = { } ;
UIRect m_stateRect = { } ;
UIRect m_previewRect = { } ;
UIRect m_shellRect = { } ;
bool m_trackingMouseLeave = false ;
std : : string m_lastStatus = { } ;
std : : string m_lastMessage = { } ;
UIColor m_lastColor = kTextMuted ;
} ;
int ScenarioApp : : 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 ) ;
}
LRESULT CALLBACK ScenarioApp : : WndProc ( HWND hwnd , UINT message , WPARAM wParam , LPARAM lParam ) {
if ( message = = WM_NCCREATE ) {
const auto * createStruct = reinterpret_cast < CREATESTRUCTW * > ( lParam ) ;
auto * app = reinterpret_cast < ScenarioApp * > ( createStruct - > lpCreateParams ) ;
SetWindowLongPtrW ( hwnd , GWLP_USERDATA , reinterpret_cast < LONG_PTR > ( app ) ) ;
return TRUE ;
}
auto * app = reinterpret_cast < ScenarioApp * > ( GetWindowLongPtrW ( hwnd , GWLP_USERDATA ) ) ;
switch ( message ) {
case WM_SIZE :
if ( app ! = nullptr & & wParam ! = SIZE_MINIMIZED ) {
app - > m_renderer . Resize ( static_cast < UINT > ( LOWORD ( lParam ) ) , static_cast < UINT > ( HIWORD ( lParam ) ) ) ;
}
return 0 ;
case WM_PAINT :
if ( app ! = nullptr ) {
PAINTSTRUCT paintStruct = { } ;
BeginPaint ( hwnd , & paintStruct ) ;
app - > RenderFrame ( ) ;
EndPaint ( hwnd , & paintStruct ) ;
return 0 ;
}
break ;
case WM_MOUSEMOVE :
if ( app ! = nullptr ) {
if ( ! app - > m_trackingMouseLeave ) {
TRACKMOUSEEVENT trackMouseEvent = { } ;
trackMouseEvent . cbSize = sizeof ( trackMouseEvent ) ;
trackMouseEvent . dwFlags = TME_LEAVE ;
trackMouseEvent . hwndTrack = hwnd ;
if ( TrackMouseEvent ( & trackMouseEvent ) ) {
app - > m_trackingMouseLeave = true ;
}
}
app - > HandleMouseMove ( static_cast < float > ( GET_X_LPARAM ( lParam ) ) , static_cast < float > ( GET_Y_LPARAM ( lParam ) ) ) ;
return 0 ;
}
break ;
case WM_MOUSELEAVE :
if ( app ! = nullptr ) {
app - > m_trackingMouseLeave = false ;
UIInputEvent event = { } ;
event . type = UIInputEventType : : PointerLeave ;
app - > m_pendingInputEvents . push_back ( event ) ;
return 0 ;
}
break ;
case WM_LBUTTONDOWN :
if ( app ! = nullptr ) {
SetFocus ( hwnd ) ;
app - > HandleLeftButtonDown ( static_cast < float > ( GET_X_LPARAM ( lParam ) ) , static_cast < float > ( GET_Y_LPARAM ( lParam ) ) ) ;
return 0 ;
}
break ;
case WM_SETFOCUS :
if ( app ! = nullptr ) {
UIInputEvent event = { } ;
event . type = UIInputEventType : : FocusGained ;
app - > m_pendingInputEvents . push_back ( event ) ;
return 0 ;
}
break ;
case WM_KILLFOCUS :
if ( app ! = nullptr ) {
UIInputEvent event = { } ;
event . type = UIInputEventType : : FocusLost ;
app - > m_pendingInputEvents . push_back ( event ) ;
return 0 ;
}
break ;
case WM_KEYDOWN :
case WM_SYSKEYDOWN :
if ( app ! = nullptr ) {
if ( wParam = = VK_F12 ) {
app - > m_autoScreenshot . RequestCapture ( " manual_f12 " ) ;
} else {
const std : : int32_t keyCode = MapVirtualKeyToUIKeyCode ( wParam ) ;
if ( keyCode ! = static_cast < std : : int32_t > ( KeyCode : : None ) ) {
UIInputEvent event = { } ;
event . type = UIInputEventType : : KeyDown ;
event . keyCode = keyCode ;
app - > m_pendingInputEvents . push_back ( event ) ;
}
}
return 0 ;
}
break ;
case WM_ERASEBKGND :
return 1 ;
case WM_DESTROY :
PostQuitMessage ( 0 ) ;
return 0 ;
default :
break ;
}
return DefWindowProcW ( hwnd , message , wParam , lParam ) ;
}
bool ScenarioApp : : Initialize ( HINSTANCE hInstance , int nCmdShow ) {
m_captureRoot =
ResolveRepoRootPath ( ) / " tests/UI/Editor/integration/shell/editor_shell_interaction/captures " ;
m_autoScreenshot . Initialize ( m_captureRoot ) ;
WNDCLASSEXW windowClass = { } ;
windowClass . cbSize = sizeof ( windowClass ) ;
windowClass . style = CS_HREDRAW | CS_VREDRAW ;
windowClass . lpfnWndProc = & ScenarioApp : : 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 ,
1540 ,
940 ,
nullptr ,
nullptr ,
hInstance ,
this ) ;
if ( m_hwnd = = nullptr ) {
return false ;
}
ShowWindow ( m_hwnd , nCmdShow ) ;
if ( ! m_renderer . Initialize ( m_hwnd ) ) {
return false ;
}
ResetScenario ( ) ;
return true ;
}
void ScenarioApp : : Shutdown ( ) {
m_autoScreenshot . Shutdown ( ) ;
m_renderer . Shutdown ( ) ;
if ( m_hwnd ! = nullptr & & IsWindow ( m_hwnd ) ) {
DestroyWindow ( m_hwnd ) ;
}
if ( m_windowClassAtom ! = 0 ) {
UnregisterClassW ( kWindowClassName , GetModuleHandleW ( nullptr ) ) ;
}
}
void ScenarioApp : : ResetScenario ( ) {
m_controller = BuildDefaultUIEditorWorkspaceController ( BuildPanelRegistry ( ) , BuildWorkspace ( ) ) ;
m_commandDispatcher = UIEditorCommandDispatcher ( BuildCommandRegistry ( ) ) ;
m_menuModel = BuildMenuModel ( ) ;
m_interactionState = { } ;
m_cachedModel = { } ;
m_cachedFrame = { } ;
m_pendingInputEvents . clear ( ) ;
m_lastStatus = " Ready " ;
m_lastMessage = " 等待交互。这里只验证 Editor 根壳统一交互 contract, 不接旧 editor 业务。 " ;
m_lastColor = kWarning ;
}
void ScenarioApp : : UpdateLayout ( ) {
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 ) ) ;
constexpr float padding = 20.0f ;
constexpr float leftWidth = 430.0f ;
m_introRect = UIRect ( padding , padding , leftWidth , 214.0f ) ;
m_controlsRect = UIRect ( padding , 250.0f , leftWidth , 118.0f ) ;
m_stateRect = UIRect ( padding , 384.0f , leftWidth , height - 404.0f ) ;
m_previewRect = UIRect ( leftWidth + padding * 2.0f , padding , width - leftWidth - padding * 3.0f , height - padding * 2.0f ) ;
m_shellRect = UIRect ( m_previewRect . x + 18.0f , m_previewRect . y + 54.0f , m_previewRect . width - 36.0f , m_previewRect . height - 72.0f ) ;
const float buttonWidth = ( m_controlsRect . width - 32.0f - 12.0f ) * 0.5f ;
const float left = m_controlsRect . x + 16.0f ;
const float top = m_controlsRect . y + 62.0f ;
m_buttons = {
{ ActionId : : Reset , " 重置 " , UIRect ( left , top , buttonWidth , 36.0f ) , false } ,
{ ActionId : : Capture , " 截图(F12) " , UIRect ( left + buttonWidth + 12.0f , top , buttonWidth , 36.0f ) , false }
} ;
}
void ScenarioApp : : HandleMouseMove ( float x , float y ) {
UpdateLayout ( ) ;
for ( ButtonState & button : m_buttons ) {
button . hovered = ContainsPoint ( button . rect , x , y ) ;
}
UIInputEvent event = { } ;
event . type = UIInputEventType : : PointerMove ;
event . position = UIPoint ( x , y ) ;
m_pendingInputEvents . push_back ( event ) ;
}
void ScenarioApp : : HandleLeftButtonDown ( float x , float y ) {
UpdateLayout ( ) ;
for ( const ButtonState & button : m_buttons ) {
if ( ContainsPoint ( button . rect , x , y ) ) {
ExecuteAction ( button . action ) ;
return ;
}
}
UIInputEvent event = { } ;
event . type = UIInputEventType : : PointerButtonDown ;
event . position = UIPoint ( x , y ) ;
event . pointerButton = UIPointerButton : : Left ;
m_pendingInputEvents . push_back ( event ) ;
}
void ScenarioApp : : ExecuteAction ( ActionId action ) {
if ( action = = ActionId : : Reset ) {
ResetScenario ( ) ;
m_lastStatus = " Ready " ;
m_lastMessage = " 场景状态已重置。请重新检查 root open / child popup / dismiss 行为。 " ;
m_lastColor = kWarning ;
return ;
}
m_autoScreenshot . RequestCapture ( " manual_button " ) ;
m_lastStatus = " Ready " ;
m_lastMessage = " 截图已排队,输出到 tests/UI/Editor/integration/shell/editor_shell_interaction/captures/。 " ;
m_lastColor = kWarning ;
}
UIEditorShellInteractionModel ScenarioApp : : BuildInteractionModel ( ) const {
UIEditorShellInteractionModel model = { } ;
model . resolvedMenuModel = BuildUIEditorResolvedMenuModel (
m_menuModel ,
m_commandDispatcher ,
m_controller ,
nullptr ) ;
model . statusSegments = {
UIEditorStatusBarSegment {
" mode " ,
" Shell Contract " ,
UIEditorStatusBarSlot : : Leading ,
{ } ,
true ,
true ,
122.0f
} ,
UIEditorStatusBarSegment {
" active " ,
m_controller . GetWorkspace ( ) . activePanelId . empty ( )
? std : : string ( " (none) " )
: m_controller . GetWorkspace ( ) . activePanelId ,
UIEditorStatusBarSlot : : Trailing ,
{ } ,
true ,
true ,
120.0f
}
} ;
UIEditorWorkspacePanelPresentationModel presentation = { } ;
presentation . panelId = " scene " ;
presentation . kind = UIEditorPanelPresentationKind : : ViewportShell ;
presentation . viewportShellModel . spec . chrome . title = " Scene " ;
presentation . viewportShellModel . spec . chrome . subtitle = " Editor Shell Interaction " ;
presentation . viewportShellModel . spec . chrome . showTopBar = true ;
presentation . viewportShellModel . spec . chrome . showBottomBar = true ;
presentation . viewportShellModel . frame . hasTexture = false ;
presentation . viewportShellModel . frame . statusText =
" 这里只验证 Editor 根壳交互,不接旧 editor 业务面板。 " ;
model . workspacePresentations = { presentation } ;
return model ;
}
void ScenarioApp : : SetInteractionResult ( const UIEditorShellInteractionResult & result ) {
if ( ! HasMeaningfulInteractionResult ( result ) ) {
return ;
}
if ( result . commandTriggered ) {
const UIEditorCommandDispatchResult dispatchResult =
m_commandDispatcher . Dispatch ( result . commandId , m_controller ) ;
SetDispatchResult ( dispatchResult ) ;
return ;
}
if ( result . menuMutation . changed ) {
if ( ! result . itemId . empty ( ) & & ! result . menuMutation . openedPopupId . empty ( ) ) {
m_lastStatus = " Changed " ;
m_lastMessage =
" 已展开子菜单 ` " + result . itemId + " `。请检查 child popup 是否在 hover 时直接弹出。 " ;
m_lastColor = kSuccess ;
} else if ( ! result . menuId . empty ( ) & & ! result . menuMutation . openedPopupId . empty ( ) ) {
m_lastStatus = " Changed " ;
m_lastMessage =
" 当前根菜单为 ` " + result . menuId + " `。请检查 root popup 是否在打开态下可直接切换。 " ;
m_lastColor = kSuccess ;
} else {
m_lastStatus = " Dismissed " ;
m_lastMessage =
" 菜单链已收起。DismissReason = " +
FormatDismissReason ( result . menuMutation . dismissReason ) +
" 。请检查 outside pointer down / Esc / focus loss 的收束是否正确。 " ;
m_lastColor = kWarning ;
}
return ;
}
if ( result . consumed ) {
m_lastStatus = " NoOp " ;
m_lastMessage = " 这次输入被根壳交互层消费,但没有触发额外状态变化。 " ;
m_lastColor = kWarning ;
}
}
void ScenarioApp : : SetDispatchResult ( const UIEditorCommandDispatchResult & result ) {
m_lastStatus = std : : string ( GetUIEditorCommandDispatchStatusName ( result . status ) ) ;
m_lastMessage = result . message . empty ( ) ? std : : string ( " 命令派发完成。 " ) : result . message ;
m_lastColor = m_lastStatus = = " Dispatched " ? kSuccess : kDanger ;
}
void ScenarioApp : : RenderFrame ( ) {
UpdateLayout ( ) ;
m_cachedModel = BuildInteractionModel ( ) ;
m_cachedFrame = UpdateUIEditorShellInteraction (
m_interactionState ,
m_shellRect ,
m_controller . GetPanelRegistry ( ) ,
m_controller . GetWorkspace ( ) ,
m_controller . GetSession ( ) ,
m_cachedModel ,
m_pendingInputEvents ) ;
m_pendingInputEvents . clear ( ) ;
SetInteractionResult ( m_cachedFrame . result ) ;
if ( m_cachedFrame . result . commandTriggered ) {
m_cachedModel = BuildInteractionModel ( ) ;
m_cachedFrame = UpdateUIEditorShellInteraction (
m_interactionState ,
m_shellRect ,
m_controller . GetPanelRegistry ( ) ,
m_controller . GetWorkspace ( ) ,
m_controller . GetSession ( ) ,
m_cachedModel ,
{ } ) ;
}
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 ) ) ;
UIDrawData drawData = { } ;
UIDrawList & drawList = drawData . EmplaceDrawList ( " EditorShellInteraction " ) ;
drawList . AddFilledRect ( UIRect ( 0.0f , 0.0f , width , height ) , kWindowBg ) ;
DrawCard ( drawList , m_introRect , " 这个测试验证什么功能 " , " 只验证 Editor 根壳统一交互 contract, 不做业务面板。 " ) ;
drawList . AddText ( UIPoint ( m_introRect . x + 16.0f , m_introRect . y + 70.0f ) , " 1. 验证 MenuBar 的 root open / root switch 行为是否统一稳定。 " , kTextPrimary , 12.0f ) ;
drawList . AddText ( UIPoint ( m_introRect . x + 16.0f , m_introRect . y + 92.0f ) , " 2. 验证 hover 子菜单时, child popup 是否直接展开,不需要额外点击。 " , kTextPrimary , 12.0f ) ;
drawList . AddText ( UIPoint ( m_introRect . x + 16.0f , m_introRect . y + 114.0f ) , " 3. 验证 outside pointer down / Esc / focus loss 是否能正确收起 popup chain。 " , kTextPrimary , 12.0f ) ;
drawList . AddText ( UIPoint ( m_introRect . x + 16.0f , m_introRect . y + 136.0f ) , " 4. 验证预览区是真实 root shell: MenuBar + Workspace + StatusBar + popup overlay。 " , kTextPrimary , 12.0f ) ;
drawList . AddText ( UIPoint ( m_introRect . x + 16.0f , m_introRect . y + 158.0f ) , " 5. 验证 command 只通过最小 dispatch hook 回传,不接旧 editor 业务。 " , kTextPrimary , 12.0f ) ;
drawList . AddText ( UIPoint ( m_introRect . x + 16.0f , m_introRect . y + 182.0f ) , " 建议操作:点击 File, hover `Workspace Tools`,再按 Esc 或点预览区外空白处。 " , kTextWeak , 11.0f ) ;
DrawCard ( drawList , m_controlsRect , " 操作 " , " 只保留这个场景必要的控制。 " ) ;
for ( const ButtonState & button : m_buttons ) {
DrawButton ( drawList , button ) ;
}
DrawCard ( drawList , m_stateRect , " 状态 " , " 重点检查根壳 contract 当前状态。 " ) ;
float stateY = m_stateRect . y + 66.0f ;
auto addStateLine = [ & ] ( std : : string label , std : : string value , const UIColor & color , float fontSize = 12.0f ) {
drawList . AddText ( UIPoint ( m_stateRect . x + 16.0f , stateY ) , std : : move ( label ) + " : " + std : : move ( value ) , color , fontSize ) ;
stateY + = 20.0f ;
} ;
addStateLine ( " Open Root " , m_cachedFrame . openRootMenuId . empty ( ) ? " (none) " : m_cachedFrame . openRootMenuId , kTextPrimary ) ;
addStateLine ( " Popup Chain " , JoinPopupChainIds ( m_interactionState ) , kTextPrimary , 11.0f ) ;
addStateLine ( " Submenu Path " , JoinSubmenuPathIds ( m_interactionState ) , kTextPrimary , 11.0f ) ;
addStateLine ( " Focused " , FormatBool ( m_cachedFrame . focused ) , m_cachedFrame . focused ? kSuccess : kTextMuted ) ;
addStateLine ( " Result " , m_lastStatus , m_lastColor ) ;
drawList . AddText ( UIPoint ( m_stateRect . x + 16.0f , stateY + 4.0f ) , m_lastMessage , kTextMuted , 11.0f ) ;
stateY + = 34.0f ;
addStateLine ( " Visible Panels " , JoinVisiblePanelIds ( m_controller . GetWorkspace ( ) , m_controller . GetSession ( ) ) , kTextWeak , 11.0f ) ;
addStateLine (
" Capture " ,
m_autoScreenshot . HasPendingCapture ( )
? " 截图排队中... "
: ( m_autoScreenshot . GetLastCaptureSummary ( ) . empty ( )
? std : : string ( " F12 或 按钮 -> captures/ " )
: m_autoScreenshot . GetLastCaptureSummary ( ) ) ,
kTextWeak ,
11.0f ) ;
DrawCard ( drawList , m_previewRect , " Preview " , " 真实 UIEditorShellInteraction 预览,不接旧 editor 业务。 " ) ;
AppendUIEditorShellInteraction ( drawList , m_cachedFrame , m_cachedModel , m_interactionState ) ;
const bool framePresented = m_renderer . Render ( drawData ) ;
m_autoScreenshot . CaptureIfRequested (
m_renderer ,
drawData ,
static_cast < unsigned int > ( width ) ,
static_cast < unsigned int > ( height ) ,
framePresented ) ;
}
} // namespace
int WINAPI wWinMain ( HINSTANCE hInstance , HINSTANCE , LPWSTR , int nCmdShow ) {
return ScenarioApp ( ) . Run ( hInstance , nCmdShow ) ;
}