@@ -0,0 +1,858 @@
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <XCNewEditor/Editor/UIEditorWorkspaceController.h>
# include <XCNewEditor/Editor/UIEditorWorkspaceLayoutPersistence.h>
# include <XCNewEditor/Host/AutoScreenshot.h>
# include <XCNewEditor/Host/NativeRenderer.h>
# include <XCEngine/UI/DrawData.h>
# include <windows.h>
# include <windowsx.h>
# include <algorithm>
# include <filesystem>
# include <iomanip>
# 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 : : NewEditor : : BuildDefaultUIEditorWorkspaceController ;
using XCEngine : : NewEditor : : BuildDefaultUIEditorWorkspaceSession ;
using XCEngine : : NewEditor : : BuildUIEditorWorkspacePanel ;
using XCEngine : : NewEditor : : BuildUIEditorWorkspaceSplit ;
using XCEngine : : NewEditor : : BuildUIEditorWorkspaceTabStack ;
using XCEngine : : NewEditor : : CollectUIEditorWorkspaceVisiblePanels ;
using XCEngine : : NewEditor : : FindUIEditorPanelSessionState ;
using XCEngine : : NewEditor : : GetUIEditorWorkspaceCommandStatusName ;
using XCEngine : : NewEditor : : GetUIEditorWorkspaceLayoutOperationStatusName ;
using XCEngine : : NewEditor : : SerializeUIEditorWorkspaceLayoutSnapshot ;
using XCEngine : : NewEditor : : UIEditorPanelRegistry ;
using XCEngine : : NewEditor : : UIEditorWorkspaceCommand ;
using XCEngine : : NewEditor : : UIEditorWorkspaceCommandKind ;
using XCEngine : : NewEditor : : UIEditorWorkspaceCommandResult ;
using XCEngine : : NewEditor : : UIEditorWorkspaceCommandStatus ;
using XCEngine : : NewEditor : : UIEditorWorkspaceController ;
using XCEngine : : NewEditor : : UIEditorWorkspaceLayoutOperationResult ;
using XCEngine : : NewEditor : : UIEditorWorkspaceLayoutOperationStatus ;
using XCEngine : : NewEditor : : UIEditorWorkspaceLayoutSnapshot ;
using XCEngine : : NewEditor : : UIEditorWorkspaceModel ;
using XCEngine : : NewEditor : : UIEditorWorkspaceSession ;
using XCEngine : : NewEditor : : UIEditorWorkspaceSplitAxis ;
using XCEngine : : UI : : UIColor ;
using XCEngine : : UI : : UIDrawData ;
using XCEngine : : UI : : UIDrawList ;
using XCEngine : : UI : : UIPoint ;
using XCEngine : : UI : : UIRect ;
using XCEngine : : XCUI : : Host : : AutoScreenshotController ;
using XCEngine : : XCUI : : Host : : NativeRenderer ;
constexpr const wchar_t * kWindowClassName = L " XCUIEditorLayoutPersistenceValidation " ;
constexpr const wchar_t * kWindowTitle = L " XCUI Editor | Layout Persistence " ;
constexpr UIColor kWindowBg ( 0.14f , 0.14f , 0.14f , 1.0f ) ;
constexpr UIColor kCardBg ( 0.19f , 0.19f , 0.19f , 1.0f ) ;
constexpr UIColor kCardBorder ( 0.29f , 0.29f , 0.29f , 1.0f ) ;
constexpr UIColor kTextPrimary ( 0.94f , 0.94f , 0.94f , 1.0f ) ;
constexpr UIColor kTextMuted ( 0.70f , 0.70f , 0.70f , 1.0f ) ;
constexpr UIColor kAccent ( 0.82f , 0.82f , 0.82f , 1.0f ) ;
constexpr UIColor kSuccess ( 0.43f , 0.71f , 0.47f , 1.0f ) ;
constexpr UIColor kWarning ( 0.78f , 0.60f , 0.30f , 1.0f ) ;
constexpr UIColor kDanger ( 0.78f , 0.34f , 0.34f , 1.0f ) ;
constexpr UIColor kButtonEnabled ( 0.31f , 0.31f , 0.31f , 1.0f ) ;
constexpr UIColor kButtonDisabled ( 0.24f , 0.24f , 0.24f , 1.0f ) ;
constexpr UIColor kButtonBorder ( 0.42f , 0.42f , 0.42f , 1.0f ) ;
constexpr UIColor kPanelRowBg ( 0.17f , 0.17f , 0.17f , 1.0f ) ;
enum class ActionId : unsigned char {
HideActive = 0 ,
SaveLayout ,
CloseDocB ,
LoadLayout ,
ActivateDetails ,
LoadInvalid ,
Reset
} ;
struct ButtonState {
ActionId action = ActionId : : HideActive ;
std : : string label = { } ;
UIRect rect = { } ;
bool enabled = 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 ( ) ;
}
UIEditorPanelRegistry BuildPanelRegistry ( ) {
UIEditorPanelRegistry registry = { } ;
registry . panels = {
{ " doc-a " , " Document A " , { } , true , true , true } ,
{ " doc-b " , " Document B " , { } , true , true , true } ,
{ " details " , " Details " , { } , true , true , true }
} ;
return registry ;
}
UIEditorWorkspaceModel BuildWorkspace ( ) {
UIEditorWorkspaceModel workspace = { } ;
workspace . root = BuildUIEditorWorkspaceSplit (
" root-split " ,
UIEditorWorkspaceSplitAxis : : Horizontal ,
0.66f ,
BuildUIEditorWorkspaceTabStack (
" document-tabs " ,
{
BuildUIEditorWorkspacePanel ( " doc-a-node " , " doc-a " , " Document A " , true ) ,
BuildUIEditorWorkspacePanel ( " doc-b-node " , " doc-b " , " Document B " , true )
} ,
0u ) ,
BuildUIEditorWorkspacePanel ( " details-node " , " details " , " Details " , true ) ) ;
workspace . activePanelId = " doc-a " ;
return workspace ;
}
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 ;
}
std : : string JoinVisiblePanelIds (
const UIEditorWorkspaceModel & workspace ,
const UIEditorWorkspaceSession & session ) {
const auto panels = CollectUIEditorWorkspaceVisiblePanels ( workspace , session ) ;
if ( panels . empty ( ) ) {
return " (none) " ;
}
std : : ostringstream stream ;
for ( std : : size_t index = 0 ; index < panels . size ( ) ; + + index ) {
if ( index > 0u ) {
stream < < " , " ;
}
stream < < panels [ index ] . panelId ;
}
return stream . str ( ) ;
}
std : : string DescribePanelState (
const UIEditorWorkspaceSession & session ,
std : : string_view panelId ,
std : : string_view displayName ) {
const auto * state = FindUIEditorPanelSessionState ( session , panelId ) ;
if ( state = = nullptr ) {
return std : : string ( displayName ) + " : missing " ;
}
std : : string visibility = { } ;
if ( ! state - > open ) {
visibility = " closed " ;
} else if ( ! state - > visible ) {
visibility = " hidden " ;
} else {
visibility = " visible " ;
}
return std : : string ( displayName ) + " : " + visibility ;
}
UIColor ResolvePanelStateColor (
const UIEditorWorkspaceSession & session ,
std : : string_view panelId ) {
const auto * state = FindUIEditorPanelSessionState ( session , panelId ) ;
if ( state = = nullptr ) {
return kDanger ;
}
if ( ! state - > open ) {
return kDanger ;
}
if ( ! state - > visible ) {
return kWarning ;
}
return kSuccess ;
}
void DrawCard (
UIDrawList & drawList ,
const UIRect & rect ,
std : : string_view title ,
std : : string_view subtitle = { } ) {
drawList . AddFilledRect ( rect , kCardBg , 12.0f ) ;
drawList . AddRectOutline ( rect , kCardBorder , 1.0f , 12.0f ) ;
drawList . AddText ( UIPoint ( rect . x + 18.0f , rect . y + 16.0f ) , std : : string ( title ) , kTextPrimary , 17.0f ) ;
if ( ! subtitle . empty ( ) ) {
drawList . AddText ( UIPoint ( rect . x + 18.0f , rect . y + 42.0f ) , std : : string ( subtitle ) , kTextMuted , 12.0f ) ;
}
}
std : : string BuildSerializedPreview ( std : : string_view serializedLayout ) {
if ( serializedLayout . empty ( ) ) {
return " 尚未保存布局 " ;
}
std : : istringstream stream { std : : string ( serializedLayout ) } ;
std : : ostringstream preview = { } ;
std : : string line = { } ;
int lineCount = 0 ;
while ( std : : getline ( stream , line ) & & lineCount < 4 ) {
if ( ! line . empty ( ) & & line . back ( ) = = ' \r ' ) {
line . pop_back ( ) ;
}
if ( line . empty ( ) ) {
continue ;
}
if ( lineCount > 0 ) {
preview < < " | " ;
}
preview < < line ;
+ + lineCount ;
}
return preview . str ( ) ;
}
std : : string ReplaceActiveRecord (
std : : string serializedLayout ,
std : : string_view replacementPanelId ) {
const std : : size_t activeStart = serializedLayout . find ( " active " ) ;
if ( activeStart = = std : : string : : npos ) {
return { } ;
}
const std : : size_t lineEnd = serializedLayout . find ( ' \n ' , activeStart ) ;
std : : ostringstream replacement = { } ;
replacement < < " active " < < std : : quoted ( std : : string ( replacementPanelId ) ) ;
serializedLayout . replace (
activeStart ,
lineEnd = = std : : string : : npos ? serializedLayout . size ( ) - activeStart : lineEnd - activeStart ,
replacement . str ( ) ) ;
return serializedLayout ;
}
UIColor ResolveCommandStatusColor ( UIEditorWorkspaceCommandStatus status ) {
switch ( status ) {
case UIEditorWorkspaceCommandStatus : : Changed :
return kSuccess ;
case UIEditorWorkspaceCommandStatus : : NoOp :
return kWarning ;
case UIEditorWorkspaceCommandStatus : : Rejected :
return kDanger ;
}
return kTextMuted ;
}
UIColor ResolveLayoutStatusColor ( UIEditorWorkspaceLayoutOperationStatus status ) {
switch ( status ) {
case UIEditorWorkspaceLayoutOperationStatus : : Changed :
return kSuccess ;
case UIEditorWorkspaceLayoutOperationStatus : : NoOp :
return kWarning ;
case UIEditorWorkspaceLayoutOperationStatus : : Rejected :
return kDanger ;
}
return kTextMuted ;
}
class ScenarioApp {
public :
int 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 ) ;
}
private :
static LRESULT CALLBACK 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 - > OnResize ( 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_LBUTTONUP :
if ( app ! = nullptr ) {
app - > HandleClick (
static_cast < float > ( GET_X_LPARAM ( lParam ) ) ,
static_cast < float > ( GET_Y_LPARAM ( lParam ) ) ) ;
return 0 ;
}
break ;
case WM_KEYDOWN :
case WM_SYSKEYDOWN :
if ( app ! = nullptr ) {
if ( wParam = = VK_F12 ) {
app - > m_autoScreenshot . RequestCapture ( " manual_f12 " ) ;
} else {
app - > HandleShortcut ( static_cast < UINT > ( wParam ) ) ;
}
return 0 ;
}
break ;
case WM_ERASEBKGND :
return 1 ;
case WM_DESTROY :
if ( app ! = nullptr ) {
app - > m_hwnd = nullptr ;
}
PostQuitMessage ( 0 ) ;
return 0 ;
default :
break ;
}
return DefWindowProcW ( hwnd , message , wParam , lParam ) ;
}
bool Initialize ( HINSTANCE hInstance , int nCmdShow ) {
m_hInstance = hInstance ;
ResetScenario ( ) ;
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 ,
1360 ,
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_autoScreenshot . Initialize (
( ResolveRepoRootPath ( ) / " tests/UI/Editor/integration/state/layout_persistence/captures " )
. lexically_normal ( ) ) ;
return true ;
}
void Shutdown ( ) {
m_autoScreenshot . Shutdown ( ) ;
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 ResetScenario ( ) {
m_controller =
BuildDefaultUIEditorWorkspaceController ( BuildPanelRegistry ( ) , BuildWorkspace ( ) ) ;
m_savedSnapshot = { } ;
m_savedSerializedLayout . clear ( ) ;
m_hasSavedLayout = false ;
SetCustomResult (
" 等待操作 " ,
" Pending " ,
" 先点 `1 Hide Active`,再点 `2 Save Layout`;保存后继续改状态,再用 `4 Load Layout` 检查恢复。 " ) ;
}
void OnResize ( UINT width , UINT height ) {
if ( width = = 0 | | height = = 0 ) {
return ;
}
m_renderer . Resize ( width , height ) ;
}
void 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 ) ) ;
UIDrawData drawData = { } ;
BuildDrawData ( 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 HandleClick ( float x , float y ) {
for ( const ButtonState & button : m_buttons ) {
if ( button . enabled & & ContainsPoint ( button . rect , x , y ) ) {
DispatchAction ( button . action ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
return ;
}
}
}
void HandleShortcut ( UINT keyCode ) {
switch ( keyCode ) {
case ' 1 ' :
DispatchAction ( ActionId : : HideActive ) ;
break ;
case ' 2 ' :
DispatchAction ( ActionId : : SaveLayout ) ;
break ;
case ' 3 ' :
DispatchAction ( ActionId : : CloseDocB ) ;
break ;
case ' 4 ' :
DispatchAction ( ActionId : : LoadLayout ) ;
break ;
case ' 5 ' :
DispatchAction ( ActionId : : ActivateDetails ) ;
break ;
case ' 6 ' :
DispatchAction ( ActionId : : LoadInvalid ) ;
break ;
case ' R ' :
DispatchAction ( ActionId : : Reset ) ;
break ;
default :
return ;
}
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
}
void DispatchAction ( ActionId action ) {
switch ( action ) {
case ActionId : : HideActive : {
UIEditorWorkspaceCommand command = { } ;
command . kind = UIEditorWorkspaceCommandKind : : HidePanel ;
command . panelId = m_controller . GetWorkspace ( ) . activePanelId ;
SetCommandResult ( " Hide Active " , m_controller . Dispatch ( command ) ) ;
return ;
}
case ActionId : : SaveLayout :
SaveLayout ( ) ;
return ;
case ActionId : : CloseDocB :
SetCommandResult (
" Close Doc B " ,
m_controller . Dispatch ( { UIEditorWorkspaceCommandKind : : ClosePanel , " doc-b " } ) ) ;
return ;
case ActionId : : LoadLayout :
LoadLayout ( ) ;
return ;
case ActionId : : ActivateDetails :
SetCommandResult (
" Activate Details " ,
m_controller . Dispatch ( { UIEditorWorkspaceCommandKind : : ActivatePanel , " details " } ) ) ;
return ;
case ActionId : : LoadInvalid :
LoadInvalidLayout ( ) ;
return ;
case ActionId : : Reset :
SetCommandResult (
" Reset " ,
m_controller . Dispatch ( { UIEditorWorkspaceCommandKind : : ResetWorkspace , { } } ) ) ;
return ;
}
}
void SaveLayout ( ) {
const auto validation = m_controller . ValidateState ( ) ;
if ( ! validation . IsValid ( ) ) {
SetCustomResult (
" Save Layout " ,
" Rejected " ,
" 当前 controller 状态非法,不能保存布局: " + validation . message ) ;
return ;
}
const UIEditorWorkspaceLayoutSnapshot snapshot = m_controller . CaptureLayoutSnapshot ( ) ;
const std : : string serialized = SerializeUIEditorWorkspaceLayoutSnapshot ( snapshot ) ;
const bool changed = ! m_hasSavedLayout | | serialized ! = m_savedSerializedLayout ;
m_savedSnapshot = snapshot ;
m_savedSerializedLayout = serialized ;
m_hasSavedLayout = true ;
SetCustomResult (
" Save Layout " ,
changed ? " Saved " : " NoOp " ,
changed
? " 已更新保存的布局快照。接下来继续改状态,再点 `Load Layout` 检查恢复。 "
: " 保存内容与当前已保存布局一致。 " ) ;
}
void LoadLayout ( ) {
if ( ! m_hasSavedLayout ) {
SetCustomResult ( " Load Layout " , " Rejected " , " 当前还没有保存过布局。 " ) ;
return ;
}
SetLayoutResult ( " Load Layout " , m_controller . RestoreSerializedLayout ( m_savedSerializedLayout ) ) ;
}
void LoadInvalidLayout ( ) {
const auto validation = m_controller . ValidateState ( ) ;
if ( ! validation . IsValid ( ) ) {
SetCustomResult (
" Load Invalid " ,
" Rejected " ,
" 当前 controller 状态非法,无法构造 invalid payload: " + validation . message ) ;
return ;
}
const std : : string source =
m_hasSavedLayout
? m_savedSerializedLayout
: SerializeUIEditorWorkspaceLayoutSnapshot ( m_controller . CaptureLayoutSnapshot ( ) ) ;
const std : : string invalidSerialized = ReplaceActiveRecord ( source , " missing-panel " ) ;
if ( invalidSerialized . empty ( ) ) {
SetCustomResult ( " Load Invalid " , " Rejected " , " 构造 invalid payload 失败。 " ) ;
return ;
}
SetLayoutResult ( " Load Invalid " , m_controller . RestoreSerializedLayout ( invalidSerialized ) ) ;
}
void SetCommandResult (
std : : string actionName ,
const UIEditorWorkspaceCommandResult & result ) {
m_lastActionName = std : : move ( actionName ) ;
m_lastStatusLabel = std : : string ( GetUIEditorWorkspaceCommandStatusName ( result . status ) ) ;
m_lastMessage = result . message ;
m_lastStatusColor = ResolveCommandStatusColor ( result . status ) ;
}
void SetLayoutResult (
std : : string actionName ,
const UIEditorWorkspaceLayoutOperationResult & result ) {
m_lastActionName = std : : move ( actionName ) ;
m_lastStatusLabel = std : : string ( GetUIEditorWorkspaceLayoutOperationStatusName ( result . status ) ) ;
m_lastMessage = result . message ;
m_lastStatusColor = ResolveLayoutStatusColor ( result . status ) ;
}
void SetCustomResult (
std : : string actionName ,
std : : string statusLabel ,
std : : string message ) {
m_lastActionName = std : : move ( actionName ) ;
m_lastStatusLabel = std : : move ( statusLabel ) ;
m_lastMessage = std : : move ( message ) ;
if ( m_lastStatusLabel = = " Rejected " ) {
m_lastStatusColor = kDanger ;
} else if ( m_lastStatusLabel = = " NoOp " ) {
m_lastStatusColor = kWarning ;
} else if ( m_lastStatusLabel = = " Pending " ) {
m_lastStatusColor = kTextMuted ;
} else {
m_lastStatusColor = kSuccess ;
}
}
bool IsButtonEnabled ( ActionId action ) const {
const UIEditorWorkspaceModel & workspace = m_controller . GetWorkspace ( ) ;
const UIEditorWorkspaceSession & session = m_controller . GetSession ( ) ;
switch ( action ) {
case ActionId : : HideActive : {
if ( workspace . activePanelId . empty ( ) ) {
return false ;
}
const auto * state = FindUIEditorPanelSessionState ( session , workspace . activePanelId ) ;
return state ! = nullptr & & state - > open & & state - > visible ;
}
case ActionId : : SaveLayout :
return m_controller . ValidateState ( ) . IsValid ( ) ;
case ActionId : : CloseDocB : {
const auto * state = FindUIEditorPanelSessionState ( session , " doc-b " ) ;
return state ! = nullptr & & state - > open ;
}
case ActionId : : LoadLayout :
return m_hasSavedLayout ;
case ActionId : : ActivateDetails : {
const auto * state = FindUIEditorPanelSessionState ( session , " details " ) ;
return state ! = nullptr & &
state - > open & &
state - > visible & &
workspace . activePanelId ! = " details " ;
}
case ActionId : : LoadInvalid :
return m_controller . ValidateState ( ) . IsValid ( ) ;
case ActionId : : Reset :
return true ;
}
return false ;
}
void DrawPanelStateRows (
UIDrawList & drawList ,
float startX ,
float startY ,
float width ,
const UIEditorWorkspaceSession & session ,
std : : string_view activePanelId ) {
const std : : vector < std : : pair < std : : string , std : : string > > panelDefs = {
{ " doc-a " , " Document A " } ,
{ " doc-b " , " Document B " } ,
{ " details " , " Details " }
} ;
float rowY = startY ;
for ( const auto & [ panelId , label ] : panelDefs ) {
const UIRect rowRect ( startX , rowY , width , 54.0f ) ;
drawList . AddFilledRect ( rowRect , kPanelRowBg , 8.0f ) ;
drawList . AddRectOutline ( rowRect , kCardBorder , 1.0f , 8.0f ) ;
drawList . AddFilledRect (
UIRect ( rowRect . x + 12.0f , rowRect . y + 15.0f , 10.0f , 10.0f ) ,
ResolvePanelStateColor ( session , panelId ) ,
5.0f ) ;
drawList . AddText (
UIPoint ( rowRect . x + 32.0f , rowRect . y + 11.0f ) ,
DescribePanelState ( session , panelId , label ) ,
kTextPrimary ,
14.0f ) ;
const bool active = activePanelId = = panelId ;
drawList . AddText (
UIPoint ( rowRect . x + 32.0f , rowRect . y + 31.0f ) ,
active ? " active = true " : " active = false " ,
active ? kAccent : kTextMuted ,
12.0f ) ;
rowY + = 64.0f ;
}
}
void BuildDrawData ( UIDrawData & drawData , float width , float height ) {
const UIEditorWorkspaceModel & workspace = m_controller . GetWorkspace ( ) ;
const UIEditorWorkspaceSession & session = m_controller . GetSession ( ) ;
const auto validation = m_controller . ValidateState ( ) ;
UIDrawList & drawList = drawData . EmplaceDrawList ( " Editor Layout Persistence " ) ;
drawList . AddFilledRect ( UIRect ( 0.0f , 0.0f , width , height ) , kWindowBg ) ;
const float margin = 20.0f ;
const UIRect headerRect ( margin , margin , width - margin * 2.0f , 178.0f ) ;
const UIRect actionRect ( margin , headerRect . y + headerRect . height + 16.0f , 320.0f , height - 254.0f ) ;
const UIRect stateRect ( actionRect . x + actionRect . width + 16.0f , actionRect . y , width - actionRect . width - margin * 2.0f - 16.0f , height - 254.0f ) ;
const UIRect footerRect ( margin , height - 100.0f , width - margin * 2.0f , 80.0f ) ;
DrawCard ( drawList , headerRect , " 测试功能: Editor Layout Persistence " , " 只验证 Save / Load / Load Invalid / Reset 的布局恢复链路;不验证业务面板。 " ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 70.0f ) , " 1. 点 `1 Hide Active`,把 Document A 隐藏; active 应切到 doc-b, visible 应变成 `doc-b, details`。 " , kTextPrimary , 13.0f ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 92.0f ) , " 2. 点 `2 Save Layout` 保存当前布局;右侧 Saved 摘要必须记录刚才的 active/visible。 " , kTextPrimary , 13.0f ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 114.0f ) , " 3. 再点 `3 Close Doc B` 或 `5 Activate Details` 改状态,然后点 `4 Load Layout`;当前状态必须恢复到保存时。 " , kTextPrimary , 13.0f ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 136.0f ) , " 4. 点 `6 Load Invalid`; Result 必须是 Rejected, 且当前 active/visible 不得被污染。 " , kTextPrimary , 13.0f ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 158.0f ) , " 5. `R Reset` 必须回到基线 active=doc-a; 按 `F12` 保存当前窗口截图。 " , kTextPrimary , 13.0f ) ;
DrawCard ( drawList , actionRect , " 操作区 " , " 只保留这一步需要检查的布局保存/恢复动作。 " ) ;
DrawCard ( drawList , stateRect , " 状态摘要 " , " 左侧看 Current, 右侧看 Saved; 重点检查 active、visible 和坏数据恢复。 " ) ;
DrawCard ( drawList , footerRect , " 最近结果 " , " 显示最近一次操作、状态和当前 validation。 " ) ;
m_buttons . clear ( ) ;
const std : : vector < std : : pair < ActionId , std : : string > > buttonDefs = {
{ ActionId : : HideActive , " 1 Hide Active " } ,
{ ActionId : : SaveLayout , " 2 Save Layout " } ,
{ ActionId : : CloseDocB , " 3 Close Doc B " } ,
{ ActionId : : LoadLayout , " 4 Load Layout " } ,
{ ActionId : : ActivateDetails , " 5 Activate Details " } ,
{ ActionId : : LoadInvalid , " 6 Load Invalid " } ,
{ ActionId : : Reset , " R Reset " }
} ;
float buttonY = actionRect . y + 72.0f ;
for ( const auto & [ action , label ] : buttonDefs ) {
ButtonState button = { } ;
button . action = action ;
button . label = label ;
button . rect = UIRect ( actionRect . x + 18.0f , buttonY , actionRect . width - 36.0f , 46.0f ) ;
button . enabled = IsButtonEnabled ( action ) ;
m_buttons . push_back ( button ) ;
drawList . AddFilledRect (
button . rect ,
button . enabled ? kButtonEnabled : kButtonDisabled ,
8.0f ) ;
drawList . AddRectOutline ( button . rect , kButtonBorder , 1.0f , 8.0f ) ;
drawList . AddText (
UIPoint ( button . rect . x + 14.0f , button . rect . y + 13.0f ) ,
button . label ,
button . enabled ? kTextPrimary : kTextMuted ,
13.0f ) ;
buttonY + = 58.0f ;
}
const float columnGap = 20.0f ;
const float contentLeft = stateRect . x + 18.0f ;
const float columnWidth = ( stateRect . width - 36.0f - columnGap ) * 0.5f ;
const float rightColumnX = contentLeft + columnWidth + columnGap ;
drawList . AddText ( UIPoint ( contentLeft , stateRect . y + 70.0f ) , " Current " , kAccent , 15.0f ) ;
drawList . AddText ( UIPoint ( contentLeft , stateRect . y + 96.0f ) , " active panel " , kTextMuted , 12.0f ) ;
drawList . AddText ( UIPoint ( contentLeft , stateRect . y + 116.0f ) , workspace . activePanelId . empty ( ) ? " (none) " : workspace . activePanelId , kTextPrimary , 14.0f ) ;
drawList . AddText ( UIPoint ( contentLeft , stateRect . y + 148.0f ) , " visible panels " , kTextMuted , 12.0f ) ;
drawList . AddText ( UIPoint ( contentLeft , stateRect . y + 168.0f ) , JoinVisiblePanelIds ( workspace , session ) , kTextPrimary , 14.0f ) ;
drawList . AddText (
UIPoint ( contentLeft , stateRect . y + 198.0f ) ,
validation . IsValid ( ) ? " validation = OK " : " validation = " + validation . message ,
validation . IsValid ( ) ? kSuccess : kDanger ,
12.0f ) ;
DrawPanelStateRows ( drawList , contentLeft , stateRect . y + 236.0f , columnWidth , session , workspace . activePanelId ) ;
drawList . AddText ( UIPoint ( rightColumnX , stateRect . y + 70.0f ) , " Saved " , kAccent , 15.0f ) ;
if ( ! m_hasSavedLayout ) {
drawList . AddText ( UIPoint ( rightColumnX , stateRect . y + 104.0f ) , " 尚未执行 Save Layout。 " , kTextMuted , 13.0f ) ;
drawList . AddText ( UIPoint ( rightColumnX , stateRect . y + 128.0f ) , " 先把状态改掉,再保存,这样 Load 才有恢复意义。 " , kTextMuted , 12.0f ) ;
} else {
drawList . AddText ( UIPoint ( rightColumnX , stateRect . y + 96.0f ) , " active panel " , kTextMuted , 12.0f ) ;
drawList . AddText ( UIPoint ( rightColumnX , stateRect . y + 116.0f ) , m_savedSnapshot . workspace . activePanelId . empty ( ) ? " (none) " : m_savedSnapshot . workspace . activePanelId , kTextPrimary , 14.0f ) ;
drawList . AddText ( UIPoint ( rightColumnX , stateRect . y + 148.0f ) , " visible panels " , kTextMuted , 12.0f ) ;
drawList . AddText (
UIPoint ( rightColumnX , stateRect . y + 168.0f ) ,
JoinVisiblePanelIds ( m_savedSnapshot . workspace , m_savedSnapshot . session ) ,
kTextPrimary ,
14.0f ) ;
drawList . AddText ( UIPoint ( rightColumnX , stateRect . y + 198.0f ) , " payload preview " , kTextMuted , 12.0f ) ;
drawList . AddText (
UIPoint ( rightColumnX , stateRect . y + 220.0f ) ,
BuildSerializedPreview ( m_savedSerializedLayout ) ,
kTextPrimary ,
12.0f ) ;
}
drawList . AddText (
UIPoint ( footerRect . x + 18.0f , footerRect . y + 28.0f ) ,
" Last operation: " + m_lastActionName + " | Result: " + m_lastStatusLabel ,
m_lastStatusColor ,
13.0f ) ;
drawList . AddText ( UIPoint ( footerRect . x + 18.0f , footerRect . y + 48.0f ) , m_lastMessage , kTextPrimary , 12.0f ) ;
drawList . AddText (
UIPoint ( footerRect . x + 18.0f , footerRect . y + 66.0f ) ,
validation . IsValid ( ) ? " Validation: OK " : " Validation: " + validation . message ,
validation . IsValid ( ) ? kSuccess : kDanger ,
12.0f ) ;
const std : : string captureSummary =
m_autoScreenshot . HasPendingCapture ( )
? " 截图排队中... "
: ( m_autoScreenshot . GetLastCaptureSummary ( ) . empty ( )
? std : : string ( " F12 -> tests/UI/Editor/integration/state/layout_persistence/captures/ " )
: m_autoScreenshot . GetLastCaptureSummary ( ) ) ;
drawList . AddText ( UIPoint ( footerRect . x + 760.0f , footerRect . y + 66.0f ) , captureSummary , kTextMuted , 12.0f ) ;
}
HWND m_hwnd = nullptr ;
HINSTANCE m_hInstance = nullptr ;
ATOM m_windowClassAtom = 0 ;
NativeRenderer m_renderer = { } ;
AutoScreenshotController m_autoScreenshot = { } ;
UIEditorWorkspaceController m_controller = { } ;
UIEditorWorkspaceLayoutSnapshot m_savedSnapshot = { } ;
std : : string m_savedSerializedLayout = { } ;
bool m_hasSavedLayout = false ;
std : : vector < ButtonState > m_buttons = { } ;
std : : string m_lastActionName = { } ;
std : : string m_lastStatusLabel = { } ;
std : : string m_lastMessage = { } ;
UIColor m_lastStatusColor = kTextMuted ;
} ;
} // namespace
int WINAPI wWinMain ( HINSTANCE hInstance , HINSTANCE , LPWSTR , int nCmdShow ) {
ScenarioApp app ;
return app . Run ( hInstance , nCmdShow ) ;
}