@@ -0,0 +1,935 @@
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <XCNewEditor/Editor/UIEditorCommandDispatcher.h>
# include <XCNewEditor/Editor/UIEditorMenuModel.h>
# include <XCNewEditor/Editor/UIEditorShortcutManager.h>
# include <XCNewEditor/Host/AutoScreenshot.h>
# include <XCNewEditor/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 : : NewEditor : : BuildDefaultUIEditorWorkspaceController ;
using XCEngine : : NewEditor : : BuildUIEditorResolvedMenuModel ;
using XCEngine : : NewEditor : : BuildUIEditorWorkspacePanel ;
using XCEngine : : NewEditor : : BuildUIEditorWorkspaceSplit ;
using XCEngine : : NewEditor : : BuildUIEditorWorkspaceTabStack ;
using XCEngine : : NewEditor : : CollectUIEditorWorkspaceVisiblePanels ;
using XCEngine : : NewEditor : : FindUIEditorPanelSessionState ;
using XCEngine : : NewEditor : : GetUIEditorCommandDispatchStatusName ;
using XCEngine : : NewEditor : : GetUIEditorMenuItemKindName ;
using XCEngine : : NewEditor : : GetUIEditorWorkspaceCommandStatusName ;
using XCEngine : : NewEditor : : UIEditorCommandDispatchResult ;
using XCEngine : : NewEditor : : UIEditorCommandDispatcher ;
using XCEngine : : NewEditor : : UIEditorCommandPanelSource ;
using XCEngine : : NewEditor : : UIEditorCommandRegistry ;
using XCEngine : : NewEditor : : UIEditorMenuCheckedStateSource ;
using XCEngine : : NewEditor : : UIEditorMenuDescriptor ;
using XCEngine : : NewEditor : : UIEditorMenuItemDescriptor ;
using XCEngine : : NewEditor : : UIEditorMenuItemKind ;
using XCEngine : : NewEditor : : UIEditorMenuModel ;
using XCEngine : : NewEditor : : UIEditorPanelRegistry ;
using XCEngine : : NewEditor : : UIEditorResolvedMenuDescriptor ;
using XCEngine : : NewEditor : : UIEditorResolvedMenuItem ;
using XCEngine : : NewEditor : : UIEditorResolvedMenuModel ;
using XCEngine : : NewEditor : : UIEditorShortcutManager ;
using XCEngine : : NewEditor : : UIEditorWorkspaceCommandKind ;
using XCEngine : : NewEditor : : UIEditorWorkspaceCommandStatus ;
using XCEngine : : NewEditor : : UIEditorWorkspaceController ;
using XCEngine : : NewEditor : : UIEditorWorkspaceModel ;
using XCEngine : : NewEditor : : UIEditorWorkspaceSession ;
using XCEngine : : NewEditor : : UIEditorWorkspaceSplitAxis ;
using XCEngine : : NewEditor : : ValidateUIEditorMenuModel ;
using XCEngine : : UI : : UIColor ;
using XCEngine : : UI : : UIDrawData ;
using XCEngine : : UI : : UIDrawList ;
using XCEngine : : UI : : UIInputEventType ;
using XCEngine : : UI : : UIPoint ;
using XCEngine : : UI : : UIRect ;
using XCEngine : : UI : : UIShortcutBinding ;
using XCEngine : : UI : : UIShortcutScope ;
using XCEngine : : XCUI : : Host : : AutoScreenshotController ;
using XCEngine : : XCUI : : Host : : NativeRenderer ;
constexpr const wchar_t * kWindowClassName = L " XCUIEditorMenuBarBasicValidation " ;
constexpr const wchar_t * kWindowTitle = L " XCUI Editor | Menu Bar Basic " ;
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 kTextDisabled ( 0.50f , 0.50f , 0.50f , 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 kMenuBarBg ( 0.16f , 0.16f , 0.16f , 1.0f ) ;
constexpr UIColor kMenuButtonBg ( 0.24f , 0.24f , 0.24f , 1.0f ) ;
constexpr UIColor kMenuButtonHover ( 0.30f , 0.30f , 0.30f , 1.0f ) ;
constexpr UIColor kMenuButtonOpen ( 0.36f , 0.36f , 0.36f , 1.0f ) ;
constexpr UIColor kMenuDropBg ( 0.17f , 0.17f , 0.17f , 1.0f ) ;
constexpr UIColor kMenuItemHover ( 0.28f , 0.28f , 0.28f , 1.0f ) ;
constexpr UIColor kMenuDivider ( 0.32f , 0.32f , 0.32f , 1.0f ) ;
constexpr UIColor kIndicatorBg ( 0.23f , 0.23f , 0.23f , 1.0f ) ;
struct MenuButtonLayout {
std : : string menuId = { } ;
std : : string label = { } ;
UIRect rect = { } ;
} ;
struct MenuItemLayout {
std : : string menuId = { } ;
std : : string itemId = { } ;
UIEditorMenuItemKind kind = UIEditorMenuItemKind : : Command ;
std : : string label = { } ;
std : : string commandId = { } ;
std : : string shortcutText = { } ;
UIRect rect = { } ;
bool enabled = false ;
bool checked = false ;
} ;
std : : filesystem : : path ResolveRepoRootPath ( ) ;
UIEditorPanelRegistry BuildPanelRegistry ( ) ;
UIEditorWorkspaceModel BuildWorkspace ( ) ;
UIEditorCommandRegistry BuildCommandRegistry ( ) ;
UIShortcutBinding MakeBinding ( std : : string commandId , KeyCode keyCode , bool shift = false ) ;
UIEditorShortcutManager BuildShortcutManager ( ) ;
UIEditorMenuModel BuildMenuModel ( ) ;
bool ContainsPoint ( const UIRect & rect , float x , float y ) ;
std : : string JoinVisiblePanelIds ( const UIEditorWorkspaceModel & workspace , const UIEditorWorkspaceSession & session ) ;
void DrawCard ( UIDrawList & drawList , const UIRect & rect , std : : string_view title , std : : string_view subtitle = { } ) ;
UIColor ResolveResultColor ( std : : string_view statusLabel ) ;
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 OnResize ( UINT width , UINT height ) ;
void RenderFrame ( ) ;
void HandleMouseMove ( float x , float y ) ;
void HandleClick ( float x , float y ) ;
void HandleKeyDown ( UINT keyCode ) ;
void SetDispatchResult ( std : : string actionName , const UIEditorCommandDispatchResult & result ) ;
void SetCustomResult ( std : : string actionName , std : : string statusLabel , std : : string message ) ;
void DrawMenuBar ( UIDrawList & drawList , const UIRect & rect , const UIEditorResolvedMenuModel & resolvedModel ) ;
void DrawOpenMenu ( UIDrawList & drawList , const UIEditorResolvedMenuDescriptor & menu , const UIRect & anchorRect ) ;
void BuildDrawData ( UIDrawData & drawData , float width , float height ) ;
HWND m_hwnd = nullptr ;
HINSTANCE m_hInstance = nullptr ;
ATOM m_windowClassAtom = 0 ;
NativeRenderer m_renderer = { } ;
AutoScreenshotController m_autoScreenshot = { } ;
UIEditorWorkspaceController m_controller = { } ;
UIEditorCommandDispatcher m_commandDispatcher = { } ;
UIEditorShortcutManager m_shortcutManager = { } ;
UIEditorMenuModel m_menuModel = { } ;
std : : vector < MenuButtonLayout > m_menuButtons = { } ;
std : : vector < MenuItemLayout > m_menuItems = { } ;
std : : string m_openMenuId = { } ;
std : : string m_hoveredMenuId = { } ;
std : : string m_hoveredItemId = { } ;
std : : string m_lastActionName = { } ;
std : : string m_lastStatusLabel = { } ;
std : : string m_lastMessage = { } ;
UIColor m_lastStatusColor = kTextMuted ;
} ;
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 ;
}
UIEditorCommandRegistry BuildCommandRegistry ( ) {
UIEditorCommandRegistry registry = { } ;
registry . commands = {
{
" workspace.show_details " ,
" Show Details " ,
{ UIEditorWorkspaceCommandKind : : ShowPanel , UIEditorCommandPanelSource : : FixedPanelId , " details " }
} ,
{
" workspace.hide_active " ,
" Hide Active " ,
{ UIEditorWorkspaceCommandKind : : HidePanel , UIEditorCommandPanelSource : : ActivePanel , { } }
} ,
{
" workspace.activate_details " ,
" Activate Details " ,
{ UIEditorWorkspaceCommandKind : : ActivatePanel , UIEditorCommandPanelSource : : FixedPanelId , " details " }
} ,
{
" workspace.reset " ,
" Reset Workspace " ,
{ UIEditorWorkspaceCommandKind : : ResetWorkspace , UIEditorCommandPanelSource : : None , { } }
}
} ;
return registry ;
}
UIShortcutBinding MakeBinding (
std : : string commandId ,
KeyCode keyCode ,
bool shift ) {
UIShortcutBinding binding = { } ;
binding . commandId = std : : move ( commandId ) ;
binding . scope = UIShortcutScope : : Global ;
binding . triggerEventType = UIInputEventType : : KeyDown ;
binding . chord . keyCode = static_cast < std : : int32_t > ( keyCode ) ;
binding . chord . modifiers . control = true ;
binding . chord . modifiers . shift = shift ;
return binding ;
}
UIEditorShortcutManager BuildShortcutManager ( ) {
UIEditorShortcutManager manager ( BuildCommandRegistry ( ) ) ;
manager . RegisterBinding ( MakeBinding ( " workspace.show_details " , KeyCode : : D , true ) ) ;
manager . RegisterBinding ( MakeBinding ( " workspace.hide_active " , KeyCode : : H ) ) ;
manager . RegisterBinding ( MakeBinding ( " workspace.activate_details " , KeyCode : : D ) ) ;
manager . RegisterBinding ( MakeBinding ( " workspace.reset " , KeyCode : : R ) ) ;
return manager ;
}
UIEditorMenuModel BuildMenuModel ( ) {
UIEditorMenuModel model = { } ;
model . menus = {
UIEditorMenuDescriptor {
" file " ,
" File " ,
{
UIEditorMenuItemDescriptor {
UIEditorMenuItemKind : : Command ,
" file-reset " ,
{ } ,
" workspace.reset " ,
{ } ,
{ }
}
}
} ,
UIEditorMenuDescriptor {
" window " ,
" Window " ,
{
UIEditorMenuItemDescriptor {
UIEditorMenuItemKind : : Command ,
" window-show-details " ,
{ } ,
" workspace.show_details " ,
{ UIEditorMenuCheckedStateSource : : PanelVisible , " details " } ,
{ }
} ,
UIEditorMenuItemDescriptor {
UIEditorMenuItemKind : : Command ,
" window-hide-active " ,
{ } ,
" workspace.hide_active " ,
{ } ,
{ }
} ,
UIEditorMenuItemDescriptor {
UIEditorMenuItemKind : : Command ,
" window-activate-details " ,
{ } ,
" workspace.activate_details " ,
{ UIEditorMenuCheckedStateSource : : PanelActive , " details " } ,
{ }
}
}
} ,
UIEditorMenuDescriptor {
" layout " ,
" Layout " ,
{
UIEditorMenuItemDescriptor {
UIEditorMenuItemKind : : Command ,
" layout-reset " ,
{ } ,
" workspace.reset " ,
{ } ,
{ }
}
}
}
} ;
return model ;
}
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 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 ( ) ;
}
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 ) ;
}
}
UIColor ResolveResultColor ( std : : string_view statusLabel ) {
if ( statusLabel = = " Changed " | | statusLabel = = " Dispatched " ) {
return kSuccess ;
}
if ( statusLabel = = " NoOp " | | statusLabel = = " Dismissed " | | statusLabel = = " Disabled " ) {
return kWarning ;
}
if ( statusLabel = = " Rejected " ) {
return kDanger ;
}
return 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 - > 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_MOUSEMOVE :
if ( app ! = nullptr ) {
app - > HandleMouseMove (
static_cast < float > ( GET_X_LPARAM ( lParam ) ) ,
static_cast < float > ( GET_Y_LPARAM ( lParam ) ) ) ;
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 - > HandleKeyDown ( 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 ScenarioApp : : 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/shell/menu_bar_basic/captures " )
. lexically_normal ( ) ) ;
return true ;
}
void ScenarioApp : : 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 ScenarioApp : : ResetScenario ( ) {
m_controller =
BuildDefaultUIEditorWorkspaceController ( BuildPanelRegistry ( ) , BuildWorkspace ( ) ) ;
m_commandDispatcher = UIEditorCommandDispatcher ( BuildCommandRegistry ( ) ) ;
m_shortcutManager = BuildShortcutManager ( ) ;
m_menuModel = BuildMenuModel ( ) ;
m_openMenuId . clear ( ) ;
m_hoveredMenuId . clear ( ) ;
m_hoveredItemId . clear ( ) ;
m_menuButtons . clear ( ) ;
m_menuItems . clear ( ) ;
SetCustomResult (
" 等待操作 " ,
" Pending " ,
" 先点 File / Window / Layout, 确认一次只会打开一个菜单; 再点菜单外区域或按 Escape 关闭。 " ) ;
}
void ScenarioApp : : OnResize ( UINT width , UINT height ) {
if ( width = = 0 | | height = = 0 ) {
return ;
}
m_renderer . Resize ( width , height ) ;
}
void ScenarioApp : : 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 ScenarioApp : : HandleMouseMove ( float x , float y ) {
std : : string hoveredMenuId = { } ;
std : : string hoveredItemId = { } ;
for ( const MenuButtonLayout & button : m_menuButtons ) {
if ( ContainsPoint ( button . rect , x , y ) ) {
hoveredMenuId = button . menuId ;
break ;
}
}
for ( const MenuItemLayout & item : m_menuItems ) {
if ( ContainsPoint ( item . rect , x , y ) ) {
hoveredItemId = item . itemId ;
break ;
}
}
if ( hoveredMenuId ! = m_hoveredMenuId | | hoveredItemId ! = m_hoveredItemId ) {
m_hoveredMenuId = std : : move ( hoveredMenuId ) ;
m_hoveredItemId = std : : move ( hoveredItemId ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
}
}
void ScenarioApp : : HandleClick ( float x , float y ) {
for ( const MenuButtonLayout & button : m_menuButtons ) {
if ( ! ContainsPoint ( button . rect , x , y ) ) {
continue ;
}
if ( m_openMenuId = = button . menuId ) {
m_openMenuId . clear ( ) ;
m_hoveredItemId . clear ( ) ;
SetCustomResult ( " 关闭菜单 " , " NoOp " , " 再次点击当前菜单按钮,菜单已关闭。 " ) ;
} else {
m_openMenuId = button . menuId ;
m_hoveredItemId . clear ( ) ;
SetCustomResult (
" 打开菜单 " ,
" Changed " ,
" 当前激活菜单: " + button . label + " 。确认同一时刻只存在一个下拉菜单。 " ) ;
}
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
return ;
}
for ( const MenuItemLayout & item : m_menuItems ) {
if ( ! ContainsPoint ( item . rect , x , y ) ) {
continue ;
}
if ( item . kind ! = UIEditorMenuItemKind : : Command ) {
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
return ;
}
if ( ! item . enabled ) {
SetCustomResult (
" 菜单项不可执行 " ,
" Disabled " ,
" 当前工作区状态下 ` " + item . label + " ` 不可执行。 " ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
return ;
}
const UIEditorCommandDispatchResult result =
m_commandDispatcher . Dispatch ( item . commandId , m_controller ) ;
m_openMenuId . clear ( ) ;
m_hoveredItemId . clear ( ) ;
SetDispatchResult ( item . label , result ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
return ;
}
if ( ! m_openMenuId . empty ( ) ) {
m_openMenuId . clear ( ) ;
m_hoveredItemId . clear ( ) ;
SetCustomResult ( " 菜单失焦 " , " Dismissed " , " 点击菜单外区域,菜单已关闭。 " ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
}
}
void ScenarioApp : : HandleKeyDown ( UINT keyCode ) {
switch ( keyCode ) {
case VK_ESCAPE :
if ( ! m_openMenuId . empty ( ) ) {
m_openMenuId . clear ( ) ;
m_hoveredItemId . clear ( ) ;
SetCustomResult ( " Esc 关闭菜单 " , " Dismissed " , " 按下 Escape 后,菜单已关闭。 " ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
}
break ;
case ' R ' :
SetDispatchResult (
" 键盘 Reset Workspace " ,
m_commandDispatcher . Dispatch ( " workspace.reset " , m_controller ) ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
break ;
default :
break ;
}
}
void ScenarioApp : : SetDispatchResult (
std : : string actionName ,
const UIEditorCommandDispatchResult & result ) {
m_lastActionName = std : : move ( actionName ) ;
if ( result . commandExecuted ) {
m_lastStatusLabel =
std : : string ( GetUIEditorWorkspaceCommandStatusName ( result . commandResult . status ) ) ;
m_lastMessage =
result . displayName + " -> " + result . commandResult . message ;
} else {
m_lastStatusLabel =
std : : string ( GetUIEditorCommandDispatchStatusName ( result . status ) ) ;
m_lastMessage = result . message ;
}
m_lastStatusColor = ResolveResultColor ( m_lastStatusLabel ) ;
}
void ScenarioApp : : 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 ) ;
m_lastStatusColor = ResolveResultColor ( m_lastStatusLabel ) ;
}
void ScenarioApp : : DrawMenuBar (
UIDrawList & drawList ,
const UIRect & rect ,
const UIEditorResolvedMenuModel & resolvedModel ) {
drawList . AddFilledRect ( rect , kMenuBarBg , 8.0f ) ;
drawList . AddRectOutline ( rect , kCardBorder , 1.0f , 8.0f ) ;
m_menuButtons . clear ( ) ;
m_menuItems . clear ( ) ;
float buttonX = rect . x + 12.0f ;
for ( const UIEditorResolvedMenuDescriptor & menu : resolvedModel . menus ) {
const bool open = m_openMenuId = = menu . menuId ;
const bool hovered = m_hoveredMenuId = = menu . menuId ;
const float buttonWidth = 96.0f ;
const UIRect buttonRect ( buttonX , rect . y + 6.0f , buttonWidth , rect . height - 12.0f ) ;
drawList . AddFilledRect (
buttonRect ,
open ? kMenuButtonOpen : ( hovered ? kMenuButtonHover : kMenuButtonBg ) ,
6.0f ) ;
drawList . AddRectOutline ( buttonRect , kCardBorder , 1.0f , 6.0f ) ;
drawList . AddText (
UIPoint ( buttonRect . x + 14.0f , buttonRect . y + 10.0f ) ,
menu . label ,
kTextPrimary ,
14.0f ) ;
m_menuButtons . push_back ( { menu . menuId , menu . label , buttonRect } ) ;
buttonX + = buttonWidth + 10.0f ;
}
}
void ScenarioApp : : DrawOpenMenu (
UIDrawList & drawList ,
const UIEditorResolvedMenuDescriptor & menu ,
const UIRect & anchorRect ) {
float contentHeight = 10.0f ;
for ( const UIEditorResolvedMenuItem & item : menu . items ) {
contentHeight + = item . kind = = UIEditorMenuItemKind : : Separator ? 12.0f : 34.0f ;
}
contentHeight + = 8.0f ;
const UIRect dropRect ( anchorRect . x , anchorRect . y + anchorRect . height + 6.0f , 320.0f , contentHeight ) ;
drawList . AddFilledRect ( dropRect , kMenuDropBg , 8.0f ) ;
drawList . AddRectOutline ( dropRect , kCardBorder , 1.0f , 8.0f ) ;
float itemY = dropRect . y + 8.0f ;
for ( const UIEditorResolvedMenuItem & item : menu . items ) {
if ( item . kind = = UIEditorMenuItemKind : : Separator ) {
drawList . AddFilledRect (
UIRect ( dropRect . x + 12.0f , itemY + 4.0f , dropRect . width - 24.0f , 1.0f ) ,
kMenuDivider ) ;
itemY + = 12.0f ;
continue ;
}
const UIRect itemRect ( dropRect . x + 8.0f , itemY , dropRect . width - 16.0f , 30.0f ) ;
const bool hovered = m_hoveredItemId = = item . itemId ;
if ( hovered ) {
drawList . AddFilledRect ( itemRect , kMenuItemHover , 6.0f ) ;
}
if ( item . checked ) {
drawList . AddFilledRect (
UIRect ( itemRect . x + 10.0f , itemRect . y + 8.0f , 10.0f , 10.0f ) ,
kAccent ,
3.0f ) ;
} else {
drawList . AddRectOutline (
UIRect ( itemRect . x + 10.0f , itemRect . y + 8.0f , 10.0f , 10.0f ) ,
kMenuDivider ,
1.0f ,
3.0f ) ;
}
drawList . AddText (
UIPoint ( itemRect . x + 30.0f , itemRect . y + 7.0f ) ,
item . label ,
item . enabled ? kTextPrimary : kTextDisabled ,
13.0f ) ;
if ( ! item . shortcutText . empty ( ) ) {
drawList . AddText (
UIPoint ( itemRect . x + itemRect . width - 90.0f , itemRect . y + 7.0f ) ,
item . shortcutText ,
item . enabled ? kTextMuted : kTextDisabled ,
12.0f ) ;
}
m_menuItems . push_back (
{ menu . menuId , item . itemId , item . kind , item . label , item . commandId , item . shortcutText , itemRect , item . enabled , item . checked } ) ;
itemY + = 34.0f ;
}
}
void ScenarioApp : : BuildDrawData ( UIDrawData & drawData , float width , float height ) {
const auto menuValidation =
ValidateUIEditorMenuModel ( m_menuModel , m_commandDispatcher . GetCommandRegistry ( ) ) ;
const auto shortcutValidation = m_shortcutManager . ValidateConfiguration ( ) ;
const auto resolvedModel =
BuildUIEditorResolvedMenuModel (
m_menuModel ,
m_commandDispatcher ,
m_controller ,
& m_shortcutManager ) ;
const UIEditorWorkspaceModel & workspace = m_controller . GetWorkspace ( ) ;
const UIEditorWorkspaceSession & session = m_controller . GetSession ( ) ;
const auto * detailsState = FindUIEditorPanelSessionState ( session , " details " ) ;
UIDrawList & drawList = drawData . EmplaceDrawList ( " Editor Menu Bar Basic " ) ;
drawList . AddFilledRect ( UIRect ( 0.0f , 0.0f , width , height ) , kWindowBg ) ;
const float margin = 20.0f ;
const UIRect headerRect ( margin , margin , width - margin * 2.0f , 184.0f ) ;
const UIRect shellRect ( margin , headerRect . y + headerRect . height + 16.0f , width * 0.58f , height - 320.0f ) ;
const UIRect stateRect ( shellRect . x + shellRect . width + 16.0f , shellRect . y , width - shellRect . width - margin * 2.0f - 16.0f , height - 320.0f ) ;
const UIRect footerRect ( margin , height - 100.0f , width - margin * 2.0f , 80.0f ) ;
DrawCard (
drawList ,
headerRect ,
" 测试内容: Editor Menu 基础壳层验证 " ,
" 只验证 MenuBar / 下拉展开 / hover / 菜单关闭 / command dispatch; 不验证业务面板, 不验证完整编辑器菜单体系。 " ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 70.0f ) , " 1. 点击 File / Window / Layout, 确认同一时刻只会有一个菜单展开。 " , kTextPrimary , 13.0f ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 92.0f ) , " 2. 展开菜单后移动鼠标,确认 hover 高亮稳定, disabled 项不会误显示成可点击。 " , kTextPrimary , 13.0f ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 114.0f ) , " 3. 点击菜单外区域或按 Escape, 菜单必须立即关闭; Footer 会显示 Dismissed。 " , kTextPrimary , 13.0f ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 136.0f ) , " 4. 点 Window -> Activate Details, 再点 Window -> Hide Active, 可检查 Details 的 checked 状态是否随 visible/active 正确变化。 " , kTextPrimary , 13.0f ) ;
drawList . AddText ( UIPoint ( headerRect . x + 18.0f , headerRect . y + 158.0f ) , " 5. 菜单右侧 shortcut 文案只做显示验证; F12 保存截图。 " , kTextPrimary , 13.0f ) ;
DrawCard ( drawList , shellRect , " 操作区 " , " 这里只放菜单栏和当前打开的下拉菜单。 " ) ;
DrawCard ( drawList , stateRect , " 状态摘要 " , " 看 open/hover/current workspace, 确认菜单交互边界和命令派发结果。 " ) ;
DrawCard ( drawList , footerRect , " 最近结果 " , " 显示最近一次菜单交互、命令状态和截图输出。 " ) ;
const UIRect menuBarRect ( shellRect . x + 18.0f , shellRect . y + 74.0f , shellRect . width - 36.0f , 46.0f ) ;
DrawMenuBar ( drawList , menuBarRect , resolvedModel ) ;
const UIRect shellInfoRect ( shellRect . x + 18.0f , shellRect . y + 144.0f , shellRect . width - 36.0f , 200.0f ) ;
drawList . AddFilledRect ( shellInfoRect , kIndicatorBg , 8.0f ) ;
drawList . AddRectOutline ( shellInfoRect , kCardBorder , 1.0f , 8.0f ) ;
drawList . AddText ( UIPoint ( shellInfoRect . x + 14.0f , shellInfoRect . y + 14.0f ) , " Open menu " , kTextMuted , 12.0f ) ;
drawList . AddText ( UIPoint ( shellInfoRect . x + 14.0f , shellInfoRect . y + 34.0f ) , m_openMenuId . empty ( ) ? " (none) " : m_openMenuId , kTextPrimary , 14.0f ) ;
drawList . AddText ( UIPoint ( shellInfoRect . x + 14.0f , shellInfoRect . y + 62.0f ) , " Hover menu " , kTextMuted , 12.0f ) ;
drawList . AddText ( UIPoint ( shellInfoRect . x + 14.0f , shellInfoRect . y + 82.0f ) , m_hoveredMenuId . empty ( ) ? " (none) " : m_hoveredMenuId , kTextPrimary , 14.0f ) ;
drawList . AddText ( UIPoint ( shellInfoRect . x + 14.0f , shellInfoRect . y + 110.0f ) , " Hover item " , kTextMuted , 12.0f ) ;
drawList . AddText ( UIPoint ( shellInfoRect . x + 14.0f , shellInfoRect . y + 130.0f ) , m_hoveredItemId . empty ( ) ? " (none) " : m_hoveredItemId , kTextPrimary , 14.0f ) ;
drawList . AddText ( UIPoint ( shellInfoRect . x + 14.0f , shellInfoRect . y + 158.0f ) , " 提示:按 R 可直接触发 Reset Workspace。 " , kTextMuted , 12.0f ) ;
if ( ! m_openMenuId . empty ( ) ) {
const UIEditorResolvedMenuDescriptor * openMenu = nullptr ;
for ( const UIEditorResolvedMenuDescriptor & menu : resolvedModel . menus ) {
if ( menu . menuId = = m_openMenuId ) {
openMenu = & menu ;
break ;
}
}
const MenuButtonLayout * openButton = nullptr ;
for ( const MenuButtonLayout & button : m_menuButtons ) {
if ( button . menuId = = m_openMenuId ) {
openButton = & button ;
break ;
}
}
if ( openMenu ! = nullptr & & openButton ! = nullptr ) {
DrawOpenMenu ( drawList , * openMenu , openButton - > rect ) ;
}
}
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 72.0f ) , " Current Workspace " , kAccent , 15.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 100.0f ) , " active panel " , kTextMuted , 12.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 120.0f ) , workspace . activePanelId . empty ( ) ? " (none) " : workspace . activePanelId , kTextPrimary , 14.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 150.0f ) , " visible panels " , kTextMuted , 12.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 170.0f ) , JoinVisiblePanelIds ( workspace , session ) , kTextPrimary , 14.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 200.0f ) , " details visible " , kTextMuted , 12.0f ) ;
drawList . AddText (
UIPoint ( stateRect . x + 18.0f , stateRect . y + 220.0f ) ,
detailsState ! = nullptr & & detailsState - > visible ? " true " : " false " ,
detailsState ! = nullptr & & detailsState - > visible ? kSuccess : kWarning ,
14.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 250.0f ) , " details active " , kTextMuted , 12.0f ) ;
drawList . AddText (
UIPoint ( stateRect . x + 18.0f , stateRect . y + 270.0f ) ,
workspace . activePanelId = = " details " ? " true " : " false " ,
workspace . activePanelId = = " details " ? kSuccess : kTextMuted ,
14.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 302.0f ) , " menu validation " , kTextMuted , 12.0f ) ;
drawList . AddText (
UIPoint ( stateRect . x + 18.0f , stateRect . y + 322.0f ) ,
menuValidation . IsValid ( ) ? " OK " : menuValidation . message ,
menuValidation . IsValid ( ) ? kSuccess : kDanger ,
12.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 352.0f ) , " shortcut validation " , kTextMuted , 12.0f ) ;
drawList . AddText (
UIPoint ( stateRect . x + 18.0f , stateRect . y + 372.0f ) ,
shortcutValidation . IsValid ( ) ? " OK " : shortcutValidation . message ,
shortcutValidation . IsValid ( ) ? kSuccess : kDanger ,
12.0f ) ;
drawList . AddText ( UIPoint ( stateRect . x + 18.0f , stateRect . y + 402.0f ) , " menu model item kinds " , kTextMuted , 12.0f ) ;
drawList . AddText (
UIPoint ( stateRect . x + 18.0f , stateRect . y + 422.0f ) ,
std : : string ( " Command / " ) + std : : string ( GetUIEditorMenuItemKindName ( UIEditorMenuItemKind : : Separator ) ) + " / " +
std : : string ( GetUIEditorMenuItemKindName ( UIEditorMenuItemKind : : Submenu ) ) ,
kTextPrimary ,
12.0f ) ;
drawList . AddText (
UIPoint ( footerRect . x + 18.0f , footerRect . y + 28.0f ) ,
" Last interaction: " + 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 ) ;
const std : : string captureSummary =
m_autoScreenshot . HasPendingCapture ( )
? " 截图排队中... "
: ( m_autoScreenshot . GetLastCaptureSummary ( ) . empty ( )
? std : : string ( " F12 -> tests/UI/Editor/integration/shell/menu_bar_basic/captures/ " )
: m_autoScreenshot . GetLastCaptureSummary ( ) ) ;
drawList . AddText (
UIPoint ( footerRect . x + 18.0f , footerRect . y + 66.0f ) ,
captureSummary ,
kTextMuted ,
12.0f ) ;
}
} // namespace
int WINAPI wWinMain ( HINSTANCE hInstance , HINSTANCE , LPWSTR , int nCmdShow ) {
ScenarioApp app ;
return app . Run ( hInstance , nCmdShow ) ;
}