@@ -0,0 +1,747 @@
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <XCEditor/Core/UIEditorTreeViewInteraction.h>
# include <XCEditor/Widgets/UIEditorTreeView.h>
# include "Host/AutoScreenshot.h"
# include "Host/NativeRenderer.h"
# include <XCEngine/UI/DrawData.h>
# include <XCEngine/UI/Widgets/UIExpansionModel.h>
# include <XCEngine/UI/Widgets/UISelectionModel.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 : : 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 : : Widgets : : UIExpansionModel ;
using XCEngine : : UI : : Widgets : : UISelectionModel ;
using XCEngine : : UI : : Editor : : Host : : AutoScreenshotController ;
using XCEngine : : UI : : Editor : : Host : : NativeRenderer ;
using XCEngine : : UI : : Editor : : UIEditorTreeViewInteractionFrame ;
using XCEngine : : UI : : Editor : : UIEditorTreeViewInteractionResult ;
using XCEngine : : UI : : Editor : : UIEditorTreeViewInteractionState ;
using XCEngine : : UI : : Editor : : UpdateUIEditorTreeViewInteraction ;
using XCEngine : : UI : : Editor : : Widgets : : AppendUIEditorTreeViewBackground ;
using XCEngine : : UI : : Editor : : Widgets : : AppendUIEditorTreeViewForeground ;
using XCEngine : : UI : : Editor : : Widgets : : HitTestUIEditorTreeView ;
using XCEngine : : UI : : Editor : : Widgets : : UIEditorTreeViewHitTarget ;
using XCEngine : : UI : : Editor : : Widgets : : UIEditorTreeViewHitTargetKind ;
using XCEngine : : UI : : Editor : : Widgets : : UIEditorTreeViewItem ;
using XCEngine : : UI : : Editor : : Widgets : : UIEditorTreeViewLayout ;
constexpr const wchar_t * kWindowClassName = L " XCUIEditorTreeViewBasicValidation " ;
constexpr const wchar_t * kWindowTitle = L " XCUI Editor | TreeView Basic " ;
constexpr UIColor kWindowBg ( 0.13f , 0.13f , 0.13f , 1.0f ) ;
constexpr UIColor kCardBg ( 0.18f , 0.18f , 0.18f , 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.72f , 0.72f , 0.72f , 1.0f ) ;
constexpr UIColor kTextWeak ( 0.56f , 0.56f , 0.56f , 1.0f ) ;
constexpr UIColor kTextSuccess ( 0.63f , 0.76f , 0.63f , 1.0f ) ;
constexpr UIColor kButtonBg ( 0.25f , 0.25f , 0.25f , 1.0f ) ;
constexpr UIColor kButtonHoverBg ( 0.32f , 0.32f , 0.32f , 1.0f ) ;
enum class ActionId : unsigned char {
Reset = 0 ,
Capture
} ;
struct ButtonLayout {
ActionId action = ActionId : : Reset ;
const char * label = " " ;
UIRect rect = { } ;
} ;
struct ScenarioLayout {
UIRect introRect = { } ;
UIRect controlRect = { } ;
UIRect stateRect = { } ;
UIRect previewRect = { } ;
UIRect treeRect = { } ;
std : : vector < ButtonLayout > buttons = { } ;
} ;
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 ;
}
ScenarioLayout BuildScenarioLayout ( float width , float height ) {
constexpr float margin = 20.0f ;
constexpr float leftWidth = 430.0f ;
constexpr float gap = 16.0f ;
ScenarioLayout layout = { } ;
layout . introRect = UIRect ( margin , margin , leftWidth , 214.0f ) ;
layout . controlRect = UIRect ( margin , layout . introRect . y + layout . introRect . height + gap , leftWidth , 92.0f ) ;
layout . stateRect = UIRect (
margin ,
layout . controlRect . y + layout . controlRect . height + gap ,
leftWidth ,
( std : : max ) ( 200.0f , height - ( layout . controlRect . y + layout . controlRect . height + gap ) - margin ) ) ;
layout . previewRect = UIRect (
leftWidth + margin * 2.0f ,
margin ,
( std : : max ) ( 420.0f , width - leftWidth - margin * 3.0f ) ,
height - margin * 2.0f ) ;
layout . treeRect = UIRect (
layout . previewRect . x + 18.0f ,
layout . previewRect . y + 64.0f ,
layout . previewRect . width - 36.0f ,
layout . previewRect . height - 84.0f ) ;
const float buttonWidth = ( layout . controlRect . width - 44.0f ) * 0.5f ;
const float buttonY = layout . controlRect . y + 40.0f ;
layout . buttons = {
{ ActionId : : Reset , " 重置 " , UIRect ( layout . controlRect . x + 14.0f , buttonY , buttonWidth , 36.0f ) } ,
{ ActionId : : Capture , " 截图(F12) " , UIRect ( layout . controlRect . x + 26.0f + buttonWidth , buttonY , buttonWidth , 36.0f ) }
} ;
return layout ;
}
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 + 40.0f ) , std : : string ( subtitle ) , kTextMuted , 12.0f ) ;
}
}
void DrawButton (
UIDrawList & drawList ,
const ButtonLayout & button ,
bool hovered ) {
drawList . AddFilledRect ( button . rect , hovered ? kButtonHoverBg : kButtonBg , 8.0f ) ;
drawList . AddRectOutline ( button . rect , kCardBorder , 1.0f , 8.0f ) ;
drawList . AddText (
UIPoint ( button . rect . x + 16.0f , button . rect . y + 10.0f ) ,
button . label ,
kTextPrimary ,
12.0f ) ;
}
std : : vector < UIEditorTreeViewItem > BuildTreeItems ( ) {
return {
{ " scene " , " Scene " , 0u , false , 0.0f } ,
{ " camera " , " Camera " , 1u , true , 0.0f } ,
{ " lights " , " Lights " , 1u , false , 0.0f } ,
{ " directional-light " , " Directional Light " , 2u , true , 0.0f } ,
{ " fill-light " , " Fill Light " , 2u , true , 0.0f } ,
{ " ui-root " , " UI Root " , 0u , false , 0.0f } ,
{ " canvas " , " Canvas " , 1u , true , 0.0f } ,
{ " event-system " , " EventSystem " , 1u , true , 0.0f }
} ;
}
std : : string JoinExpandedItems (
const std : : vector < UIEditorTreeViewItem > & items ,
const UIExpansionModel & expansionModel ) {
std : : ostringstream stream = { } ;
bool first = true ;
for ( const UIEditorTreeViewItem & item : items ) {
if ( ! expansionModel . IsExpanded ( item . itemId ) ) {
continue ;
}
if ( ! first ) {
stream < < " | " ;
}
first = false ;
stream < < item . label ;
}
return first ? " (none) " : stream . str ( ) ;
}
std : : string JoinVisibleItems (
const std : : vector < UIEditorTreeViewItem > & items ,
const UIEditorTreeViewLayout & layout ) {
if ( layout . visibleItemIndices . empty ( ) ) {
return " (none) " ;
}
constexpr std : : size_t kMaxVisibleLabels = 5u ;
std : : ostringstream stream = { } ;
const std : : size_t labelCount = ( std : : min ) ( layout . visibleItemIndices . size ( ) , kMaxVisibleLabels ) ;
for ( std : : size_t index = 0 ; index < labelCount ; + + index ) {
if ( index > 0u ) {
stream < < " | " ;
}
const std : : size_t itemIndex = layout . visibleItemIndices [ index ] ;
if ( itemIndex < items . size ( ) ) {
stream < < items [ itemIndex ] . label ;
}
}
if ( layout . visibleItemIndices . size ( ) > labelCount ) {
stream < < " | + " < < ( layout . visibleItemIndices . size ( ) - labelCount ) < < " more " ;
}
return stream . str ( ) ;
}
std : : string DescribeHitTarget (
const UIEditorTreeViewHitTarget & hitTarget ,
const std : : vector < UIEditorTreeViewItem > & items ) {
if ( hitTarget . itemIndex > = items . size ( ) ) {
return " 无 " ;
}
const std : : string & label = items [ hitTarget . itemIndex ] . label ;
switch ( hitTarget . kind ) {
case UIEditorTreeViewHitTargetKind : : Disclosure :
return " disclosure: " + label ;
case UIEditorTreeViewHitTargetKind : : Row :
return " row: " + label ;
case UIEditorTreeViewHitTargetKind : : None :
default :
return " 无 " ;
}
}
UIInputEvent MakePointerEvent (
UIInputEventType type ,
const UIPoint & position ,
UIPointerButton button = UIPointerButton : : None ) {
UIInputEvent event = { } ;
event . type = type ;
event . position = position ;
event . pointerButton = button ;
return event ;
}
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_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_MOUSELEAVE :
if ( app ! = nullptr ) {
app - > HandleMouseLeave ( ) ;
return 0 ;
}
break ;
case WM_LBUTTONDOWN :
if ( app ! = nullptr ) {
app - > HandleLeftButtonDown (
static_cast < float > ( GET_X_LPARAM ( lParam ) ) ,
static_cast < float > ( GET_Y_LPARAM ( lParam ) ) ) ;
return 0 ;
}
break ;
case WM_LBUTTONUP :
if ( app ! = nullptr ) {
app - > HandleLeftButtonUp (
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 & & wParam = = VK_F12 ) {
app - > m_autoScreenshot . RequestCapture ( " manual_f12 " ) ;
app - > m_lastResult = " 已请求截图,输出到 captures/latest.png " ;
InvalidateRect ( hwnd , nullptr , FALSE ) ;
return 0 ;
}
break ;
case WM_PAINT :
if ( app ! = nullptr ) {
PAINTSTRUCT paintStruct = { } ;
BeginPaint ( hwnd , & paintStruct ) ;
app - > RenderFrame ( ) ;
EndPaint ( hwnd , & paintStruct ) ;
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 ) {
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 ,
1480 ,
920 ,
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_captureRoot =
ResolveRepoRootPath ( ) / " tests/UI/Editor/integration/shell/tree_view_basic/captures " ;
m_autoScreenshot . Initialize ( m_captureRoot ) ;
ResetScenario ( ) ;
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 ) {
UnregisterClassW ( kWindowClassName , GetModuleHandleW ( nullptr ) ) ;
m_windowClassAtom = 0 ;
}
}
ScenarioLayout GetLayout ( ) const {
RECT clientRect = { } ;
GetClientRect ( m_hwnd , & clientRect ) ;
const float width = static_cast < float > ( ( std : : max ) ( 1L , clientRect . right - clientRect . left ) ) ;
const float height = static_cast < float > ( ( std : : max ) ( 1L , clientRect . bottom - clientRect . top ) ) ;
return BuildScenarioLayout ( width , height ) ;
}
void ResetScenario ( ) {
m_items = BuildTreeItems ( ) ;
m_selectionModel = { } ;
m_selectionModel . SetSelection ( " camera " ) ;
m_expansionModel = { } ;
m_expansionModel . Expand ( " scene " ) ;
m_expansionModel . Expand ( " lights " ) ;
m_expansionModel . Expand ( " ui-root " ) ;
m_interactionState = { } ;
m_interactionState . treeViewState . focused = true ;
m_mousePosition = UIPoint ( - 1000.0f , - 1000.0f ) ;
m_hoveredAction = ActionId : : Reset ;
m_hasHoveredAction = false ;
m_lastResult = " 已重置到默认树结构 " ;
RefreshTreeFrame ( ) ;
}
void RefreshTreeFrame ( ) {
if ( m_hwnd = = nullptr ) {
return ;
}
const ScenarioLayout layout = GetLayout ( ) ;
m_treeFrame =
UpdateUIEditorTreeViewInteraction (
m_interactionState ,
m_selectionModel ,
m_expansionModel ,
layout . treeRect ,
m_items ,
{ } ) ;
}
void OnResize ( UINT width , UINT height ) {
if ( width = = 0u | | height = = 0u ) {
return ;
}
m_renderer . Resize ( width , height ) ;
RefreshTreeFrame ( ) ;
}
void HandleMouseMove ( float x , float y ) {
m_mousePosition = UIPoint ( x , y ) ;
const ScenarioLayout layout = GetLayout ( ) ;
UpdateHoveredAction ( layout , x , y ) ;
TRACKMOUSEEVENT trackEvent = { } ;
trackEvent . cbSize = sizeof ( trackEvent ) ;
trackEvent . dwFlags = TME_LEAVE ;
trackEvent . hwndTrack = m_hwnd ;
TrackMouseEvent ( & trackEvent ) ;
PumpTreeEvents ( { MakePointerEvent ( UIInputEventType : : PointerMove , m_mousePosition ) } ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
}
void HandleMouseLeave ( ) {
m_mousePosition = UIPoint ( - 1000.0f , - 1000.0f ) ;
m_hasHoveredAction = false ;
PumpTreeEvents ( { MakePointerEvent ( UIInputEventType : : PointerLeave , m_mousePosition ) } ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
}
void HandleLeftButtonDown ( float x , float y ) {
m_mousePosition = UIPoint ( x , y ) ;
const ScenarioLayout layout = GetLayout ( ) ;
if ( HitTestAction ( layout , x , y ) ! = nullptr ) {
UpdateHoveredAction ( layout , x , y ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
return ;
}
PumpTreeEvents ( { MakePointerEvent ( UIInputEventType : : PointerButtonDown , m_mousePosition , UIPointerButton : : Left ) } ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
}
void HandleLeftButtonUp ( float x , float y ) {
m_mousePosition = UIPoint ( x , y ) ;
const ScenarioLayout layout = GetLayout ( ) ;
const ButtonLayout * button = HitTestAction ( layout , x , y ) ;
if ( button ! = nullptr ) {
ExecuteAction ( button - > action ) ;
UpdateHoveredAction ( layout , x , y ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
return ;
}
const bool wasFocused = m_interactionState . treeViewState . focused ;
const bool insideTree = ContainsPoint ( layout . treeRect , x , y ) ;
const UIEditorTreeViewInteractionResult result =
PumpTreeEvents ( { MakePointerEvent ( UIInputEventType : : PointerButtonUp , m_mousePosition , UIPointerButton : : Left ) } ) ;
UpdateResultText ( result , wasFocused , insideTree ) ;
InvalidateRect ( m_hwnd , nullptr , FALSE ) ;
}
void UpdateHoveredAction ( const ScenarioLayout & layout , float x , float y ) {
const ButtonLayout * button = HitTestAction ( layout , x , y ) ;
if ( button = = nullptr ) {
m_hasHoveredAction = false ;
return ;
}
m_hoveredAction = button - > action ;
m_hasHoveredAction = true ;
}
const ButtonLayout * HitTestAction ( const ScenarioLayout & layout , float x , float y ) const {
for ( const ButtonLayout & button : layout . buttons ) {
if ( ContainsPoint ( button . rect , x , y ) ) {
return & button ;
}
}
return nullptr ;
}
UIEditorTreeViewInteractionResult PumpTreeEvents ( std : : vector < UIInputEvent > events ) {
const ScenarioLayout layout = GetLayout ( ) ;
m_treeFrame =
UpdateUIEditorTreeViewInteraction (
m_interactionState ,
m_selectionModel ,
m_expansionModel ,
layout . treeRect ,
m_items ,
events ) ;
return m_treeFrame . result ;
}
void UpdateResultText (
const UIEditorTreeViewInteractionResult & result ,
bool wasFocused ,
bool insideTree ) {
if ( result . expansionChanged & & ! result . toggledItemId . empty ( ) ) {
m_lastResult = " 切换展开: " + result . toggledItemId ;
return ;
}
if ( result . selectionChanged & & ! result . selectedItemId . empty ( ) ) {
m_lastResult = " 选中行: " + result . selectedItemId ;
return ;
}
if ( ! insideTree & & wasFocused & & ! m_interactionState . treeViewState . focused ) {
m_lastResult = " 点击树外空白: focus 已清除, selection 保留 " ;
return ;
}
if ( insideTree ) {
m_lastResult = " 点击树内空白: 只更新 focus / hover " ;
return ;
}
m_lastResult = " 等待交互 " ;
}
void ExecuteAction ( ActionId action ) {
switch ( action ) {
case ActionId : : Reset :
ResetScenario ( ) ;
break ;
case ActionId : : Capture :
m_autoScreenshot . RequestCapture ( " manual_button " ) ;
m_lastResult = " 已请求截图,输出到 captures/latest.png " ;
break ;
}
}
void RenderFrame ( ) {
if ( m_hwnd = = nullptr ) {
return ;
}
RECT clientRect = { } ;
GetClientRect ( m_hwnd , & clientRect ) ;
const float width = static_cast < float > ( ( std : : max ) ( 1L , clientRect . right - clientRect . left ) ) ;
const float height = static_cast < float > ( ( std : : max ) ( 1L , clientRect . bottom - clientRect . top ) ) ;
const ScenarioLayout layout = BuildScenarioLayout ( width , height ) ;
RefreshTreeFrame ( ) ;
const UIEditorTreeViewHitTarget currentHit =
HitTestUIEditorTreeView ( m_treeFrame . layout , m_mousePosition ) ;
UIDrawData drawData = { } ;
UIDrawList & drawList = drawData . EmplaceDrawList ( " EditorTreeViewBasic " ) ;
drawList . AddFilledRect ( UIRect ( 0.0f , 0.0f , width , height ) , kWindowBg ) ;
DrawCard (
drawList ,
layout . introRect ,
" 这个测试在验证什么功能 " ,
" 只验证 Editor TreeView 基础控件,不涉及任何业务面板。 " ) ;
drawList . AddText (
UIPoint ( layout . introRect . x + 16.0f , layout . introRect . y + 72.0f ) ,
" 1. 验证行缩进是否正确: Scene 的子项右移一层, Directional Light 再右移一层。 " ,
kTextPrimary ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . introRect . x + 16.0f , layout . introRect . y + 94.0f ) ,
" 2. 点击 disclosure 只切换展开/折叠,不应误改 selection。 " ,
kTextPrimary ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . introRect . x + 16.0f , layout . introRect . y + 116.0f ) ,
" 3. 点击行只切换 selection; hover、selected、focused 三种视觉状态要能区分。 " ,
kTextPrimary ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . introRect . x + 16.0f , layout . introRect . y + 138.0f ) ,
" 4. 点击树外空白后 focus 应清除,但 selection 不应丢失。 " ,
kTextPrimary ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . introRect . x + 16.0f , layout . introRect . y + 160.0f ) ,
" 5. 按 F12 手动截图;设置 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可自动截图。 " ,
kTextPrimary ,
12.0f ) ;
DrawCard ( drawList , layout . controlRect , " 操作 " ) ;
for ( const ButtonLayout & button : layout . buttons ) {
DrawButton (
drawList ,
button ,
m_hasHoveredAction & & m_hoveredAction = = button . action ) ;
}
DrawCard ( drawList , layout . stateRect , " 状态摘要 " , " 重点检查 hit / focus / selection / expanded / visible。 " ) ;
drawList . AddText (
UIPoint ( layout . stateRect . x + 16.0f , layout . stateRect . y + 70.0f ) ,
" Hover: " + DescribeHitTarget ( currentHit , m_items ) ,
kTextPrimary ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . stateRect . x + 16.0f , layout . stateRect . y + 94.0f ) ,
std : : string ( " Focused: " ) + ( m_interactionState . treeViewState . focused ? " 开 " : " 关 " ) ,
kTextPrimary ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . stateRect . x + 16.0f , layout . stateRect . y + 118.0f ) ,
" Selected: " +
( m_selectionModel . HasSelection ( ) ? m_selectionModel . GetSelectedId ( ) : std : : string ( " (none) " ) ) ,
kTextSuccess ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . stateRect . x + 16.0f , layout . stateRect . y + 142.0f ) ,
" Expanded: " + JoinExpandedItems ( m_items , m_expansionModel ) ,
kTextMuted ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . stateRect . x + 16.0f , layout . stateRect . y + 166.0f ) ,
" Visible( " + std : : to_string ( m_treeFrame . layout . visibleItemIndices . size ( ) ) + " ): " +
JoinVisibleItems ( m_items , m_treeFrame . layout ) ,
kTextMuted ,
12.0f ) ;
drawList . AddText (
UIPoint ( layout . stateRect . x + 16.0f , layout . stateRect . y + 190.0f ) ,
" Result: " + m_lastResult ,
kTextPrimary ,
12.0f ) ;
const std : : string captureSummary =
m_autoScreenshot . HasPendingCapture ( )
? " 截图排队中... "
: ( m_autoScreenshot . GetLastCaptureSummary ( ) . empty ( )
? std : : string ( " F12 -> tests/UI/Editor/integration/shell/tree_view_basic/captures/ " )
: m_autoScreenshot . GetLastCaptureSummary ( ) ) ;
drawList . AddText (
UIPoint ( layout . stateRect . x + 16.0f , layout . stateRect . y + 216.0f ) ,
captureSummary ,
kTextWeak ,
12.0f ) ;
DrawCard ( drawList , layout . previewRect , " TreeView 预览 " , " 这里只放一个 TreeView, 不混入 Hierarchy/Inspector 等业务内容。 " ) ;
AppendUIEditorTreeViewBackground (
drawList ,
m_treeFrame . layout ,
m_items ,
m_selectionModel ,
m_interactionState . treeViewState ) ;
AppendUIEditorTreeViewForeground ( drawList , m_treeFrame . layout , m_items ) ;
const bool framePresented = m_renderer . Render ( drawData ) ;
m_autoScreenshot . CaptureIfRequested (
m_renderer ,
drawData ,
static_cast < unsigned int > ( width ) ,
static_cast < unsigned int > ( height ) ,
framePresented ) ;
}
HWND m_hwnd = nullptr ;
ATOM m_windowClassAtom = 0 ;
NativeRenderer m_renderer = { } ;
AutoScreenshotController m_autoScreenshot = { } ;
std : : filesystem : : path m_captureRoot = { } ;
std : : vector < UIEditorTreeViewItem > m_items = { } ;
UISelectionModel m_selectionModel = { } ;
UIExpansionModel m_expansionModel = { } ;
UIEditorTreeViewInteractionState m_interactionState = { } ;
UIEditorTreeViewInteractionFrame m_treeFrame = { } ;
UIPoint m_mousePosition = UIPoint ( - 1000.0f , - 1000.0f ) ;
ActionId m_hoveredAction = ActionId : : Reset ;
bool m_hasHoveredAction = false ;
std : : string m_lastResult = { } ;
} ;
} // namespace
int WINAPI wWinMain ( HINSTANCE hInstance , HINSTANCE , LPWSTR , int nCmdShow ) {
return ScenarioApp ( ) . Run ( hInstance , nCmdShow ) ;
}