From 7f0d1f0b0802e955c6dcf7ac739a8fb57a6df641 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 04:23:33 +0800 Subject: [PATCH] Build XCEditor viewport slot shell foundation --- new_editor/CMakeLists.txt | 1 + .../XCEditor/Widgets/UIEditorViewportSlot.h | 203 +++++ .../src/Widgets/UIEditorViewportSlot.cpp | 532 +++++++++++++ tests/UI/Editor/integration/CMakeLists.txt | 5 + tests/UI/Editor/integration/README.md | 9 + .../Editor/integration/shell/CMakeLists.txt | 3 + .../shell/viewport_slot_basic/CMakeLists.txt | 30 + .../viewport_slot_basic/captures/.gitkeep | 1 + .../shell/viewport_slot_basic/main.cpp | 741 ++++++++++++++++++ tests/UI/Editor/unit/CMakeLists.txt | 1 + .../unit/test_ui_editor_viewport_slot.cpp | 281 +++++++ 11 files changed, 1807 insertions(+) create mode 100644 new_editor/include/XCEditor/Widgets/UIEditorViewportSlot.h create mode 100644 new_editor/src/Widgets/UIEditorViewportSlot.cpp create mode 100644 tests/UI/Editor/integration/shell/viewport_slot_basic/CMakeLists.txt create mode 100644 tests/UI/Editor/integration/shell/viewport_slot_basic/captures/.gitkeep create mode 100644 tests/UI/Editor/integration/shell/viewport_slot_basic/main.cpp create mode 100644 tests/UI/Editor/unit/test_ui_editor_viewport_slot.cpp diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 8467b3a6..d4e13fed 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -30,6 +30,7 @@ add_library(XCUIEditorLib STATIC src/Widgets/UIEditorPanelFrame.cpp src/Widgets/UIEditorStatusBar.cpp src/Widgets/UIEditorTabStrip.cpp + src/Widgets/UIEditorViewportSlot.cpp ) target_include_directories(XCUIEditorLib diff --git a/new_editor/include/XCEditor/Widgets/UIEditorViewportSlot.h b/new_editor/include/XCEditor/Widgets/UIEditorViewportSlot.h new file mode 100644 index 00000000..3a0a0120 --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorViewportSlot.h @@ -0,0 +1,203 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Widgets { + +inline constexpr std::size_t UIEditorViewportSlotInvalidIndex = + static_cast(-1); + +enum class UIEditorViewportSlotToolSlot : std::uint8_t { + Leading = 0, + Trailing +}; + +enum class UIEditorViewportSlotHitTargetKind : std::uint8_t { + None = 0, + TopBar, + Title, + ToolItem, + Surface, + BottomBar, + StatusSegment, + StatusSeparator +}; + +struct UIEditorViewportSlotFrame { + ::XCEngine::UI::UITextureHandle texture = {}; + ::XCEngine::UI::UISize requestedSize = {}; + ::XCEngine::UI::UISize presentedSize = {}; + bool hasTexture = false; + std::string statusText = {}; +}; + +struct UIEditorViewportSlotChrome { + std::string_view title = {}; + std::string_view subtitle = {}; + bool showTopBar = true; + bool showBottomBar = true; + float topBarHeight = 40.0f; + float bottomBarHeight = 28.0f; +}; + +struct UIEditorViewportSlotToolItem { + std::string itemId = {}; + std::string label = {}; + UIEditorViewportSlotToolSlot slot = UIEditorViewportSlotToolSlot::Trailing; + bool enabled = true; + bool selected = false; + float desiredWidth = 0.0f; +}; + +struct UIEditorViewportSlotState { + bool focused = false; + bool surfaceHovered = false; + bool surfaceActive = false; + bool inputCaptured = false; + std::size_t hoveredToolIndex = UIEditorViewportSlotInvalidIndex; + std::size_t activeToolIndex = UIEditorViewportSlotInvalidIndex; + UIEditorStatusBarState statusBarState = {}; +}; + +struct UIEditorViewportSlotMetrics { + float cornerRounding = 10.0f; + float outerBorderThickness = 1.0f; + float focusedBorderThickness = 1.5f; + float topBarPaddingX = 12.0f; + float topBarPaddingY = 7.0f; + float toolPaddingX = 10.0f; + float toolGap = 6.0f; + float toolCornerRounding = 6.0f; + float titleGap = 10.0f; + float titleInsetY = 10.0f; + float subtitleInsetY = 25.0f; + float estimatedGlyphWidth = 7.0f; + float surfaceInset = 0.0f; + float surfaceBorderThickness = 1.0f; + float focusedSurfaceBorderThickness = 1.5f; +}; + +struct UIEditorViewportSlotPalette { + ::XCEngine::UI::UIColor frameColor = + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor topBarColor = + ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f); + ::XCEngine::UI::UIColor surfaceColor = + ::XCEngine::UI::UIColor(0.10f, 0.10f, 0.10f, 1.0f); + ::XCEngine::UI::UIColor surfaceHoverOverlayColor = + ::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 0.24f); + ::XCEngine::UI::UIColor surfaceActiveOverlayColor = + ::XCEngine::UI::UIColor(0.34f, 0.34f, 0.34f, 0.18f); + ::XCEngine::UI::UIColor captureOverlayColor = + ::XCEngine::UI::UIColor(0.70f, 0.70f, 0.70f, 0.10f); + ::XCEngine::UI::UIColor borderColor = + ::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f); + ::XCEngine::UI::UIColor focusedBorderColor = + ::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f); + ::XCEngine::UI::UIColor surfaceBorderColor = + ::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f); + ::XCEngine::UI::UIColor surfaceHoveredBorderColor = + ::XCEngine::UI::UIColor(0.44f, 0.44f, 0.44f, 1.0f); + ::XCEngine::UI::UIColor surfaceActiveBorderColor = + ::XCEngine::UI::UIColor(0.64f, 0.64f, 0.64f, 1.0f); + ::XCEngine::UI::UIColor surfaceCapturedBorderColor = + ::XCEngine::UI::UIColor(0.86f, 0.86f, 0.86f, 1.0f); + ::XCEngine::UI::UIColor toolColor = + ::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f); + ::XCEngine::UI::UIColor toolHoveredColor = + ::XCEngine::UI::UIColor(0.31f, 0.31f, 0.31f, 1.0f); + ::XCEngine::UI::UIColor toolSelectedColor = + ::XCEngine::UI::UIColor(0.39f, 0.39f, 0.39f, 1.0f); + ::XCEngine::UI::UIColor toolDisabledColor = + ::XCEngine::UI::UIColor(0.19f, 0.19f, 0.19f, 1.0f); + ::XCEngine::UI::UIColor toolBorderColor = + ::XCEngine::UI::UIColor(0.40f, 0.40f, 0.40f, 1.0f); + ::XCEngine::UI::UIColor textPrimary = + ::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f); + ::XCEngine::UI::UIColor textSecondary = + ::XCEngine::UI::UIColor(0.76f, 0.76f, 0.76f, 1.0f); + ::XCEngine::UI::UIColor textMuted = + ::XCEngine::UI::UIColor(0.62f, 0.62f, 0.62f, 1.0f); + ::XCEngine::UI::UIColor imageTint = + ::XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f); + UIEditorStatusBarPalette statusBarPalette = {}; +}; + +struct UIEditorViewportSlotLayout { + ::XCEngine::UI::UIRect bounds = {}; + ::XCEngine::UI::UIRect topBarRect = {}; + ::XCEngine::UI::UIRect titleRect = {}; + ::XCEngine::UI::UIRect subtitleRect = {}; + ::XCEngine::UI::UIRect toolbarLeadingRect = {}; + ::XCEngine::UI::UIRect toolbarTrailingRect = {}; + ::XCEngine::UI::UIRect surfaceRect = {}; + ::XCEngine::UI::UIRect textureRect = {}; + ::XCEngine::UI::UIRect inputRect = {}; + ::XCEngine::UI::UIRect bottomBarRect = {}; + std::vector<::XCEngine::UI::UIRect> toolItemRects = {}; + UIEditorStatusBarLayout statusBarLayout = {}; + ::XCEngine::UI::UISize requestedSurfaceSize = {}; + bool hasTopBar = false; + bool hasBottomBar = false; +}; + +struct UIEditorViewportSlotHitTarget { + UIEditorViewportSlotHitTargetKind kind = UIEditorViewportSlotHitTargetKind::None; + std::size_t index = UIEditorViewportSlotInvalidIndex; +}; + +float ResolveUIEditorViewportSlotDesiredToolWidth( + const UIEditorViewportSlotToolItem& item, + const UIEditorViewportSlotMetrics& metrics = {}); + +UIEditorViewportSlotLayout BuildUIEditorViewportSlotLayout( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportSlotChrome& chrome, + const UIEditorViewportSlotFrame& frame, + const std::vector& toolItems, + const std::vector& statusSegments, + const UIEditorViewportSlotMetrics& metrics = {}); + +UIEditorViewportSlotHitTarget HitTestUIEditorViewportSlot( + const UIEditorViewportSlotLayout& layout, + const ::XCEngine::UI::UIPoint& point); + +void AppendUIEditorViewportSlotBackground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorViewportSlotLayout& layout, + const std::vector& toolItems, + const std::vector& statusSegments, + const UIEditorViewportSlotState& state, + const UIEditorViewportSlotPalette& palette = {}, + const UIEditorViewportSlotMetrics& metrics = {}); + +void AppendUIEditorViewportSlotForeground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorViewportSlotLayout& layout, + const UIEditorViewportSlotChrome& chrome, + const UIEditorViewportSlotFrame& frame, + const std::vector& toolItems, + const std::vector& statusSegments, + const UIEditorViewportSlotState& state, + const UIEditorViewportSlotPalette& palette = {}, + const UIEditorViewportSlotMetrics& metrics = {}); + +void AppendUIEditorViewportSlot( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportSlotChrome& chrome, + const UIEditorViewportSlotFrame& frame, + const std::vector& toolItems, + const std::vector& statusSegments, + const UIEditorViewportSlotState& state, + const UIEditorViewportSlotPalette& palette = {}, + const UIEditorViewportSlotMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Widgets/UIEditorViewportSlot.cpp b/new_editor/src/Widgets/UIEditorViewportSlot.cpp new file mode 100644 index 00000000..4aa6f262 --- /dev/null +++ b/new_editor/src/Widgets/UIEditorViewportSlot.cpp @@ -0,0 +1,532 @@ +#include + +#include +#include + +namespace XCEngine::UI::Editor::Widgets { + +namespace { + +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; +using ::XCEngine::UI::UISize; + +bool ContainsPoint(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + +bool HasArea(const UIRect& rect) { + return rect.width > 0.0f && rect.height > 0.0f; +} + +float ClampNonNegative(float value) { + return (std::max)(value, 0.0f); +} + +UIRect InsetRect(const UIRect& rect, float inset) { + const float clampedInset = ClampNonNegative(inset); + return UIRect( + rect.x + clampedInset, + rect.y + clampedInset, + (std::max)(rect.width - clampedInset * 2.0f, 0.0f), + (std::max)(rect.height - clampedInset * 2.0f, 0.0f)); +} + +UISize ResolveFrameAspectSize(const UIEditorViewportSlotFrame& frame, const UISize& fallback) { + if (frame.presentedSize.width > 0.0f && frame.presentedSize.height > 0.0f) { + return frame.presentedSize; + } + + if (frame.texture.IsValid()) { + return UISize( + static_cast(frame.texture.width), + static_cast(frame.texture.height)); + } + + if (frame.requestedSize.width > 0.0f && frame.requestedSize.height > 0.0f) { + return frame.requestedSize; + } + + return fallback; +} + +UIRect FitRectToAspect(const UIRect& container, const UISize& size) { + if (!HasArea(container) || size.width <= 0.0f || size.height <= 0.0f) { + return container; + } + + const float containerAspect = container.width / container.height; + const float frameAspect = size.width / size.height; + if (frameAspect <= 0.0f) { + return container; + } + + if (frameAspect >= containerAspect) { + const float fittedHeight = container.width / frameAspect; + return UIRect( + container.x, + container.y + (container.height - fittedHeight) * 0.5f, + container.width, + fittedHeight); + } + + const float fittedWidth = container.height * frameAspect; + return UIRect( + container.x + (container.width - fittedWidth) * 0.5f, + container.y, + fittedWidth, + container.height); +} + +UIColor ResolveToolFillColor( + const UIEditorViewportSlotToolItem& item, + bool hovered, + bool active, + const UIEditorViewportSlotPalette& palette) { + if (!item.enabled) { + return palette.toolDisabledColor; + } + + if (active || item.selected) { + return palette.toolSelectedColor; + } + + if (hovered) { + return palette.toolHoveredColor; + } + + return palette.toolColor; +} + +UIColor ResolveSurfaceBorderColor( + const UIEditorViewportSlotState& state, + const UIEditorViewportSlotPalette& palette) { + if (state.inputCaptured) { + return palette.surfaceCapturedBorderColor; + } + + if (state.surfaceActive) { + return palette.surfaceActiveBorderColor; + } + + if (state.surfaceHovered) { + return palette.surfaceHoveredBorderColor; + } + + return palette.surfaceBorderColor; +} + +float ResolveSurfaceBorderThickness( + const UIEditorViewportSlotState& state, + const UIEditorViewportSlotMetrics& metrics) { + if (state.inputCaptured || state.focused) { + return metrics.focusedSurfaceBorderThickness; + } + + return metrics.surfaceBorderThickness; +} + +} // namespace + +float ResolveUIEditorViewportSlotDesiredToolWidth( + const UIEditorViewportSlotToolItem& item, + const UIEditorViewportSlotMetrics& metrics) { + if (item.desiredWidth > 0.0f) { + return item.desiredWidth; + } + + return item.label.empty() + ? metrics.toolPaddingX * 2.0f + : metrics.toolPaddingX * 2.0f + + static_cast(item.label.size()) * metrics.estimatedGlyphWidth; +} + +UIEditorViewportSlotLayout BuildUIEditorViewportSlotLayout( + const UIRect& bounds, + const UIEditorViewportSlotChrome& chrome, + const UIEditorViewportSlotFrame& frame, + const std::vector& toolItems, + const std::vector& statusSegments, + const UIEditorViewportSlotMetrics& metrics) { + UIEditorViewportSlotLayout layout = {}; + layout.bounds = UIRect( + bounds.x, + bounds.y, + ClampNonNegative(bounds.width), + ClampNonNegative(bounds.height)); + layout.toolItemRects.resize(toolItems.size(), UIRect{}); + + float remainingHeight = layout.bounds.height; + const float topBarHeight = + chrome.showTopBar + ? (std::min)(ClampNonNegative(chrome.topBarHeight), remainingHeight) + : 0.0f; + remainingHeight = (std::max)(remainingHeight - topBarHeight, 0.0f); + const float bottomBarHeight = + chrome.showBottomBar + ? (std::min)(ClampNonNegative(chrome.bottomBarHeight), remainingHeight) + : 0.0f; + + layout.hasTopBar = topBarHeight > 0.0f; + layout.hasBottomBar = bottomBarHeight > 0.0f; + + if (layout.hasTopBar) { + layout.topBarRect = UIRect( + layout.bounds.x, + layout.bounds.y, + layout.bounds.width, + topBarHeight); + } + + if (layout.hasBottomBar) { + layout.bottomBarRect = UIRect( + layout.bounds.x, + layout.bounds.y + layout.bounds.height - bottomBarHeight, + layout.bounds.width, + bottomBarHeight); + layout.statusBarLayout = + BuildUIEditorStatusBarLayout(layout.bottomBarRect, statusSegments); + } + + const float surfaceTop = layout.hasTopBar + ? layout.topBarRect.y + layout.topBarRect.height + : layout.bounds.y; + const float surfaceBottom = layout.hasBottomBar + ? layout.bottomBarRect.y + : layout.bounds.y + layout.bounds.height; + layout.surfaceRect = UIRect( + layout.bounds.x, + surfaceTop, + layout.bounds.width, + (std::max)(surfaceBottom - surfaceTop, 0.0f)); + layout.inputRect = InsetRect(layout.surfaceRect, metrics.surfaceInset); + layout.requestedSurfaceSize = + UISize(layout.inputRect.width, layout.inputRect.height); + layout.textureRect = frame.hasTexture + ? FitRectToAspect( + layout.inputRect, + ResolveFrameAspectSize(frame, layout.requestedSurfaceSize)) + : layout.inputRect; + + if (!layout.hasTopBar) { + return layout; + } + + const float toolbarTop = layout.topBarRect.y + metrics.topBarPaddingY; + const float toolbarHeight = + (std::max)(layout.topBarRect.height - metrics.topBarPaddingY * 2.0f, 0.0f); + const float innerLeft = layout.topBarRect.x + metrics.topBarPaddingX; + const float innerRight = + layout.topBarRect.x + layout.topBarRect.width - metrics.topBarPaddingX; + + float leadingCursor = innerLeft; + float trailingCursor = innerRight; + float leadingRight = innerLeft; + float trailingLeft = innerRight; + + for (std::size_t index = 0u; index < toolItems.size(); ++index) { + const auto& item = toolItems[index]; + const float itemWidth = ResolveUIEditorViewportSlotDesiredToolWidth(item, metrics); + if (item.slot != UIEditorViewportSlotToolSlot::Leading) { + continue; + } + + layout.toolItemRects[index] = UIRect( + leadingCursor, + toolbarTop, + itemWidth, + toolbarHeight); + leadingCursor += itemWidth + metrics.toolGap; + leadingRight = layout.toolItemRects[index].x + layout.toolItemRects[index].width; + } + + for (std::size_t reverseIndex = toolItems.size(); reverseIndex > 0u; --reverseIndex) { + const std::size_t index = reverseIndex - 1u; + const auto& item = toolItems[index]; + const float itemWidth = ResolveUIEditorViewportSlotDesiredToolWidth(item, metrics); + if (item.slot != UIEditorViewportSlotToolSlot::Trailing) { + continue; + } + + trailingCursor -= itemWidth; + layout.toolItemRects[index] = UIRect( + trailingCursor, + toolbarTop, + itemWidth, + toolbarHeight); + trailingLeft = trailingCursor; + trailingCursor -= metrics.toolGap; + } + + if (leadingRight > innerLeft) { + layout.toolbarLeadingRect = UIRect( + innerLeft, + layout.topBarRect.y, + leadingRight - innerLeft, + layout.topBarRect.height); + } + + if (trailingLeft < innerRight) { + layout.toolbarTrailingRect = UIRect( + trailingLeft, + layout.topBarRect.y, + innerRight - trailingLeft, + layout.topBarRect.height); + } + + float titleLeft = innerLeft; + if (HasArea(layout.toolbarLeadingRect)) { + titleLeft = layout.toolbarLeadingRect.x + layout.toolbarLeadingRect.width + metrics.titleGap; + } + + float titleRight = innerRight; + if (HasArea(layout.toolbarTrailingRect)) { + titleRight = layout.toolbarTrailingRect.x - metrics.titleGap; + } + + layout.titleRect = UIRect( + titleLeft, + layout.topBarRect.y, + (std::max)(titleRight - titleLeft, 0.0f), + layout.topBarRect.height); + layout.subtitleRect = layout.titleRect; + return layout; +} + +UIEditorViewportSlotHitTarget HitTestUIEditorViewportSlot( + const UIEditorViewportSlotLayout& layout, + const UIPoint& point) { + if (!ContainsPoint(layout.bounds, point)) { + return {}; + } + + if (layout.hasTopBar) { + for (std::size_t index = 0u; index < layout.toolItemRects.size(); ++index) { + if (HasArea(layout.toolItemRects[index]) && + ContainsPoint(layout.toolItemRects[index], point)) { + return { UIEditorViewportSlotHitTargetKind::ToolItem, index }; + } + } + + if (HasArea(layout.titleRect) && ContainsPoint(layout.titleRect, point)) { + return { UIEditorViewportSlotHitTargetKind::Title, UIEditorViewportSlotInvalidIndex }; + } + + if (ContainsPoint(layout.topBarRect, point)) { + return { UIEditorViewportSlotHitTargetKind::TopBar, UIEditorViewportSlotInvalidIndex }; + } + } + + if (layout.hasBottomBar && ContainsPoint(layout.bottomBarRect, point)) { + const UIEditorStatusBarHitTarget hit = + HitTestUIEditorStatusBar(layout.statusBarLayout, point); + switch (hit.kind) { + case UIEditorStatusBarHitTargetKind::Segment: + return { UIEditorViewportSlotHitTargetKind::StatusSegment, hit.index }; + case UIEditorStatusBarHitTargetKind::Separator: + return { UIEditorViewportSlotHitTargetKind::StatusSeparator, hit.index }; + case UIEditorStatusBarHitTargetKind::Background: + return { UIEditorViewportSlotHitTargetKind::BottomBar, UIEditorViewportSlotInvalidIndex }; + case UIEditorStatusBarHitTargetKind::None: + default: + break; + } + } + + if (ContainsPoint(layout.inputRect, point) || ContainsPoint(layout.surfaceRect, point)) { + return { UIEditorViewportSlotHitTargetKind::Surface, UIEditorViewportSlotInvalidIndex }; + } + + return {}; +} + +void AppendUIEditorViewportSlotBackground( + UIDrawList& drawList, + const UIEditorViewportSlotLayout& layout, + const std::vector& toolItems, + const std::vector& statusSegments, + const UIEditorViewportSlotState& state, + const UIEditorViewportSlotPalette& palette, + const UIEditorViewportSlotMetrics& metrics) { + drawList.AddFilledRect(layout.bounds, palette.frameColor, metrics.cornerRounding); + drawList.AddRectOutline( + layout.bounds, + state.focused ? palette.focusedBorderColor : palette.borderColor, + state.focused ? metrics.focusedBorderThickness : metrics.outerBorderThickness, + metrics.cornerRounding); + + if (layout.hasTopBar) { + drawList.AddFilledRect(layout.topBarRect, palette.topBarColor, metrics.cornerRounding); + } + + drawList.AddFilledRect(layout.surfaceRect, palette.surfaceColor); + + if (state.surfaceHovered) { + drawList.AddFilledRect(layout.inputRect, palette.surfaceHoverOverlayColor); + } + if (state.surfaceActive) { + drawList.AddFilledRect(layout.inputRect, palette.surfaceActiveOverlayColor); + } + if (state.inputCaptured) { + drawList.AddFilledRect(layout.inputRect, palette.captureOverlayColor); + } + + drawList.AddRectOutline( + layout.inputRect, + ResolveSurfaceBorderColor(state, palette), + ResolveSurfaceBorderThickness(state, metrics)); + + for (std::size_t index = 0u; index < toolItems.size(); ++index) { + if (!HasArea(layout.toolItemRects[index])) { + continue; + } + + const bool hovered = state.hoveredToolIndex == index; + const bool active = state.activeToolIndex == index; + drawList.AddFilledRect( + layout.toolItemRects[index], + ResolveToolFillColor(toolItems[index], hovered, active, palette), + metrics.toolCornerRounding); + drawList.AddRectOutline( + layout.toolItemRects[index], + palette.toolBorderColor, + 1.0f, + metrics.toolCornerRounding); + } + + if (layout.hasBottomBar) { + AppendUIEditorStatusBarBackground( + drawList, + layout.statusBarLayout, + statusSegments, + state.statusBarState, + palette.statusBarPalette); + } +} + +void AppendUIEditorViewportSlotForeground( + UIDrawList& drawList, + const UIEditorViewportSlotLayout& layout, + const UIEditorViewportSlotChrome& chrome, + const UIEditorViewportSlotFrame& frame, + const std::vector& toolItems, + const std::vector& statusSegments, + const UIEditorViewportSlotState& state, + const UIEditorViewportSlotPalette& palette, + const UIEditorViewportSlotMetrics& metrics) { + if (layout.hasTopBar) { + if (!chrome.title.empty()) { + drawList.AddText( + UIPoint(layout.titleRect.x, layout.topBarRect.y + metrics.titleInsetY), + std::string(chrome.title), + palette.textPrimary, + 15.0f); + } + + if (!chrome.subtitle.empty()) { + drawList.AddText( + UIPoint(layout.subtitleRect.x, layout.topBarRect.y + metrics.subtitleInsetY), + std::string(chrome.subtitle), + palette.textSecondary, + 11.0f); + } + + for (std::size_t index = 0u; index < toolItems.size(); ++index) { + if (!HasArea(layout.toolItemRects[index]) || toolItems[index].label.empty()) { + continue; + } + + drawList.AddText( + UIPoint( + layout.toolItemRects[index].x + metrics.toolPaddingX, + layout.toolItemRects[index].y + 5.0f), + toolItems[index].label, + toolItems[index].enabled ? palette.textPrimary : palette.textMuted, + 12.0f); + } + } + + const UISize frameSize = ResolveFrameAspectSize(frame, layout.requestedSurfaceSize); + if (frame.hasTexture && frame.texture.IsValid()) { + drawList.AddImage(layout.textureRect, frame.texture, palette.imageTint); + drawList.AddText( + UIPoint(layout.inputRect.x + 14.0f, layout.inputRect.y + 14.0f), + "Texture " + + std::to_string(static_cast(frameSize.width)) + + "x" + + std::to_string(static_cast(frameSize.height)), + palette.textMuted, + 12.0f); + } else { + const std::string statusText = frame.statusText.empty() + ? std::string("Viewport is waiting for frame") + : frame.statusText; + drawList.AddText( + UIPoint(layout.inputRect.x + 16.0f, layout.inputRect.y + 18.0f), + statusText, + palette.textPrimary, + 14.0f); + drawList.AddText( + UIPoint(layout.inputRect.x + 16.0f, layout.inputRect.y + 42.0f), + "Requested surface: " + + std::to_string(static_cast(layout.requestedSurfaceSize.width)) + + "x" + + std::to_string(static_cast(layout.requestedSurfaceSize.height)), + palette.textMuted, + 12.0f); + } + + if (layout.hasBottomBar) { + AppendUIEditorStatusBarForeground( + drawList, + layout.statusBarLayout, + statusSegments, + state.statusBarState, + palette.statusBarPalette); + } +} + +void AppendUIEditorViewportSlot( + UIDrawList& drawList, + const UIRect& bounds, + const UIEditorViewportSlotChrome& chrome, + const UIEditorViewportSlotFrame& frame, + const std::vector& toolItems, + const std::vector& statusSegments, + const UIEditorViewportSlotState& state, + const UIEditorViewportSlotPalette& palette, + const UIEditorViewportSlotMetrics& metrics) { + const UIEditorViewportSlotLayout layout = + BuildUIEditorViewportSlotLayout( + bounds, + chrome, + frame, + toolItems, + statusSegments, + metrics); + AppendUIEditorViewportSlotBackground( + drawList, + layout, + toolItems, + statusSegments, + state, + palette, + metrics); + AppendUIEditorViewportSlotForeground( + drawList, + layout, + chrome, + frame, + toolItems, + statusSegments, + state, + palette, + metrics); +} + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/tests/UI/Editor/integration/CMakeLists.txt b/tests/UI/Editor/integration/CMakeLists.txt index f9050de4..2ed4c9a1 100644 --- a/tests/UI/Editor/integration/CMakeLists.txt +++ b/tests/UI/Editor/integration/CMakeLists.txt @@ -24,6 +24,11 @@ if(TARGET editor_ui_context_menu_basic_validation) editor_ui_context_menu_basic_validation) endif() +if(TARGET editor_ui_viewport_slot_basic_validation) + list(APPEND EDITOR_UI_INTEGRATION_TARGETS + editor_ui_viewport_slot_basic_validation) +endif() + add_custom_target(editor_ui_integration_tests DEPENDS ${EDITOR_UI_INTEGRATION_TARGETS} diff --git a/tests/UI/Editor/integration/README.md b/tests/UI/Editor/integration/README.md index 6e1d0a66..8aa1f835 100644 --- a/tests/UI/Editor/integration/README.md +++ b/tests/UI/Editor/integration/README.md @@ -18,6 +18,7 @@ Layout: - `shell/panel_frame_basic/`: panel frame layout/state/hit-test only - `shell/status_bar_basic/`: status bar slot/segment/hit-test only - `shell/tab_strip_basic/`: tab strip layout/state/hit-test/close/navigation only +- `shell/viewport_slot_basic/`: viewport shell chrome/surface/status only - `state/panel_session_flow/`: panel session state flow only - `state/layout_persistence/`: layout save/load/reset only - `state/shortcut_dispatch/`: shortcut match/suppression/dispatch only @@ -54,6 +55,11 @@ Scenarios: Executable: `XCUIEditorTabStripBasicValidation.exe` Scope: tab header layout, selected/hover/focus, close hit target, close fallback, Left/Right/Home/End navigation only +- `editor.shell.viewport_slot_basic` + Build target: `editor_ui_viewport_slot_basic_validation` + Executable: `XCUIEditorViewportSlotBasicValidation.exe` + Scope: viewport top bar / surface / status bar layout, hover/focus/active/capture, texture vs fallback only + - `editor.state.panel_session_flow` Build target: `editor_ui_panel_session_flow_validation` Executable: `XCUIEditorPanelSessionFlowValidation.exe` @@ -100,6 +106,9 @@ Selected controls: - `shell/tab_strip_basic/` Click `Document A / B / C`, click `X` on closable tabs, click content to focus, press `Left / Right / Home / End`, press `Reset`, press `F12`. +- `shell/viewport_slot_basic/` + Hover toolbar / surface / status bar, click surface to focus, hold and release left mouse to inspect capture, toggle `TopBar / 状态条 / Texture / 方形比例`, press `F12`. + - `state/panel_session_flow/` Click `Hide Active / Show Doc A / Close Doc B / Open Doc B / Activate Details / Reset`, press `F12`. diff --git a/tests/UI/Editor/integration/shell/CMakeLists.txt b/tests/UI/Editor/integration/shell/CMakeLists.txt index be8a3e39..44169b5c 100644 --- a/tests/UI/Editor/integration/shell/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/CMakeLists.txt @@ -10,3 +10,6 @@ endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/context_menu_basic/CMakeLists.txt") add_subdirectory(context_menu_basic) endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/viewport_slot_basic/CMakeLists.txt") + add_subdirectory(viewport_slot_basic) +endif() diff --git a/tests/UI/Editor/integration/shell/viewport_slot_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/viewport_slot_basic/CMakeLists.txt new file mode 100644 index 00000000..feff4f18 --- /dev/null +++ b/tests/UI/Editor/integration/shell/viewport_slot_basic/CMakeLists.txt @@ -0,0 +1,30 @@ +add_executable(editor_ui_viewport_slot_basic_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_viewport_slot_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/new_editor/include + ${CMAKE_SOURCE_DIR}/new_editor/app + ${CMAKE_SOURCE_DIR}/engine/include +) + +target_compile_definitions(editor_ui_viewport_slot_basic_validation PRIVATE + UNICODE + _UNICODE + XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}" +) + +if(MSVC) + target_compile_options(editor_ui_viewport_slot_basic_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_viewport_slot_basic_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_viewport_slot_basic_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_viewport_slot_basic_validation PROPERTIES + OUTPUT_NAME "XCUIEditorViewportSlotBasicValidation" +) diff --git a/tests/UI/Editor/integration/shell/viewport_slot_basic/captures/.gitkeep b/tests/UI/Editor/integration/shell/viewport_slot_basic/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/viewport_slot_basic/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/viewport_slot_basic/main.cpp b/tests/UI/Editor/integration/shell/viewport_slot_basic/main.cpp new file mode 100644 index 00000000..25011c74 --- /dev/null +++ b/tests/UI/Editor/integration/shell/viewport_slot_basic/main.cpp @@ -0,0 +1,741 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include "Host/AutoScreenshot.h" +#include "Host/NativeRenderer.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#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::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::UISize; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotBackground; +using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotForeground; +using XCEngine::UI::Editor::Widgets::BuildUIEditorViewportSlotLayout; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorViewportSlot; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotChrome; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotFrame; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTarget; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotInvalidIndex; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotLayout; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotState; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorViewportSlotBasicValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ViewportSlot Basic"; + +constexpr UIColor kWindowBg(0.12f, 0.12f, 0.12f, 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 kButtonBg(0.25f, 0.25f, 0.25f, 1.0f); +constexpr UIColor kButtonOnBg(0.39f, 0.39f, 0.39f, 1.0f); +constexpr UIColor kButtonBorder(0.47f, 0.47f, 0.47f, 1.0f); +constexpr UIColor kPreviewBg(0.15f, 0.15f, 0.15f, 1.0f); + +enum class ActionId : unsigned char { + ToggleTopBar = 0, + ToggleBottomBar, + ToggleTexture, + ToggleSquareAspect, + Reset, + Capture +}; + +struct ButtonState { + ActionId action = ActionId::ToggleTopBar; + std::string label = {}; + UIRect rect = {}; + bool selected = 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; +} + +std::string BoolText(bool value) { + return value ? "On" : "Off"; +} + +std::string DescribeHitTarget(const UIEditorViewportSlotHitTarget& hit) { + switch (hit.kind) { + case UIEditorViewportSlotHitTargetKind::ToolItem: + return "ToolItem[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::Surface: + return "Surface"; + case UIEditorViewportSlotHitTargetKind::StatusSegment: + return "StatusSegment[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::StatusSeparator: + return "StatusSeparator[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::TopBar: + return "TopBar"; + case UIEditorViewportSlotHitTargetKind::Title: + return "Title"; + case UIEditorViewportSlotHitTargetKind::BottomBar: + return "BottomBar"; + case UIEditorViewportSlotHitTargetKind::None: + default: + return "None"; + } +} + +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.selected ? kButtonOnBg : kButtonBg, 8.0f); + drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f); + drawList.AddText( + UIPoint(button.rect.x + 12.0f, button.rect.y + 10.0f), + button.label, + kTextPrimary, + 12.0f); +} + +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(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove( + static_cast(GET_X_LPARAM(lParam)), + static_cast(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(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleLeftButtonUp( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + case WM_CAPTURECHANGED: + if (app != nullptr) { + app->m_slotState.inputCaptured = false; + return 0; + } + break; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr && wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + InvalidateRect(hwnd, nullptr, FALSE); + UpdateWindow(hwnd); + 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: + PostQuitMessage(0); + return 0; + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/viewport_slot_basic/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, + 1520, + 940, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + ResetState(); + 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; + } + } + + void ResetState() { + m_showTopBar = true; + m_showBottomBar = true; + m_textureEnabled = true; + m_squareAspect = false; + m_slotState = {}; + m_slotState.focused = true; + m_slotState.surfaceActive = true; + m_lastHover = {}; + m_lastResult = "Ready"; + } + + UIEditorViewportSlotChrome BuildChrome() const { + UIEditorViewportSlotChrome chrome = {}; + chrome.title = "Scene View"; + chrome.subtitle = "ViewportSlot 基础壳层"; + chrome.showTopBar = m_showTopBar; + chrome.showBottomBar = m_showBottomBar; + chrome.topBarHeight = 40.0f; + chrome.bottomBarHeight = 28.0f; + return chrome; + } + + UIEditorViewportSlotFrame BuildFrame() const { + UIEditorViewportSlotFrame frame = {}; + if (m_textureEnabled) { + const unsigned int width = m_squareAspect ? 1024u : 1280u; + const unsigned int height = m_squareAspect ? 1024u : 720u; + frame.hasTexture = true; + frame.texture = { 1u, width, height }; + frame.presentedSize = UISize(static_cast(width), static_cast(height)); + frame.statusText = "Fake viewport frame"; + } else { + frame.hasTexture = false; + frame.statusText = "当前无纹理,检查 fallback message 与 input 边界。"; + } + return frame; + } + + std::vector BuildToolItems() const { + return { + { "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 98.0f }, + { "lit", "Lit", UIEditorViewportSlotToolSlot::Trailing, true, m_textureEnabled, 48.0f }, + { "gizmos", "Gizmos", UIEditorViewportSlotToolSlot::Trailing, true, true, 72.0f } + }; + } + + std::vector BuildStatusSegments() const { + return { + { "scene", "Scene", UIEditorStatusBarSlot::Leading, {}, true, true, 72.0f }, + { "resolution", m_squareAspect ? "1024x1024" : "1280x720", UIEditorStatusBarSlot::Leading, {}, true, false, 92.0f }, + { "capture", std::string("Capture ") + BoolText(m_slotState.inputCaptured), UIEditorStatusBarSlot::Trailing, {}, true, true, 104.0f }, + { "focus", std::string("Focus ") + BoolText(m_slotState.focused), UIEditorStatusBarSlot::Trailing, {}, true, false, 88.0f } + }; + } + + void OnResize(UINT width, UINT height) { + m_renderer.Resize(width, height); + if (width > 0u && height > 0u) { + UpdateLayoutForCurrentWindow(); + } + } + + void UpdateLayoutForCurrentWindow() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + + const float leftColumnWidth = 420.0f; + const float outerPadding = 20.0f; + m_introRect = UIRect(outerPadding, outerPadding, leftColumnWidth, 224.0f); + m_controlsRect = UIRect(outerPadding, 264.0f, leftColumnWidth, 324.0f); + m_stateRect = UIRect(outerPadding, 608.0f, leftColumnWidth, height - 628.0f); + m_previewRect = UIRect( + leftColumnWidth + outerPadding * 2.0f, + outerPadding, + width - leftColumnWidth - outerPadding * 3.0f, + height - outerPadding * 2.0f); + m_slotRect = UIRect( + m_previewRect.x + 18.0f, + m_previewRect.y + 18.0f, + m_previewRect.width - 36.0f, + m_previewRect.height - 36.0f); + + BuildButtons(); + m_layout = BuildUIEditorViewportSlotLayout( + m_slotRect, + BuildChrome(), + BuildFrame(), + BuildToolItems(), + BuildStatusSegments()); + UpdateHoverState(); + } + + void BuildButtons() { + const float buttonHeight = 34.0f; + const float gap = 10.0f; + const float left = m_controlsRect.x + 16.0f; + const float top = m_controlsRect.y + 54.0f; + const float width = m_controlsRect.width - 32.0f; + m_buttons = { + { ActionId::ToggleTopBar, "TopBar", UIRect(left, top, width, buttonHeight), m_showTopBar }, + { ActionId::ToggleBottomBar, "状态条", UIRect(left, top + (buttonHeight + gap), width, buttonHeight), m_showBottomBar }, + { ActionId::ToggleTexture, "Texture", UIRect(left, top + (buttonHeight + gap) * 2.0f, width, buttonHeight), m_textureEnabled }, + { ActionId::ToggleSquareAspect, "方形比例", UIRect(left, top + (buttonHeight + gap) * 3.0f, width, buttonHeight), m_squareAspect }, + { ActionId::Reset, "Reset", UIRect(left, top + (buttonHeight + gap) * 4.0f, width, buttonHeight), false }, + { ActionId::Capture, "截图", UIRect(left, top + (buttonHeight + gap) * 5.0f, width, buttonHeight), false } + }; + } + + void UpdateHoverState() { + m_lastHover = HitTestUIEditorViewportSlot(m_layout, m_mousePosition); + m_slotState.hoveredToolIndex = UIEditorViewportSlotInvalidIndex; + m_slotState.statusBarState.hoveredIndex = UIEditorViewportSlotInvalidIndex; + m_slotState.surfaceHovered = false; + + switch (m_lastHover.kind) { + case UIEditorViewportSlotHitTargetKind::ToolItem: + m_slotState.hoveredToolIndex = m_lastHover.index; + break; + case UIEditorViewportSlotHitTargetKind::StatusSegment: + m_slotState.statusBarState.hoveredIndex = m_lastHover.index; + break; + case UIEditorViewportSlotHitTargetKind::Surface: + m_slotState.surfaceHovered = true; + break; + default: + break; + } + + m_slotState.statusBarState.focused = m_slotState.focused; + } + + void InvalidateScenario() { + if (m_hwnd != nullptr) { + InvalidateRect(m_hwnd, nullptr, FALSE); + } + } + + void HandleMouseMove(float x, float y) { + m_mousePosition = UIPoint(x, y); + TRACKMOUSEEVENT event = {}; + event.cbSize = sizeof(event); + event.dwFlags = TME_LEAVE; + event.hwndTrack = m_hwnd; + TrackMouseEvent(&event); + UpdateHoverState(); + InvalidateScenario(); + } + + void HandleMouseLeave() { + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + UpdateHoverState(); + InvalidateScenario(); + } + + void HandleLeftButtonDown(float x, float y) { + SetFocus(m_hwnd); + m_mousePosition = UIPoint(x, y); + UpdateHoverState(); + + const UIEditorViewportSlotHitTarget hit = + HitTestUIEditorViewportSlot(m_layout, UIPoint(x, y)); + if (hit.kind == UIEditorViewportSlotHitTargetKind::Surface) { + SetCapture(m_hwnd); + m_slotState.focused = true; + m_slotState.surfaceActive = true; + m_slotState.inputCaptured = true; + m_lastResult = "Surface 按下:focus + active + capture"; + } + InvalidateScenario(); + } + + void ExecuteAction(ActionId action) { + switch (action) { + case ActionId::ToggleTopBar: + m_showTopBar = !m_showTopBar; + m_lastResult = m_showTopBar ? "TopBar 已打开" : "TopBar 已关闭"; + break; + case ActionId::ToggleBottomBar: + m_showBottomBar = !m_showBottomBar; + m_lastResult = m_showBottomBar ? "状态条已打开" : "状态条已关闭"; + break; + case ActionId::ToggleTexture: + m_textureEnabled = !m_textureEnabled; + m_lastResult = m_textureEnabled ? "Texture 分支已打开" : "Fallback 分支已打开"; + break; + case ActionId::ToggleSquareAspect: + m_squareAspect = !m_squareAspect; + m_lastResult = m_squareAspect ? "切到 1:1 比例" : "切到 16:9 比例"; + break; + case ActionId::Reset: + ResetState(); + m_lastResult = "状态已重置"; + break; + case ActionId::Capture: + m_autoScreenshot.RequestCapture("manual_button"); + InvalidateRect(m_hwnd, nullptr, FALSE); + UpdateWindow(m_hwnd); + m_lastResult = "截图已排队"; + break; + } + + UpdateLayoutForCurrentWindow(); + } + + void HandleLeftButtonUp(float x, float y) { + if (GetCapture() == m_hwnd) { + ReleaseCapture(); + } + m_slotState.inputCaptured = false; + m_mousePosition = UIPoint(x, y); + + for (const ButtonState& button : m_buttons) { + if (ContainsPoint(button.rect, x, y)) { + ExecuteAction(button.action); + return; + } + } + + const UIEditorViewportSlotHitTarget hit = + HitTestUIEditorViewportSlot(m_layout, UIPoint(x, y)); + switch (hit.kind) { + case UIEditorViewportSlotHitTargetKind::ToolItem: + m_slotState.activeToolIndex = hit.index; + m_lastResult = "ToolItem 命中:" + std::to_string(hit.index); + break; + case UIEditorViewportSlotHitTargetKind::StatusSegment: + m_slotState.statusBarState.activeIndex = hit.index; + m_lastResult = "StatusSegment 命中:" + std::to_string(hit.index); + break; + case UIEditorViewportSlotHitTargetKind::Surface: + m_slotState.focused = true; + m_slotState.surfaceActive = true; + m_lastResult = "Surface 激活,capture 已释放"; + break; + default: + m_slotState.focused = false; + m_slotState.surfaceActive = false; + m_slotState.activeToolIndex = UIEditorViewportSlotInvalidIndex; + m_slotState.statusBarState.activeIndex = UIEditorViewportSlotInvalidIndex; + m_lastResult = "点击外部,focus / active 已清除"; + break; + } + + UpdateHoverState(); + InvalidateScenario(); + } + + void RenderFrame() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + + UpdateLayoutForCurrentWindow(); + const auto chrome = BuildChrome(); + const auto frame = BuildFrame(); + const auto toolItems = BuildToolItems(); + const auto statusSegments = BuildStatusSegments(); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("ViewportSlotBasic"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + + DrawCard( + drawList, + m_introRect, + "测试功能:ViewportSlot 基础壳层", + "只验证 Editor 基础层。"); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f), + "检查1:TopBar / Surface / 状态条布局。", + kTextMuted, + 12.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 88.0f), + "检查2:hover / focus / active / capture。", + kTextMuted, + 12.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 110.0f), + "操作1:hover toolbar / surface / status bar。", + kTextMuted, + 12.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 132.0f), + "操作2:click surface;按住左键观察 capture。", + kTextMuted, + 12.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 154.0f), + "操作3:切换 TopBar / 状态条 / Texture / 比例;F12 或“截图”。", + kTextMuted, + 12.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 180.0f), + "结果:chrome 开关后 surface 扩张;Texture/Fallback 正确。", + kTextWeak, + 12.0f); + + DrawCard(drawList, m_controlsRect, "开关", "只保留和 ViewportSlot contract 直接相关的操作。"); + for (const ButtonState& button : m_buttons) { + DrawButton(drawList, button); + } + + DrawCard(drawList, m_stateRect, "状态", "实时显示 hover / focus / active / capture 与截图状态。"); + drawList.AddText( + UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 66.0f), + "Hover: " + DescribeHitTarget(m_lastHover), + kTextPrimary, + 13.0f); + drawList.AddText( + UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 92.0f), + "Focus: " + BoolText(m_slotState.focused), + kTextPrimary, + 13.0f); + drawList.AddText( + UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 118.0f), + "Active: " + BoolText(m_slotState.surfaceActive), + kTextPrimary, + 13.0f); + drawList.AddText( + UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 144.0f), + "Capture: " + BoolText(m_slotState.inputCaptured), + kTextPrimary, + 13.0f); + drawList.AddText( + UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 170.0f), + "Chrome: TopBar " + BoolText(m_showTopBar) + " | 状态条 " + BoolText(m_showBottomBar), + kTextPrimary, + 13.0f); + drawList.AddText( + UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 196.0f), + "Frame: " + std::string(m_textureEnabled ? (m_squareAspect ? "1024x1024" : "1280x720") : "Fallback"), + kTextPrimary, + 13.0f); + drawList.AddText( + UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 222.0f), + "Result: " + m_lastResult, + kTextMuted, + 12.0f); + const std::string captureSummary = + m_autoScreenshot.HasPendingCapture() + ? "截图排队中..." + : (m_autoScreenshot.GetLastCaptureSummary().empty() + ? std::string("截图:F12 或按钮 -> viewport_slot_basic/captures/") + : m_autoScreenshot.GetLastCaptureSummary()); + drawList.AddText( + UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 248.0f), + captureSummary, + kTextWeak, + 12.0f); + + DrawCard(drawList, m_previewRect, "Preview", "这里只放一个 ViewportSlot,专门检查基础壳层。"); + drawList.AddFilledRect( + UIRect( + m_previewRect.x + 12.0f, + m_previewRect.y + 44.0f, + m_previewRect.width - 24.0f, + m_previewRect.height - 56.0f), + kPreviewBg, + 10.0f); + + m_layout = BuildUIEditorViewportSlotLayout( + m_slotRect, + chrome, + frame, + toolItems, + statusSegments); + UpdateHoverState(); + + AppendUIEditorViewportSlotBackground( + drawList, + m_layout, + toolItems, + statusSegments, + m_slotState); + AppendUIEditorViewportSlotForeground( + drawList, + m_layout, + chrome, + frame, + toolItems, + statusSegments, + m_slotState); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); + } + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + std::filesystem::path m_captureRoot = {}; + std::vector m_buttons = {}; + UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); + UIEditorViewportSlotState m_slotState = {}; + UIEditorViewportSlotHitTarget m_lastHover = {}; + UIEditorViewportSlotLayout m_layout = {}; + UIRect m_introRect = {}; + UIRect m_controlsRect = {}; + UIRect m_stateRect = {}; + UIRect m_previewRect = {}; + UIRect m_slotRect = {}; + bool m_showTopBar = true; + bool m_showBottomBar = true; + bool m_textureEnabled = true; + bool m_squareAspect = false; + std::string m_lastResult = {}; +}; + +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { + return ScenarioApp().Run(hInstance, nCmdShow); +} diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index b6918e81..44970007 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -15,6 +15,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_panel_frame.cpp test_ui_editor_status_bar.cpp test_ui_editor_tab_strip.cpp + test_ui_editor_viewport_slot.cpp test_ui_editor_shortcut_manager.cpp test_ui_editor_workspace_controller.cpp test_ui_editor_workspace_layout_persistence.cpp diff --git a/tests/UI/Editor/unit/test_ui_editor_viewport_slot.cpp b/tests/UI/Editor/unit/test_ui_editor_viewport_slot.cpp new file mode 100644 index 00000000..3dac0c8b --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_viewport_slot.cpp @@ -0,0 +1,281 @@ +#include + +#include +#include + +namespace { + +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawCommand; +using XCEngine::UI::UIDrawCommandType; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::UISize; +using XCEngine::UI::UITextureHandle; +using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotBackground; +using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotForeground; +using XCEngine::UI::Editor::Widgets::BuildUIEditorViewportSlotLayout; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorViewportSlot; +using XCEngine::UI::Editor::Widgets::ResolveUIEditorViewportSlotDesiredToolWidth; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotChrome; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotFrame; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotInvalidIndex; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotLayout; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotPalette; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotState; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot; + +void ExpectColorEq(const UIColor& actual, const UIColor& expected) { + EXPECT_FLOAT_EQ(actual.r, expected.r); + EXPECT_FLOAT_EQ(actual.g, expected.g); + EXPECT_FLOAT_EQ(actual.b, expected.b); + EXPECT_FLOAT_EQ(actual.a, expected.a); +} + +bool RectEq(const UIRect& actual, const UIRect& expected) { + return actual.x == expected.x && + actual.y == expected.y && + actual.width == expected.width && + actual.height == expected.height; +} + +const UIDrawCommand* FindCommand( + const UIDrawList& drawList, + UIDrawCommandType type, + const UIRect& rect) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + if (command.type == type && RectEq(command.rect, rect)) { + return &command; + } + } + + return nullptr; +} + +bool ContainsText(const UIDrawList& drawList, std::string_view text) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + if (command.type == UIDrawCommandType::Text && command.text == text) { + return true; + } + } + + return false; +} + +std::vector BuildToolItems() { + return { + { "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 96.0f }, + { "lit", "Lit", UIEditorViewportSlotToolSlot::Trailing, true, false, 48.0f }, + { "gizmos", "Gizmos", UIEditorViewportSlotToolSlot::Trailing, true, false, 72.0f } + }; +} + +std::vector BuildStatusSegments() { + return { + { "scene", "Scene", UIEditorStatusBarSlot::Leading, {}, true, true, 72.0f }, + { "resolution", "1280x720", UIEditorStatusBarSlot::Leading, {}, true, false, 92.0f }, + { "frame", "16.7 ms", UIEditorStatusBarSlot::Trailing, {}, true, false, 72.0f } + }; +} + +TEST(UIEditorViewportSlotTest, DesiredToolWidthUsesExplicitValueBeforeEstimatedLabelWidth) { + UIEditorViewportSlotToolItem explicitWidth = {}; + explicitWidth.label = "Scene"; + explicitWidth.desiredWidth = 88.0f; + + UIEditorViewportSlotToolItem inferredWidth = {}; + inferredWidth.label = "Scene"; + + EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(explicitWidth), 88.0f); + EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(inferredWidth), 55.0f); +} + +TEST(UIEditorViewportSlotTest, LayoutBuildsTopBarSurfaceBottomBarAndAspectFittedTexture) { + UIEditorViewportSlotChrome chrome = {}; + chrome.topBarHeight = 40.0f; + chrome.bottomBarHeight = 28.0f; + + UIEditorViewportSlotFrame frame = {}; + frame.hasTexture = true; + frame.presentedSize = UISize(1600.0f, 900.0f); + + const UIEditorViewportSlotLayout layout = + BuildUIEditorViewportSlotLayout( + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + chrome, + frame, + BuildToolItems(), + BuildStatusSegments()); + + EXPECT_TRUE(layout.hasTopBar); + EXPECT_TRUE(layout.hasBottomBar); + EXPECT_FLOAT_EQ(layout.topBarRect.y, 20.0f); + EXPECT_FLOAT_EQ(layout.topBarRect.height, 40.0f); + EXPECT_FLOAT_EQ(layout.bottomBarRect.y, 592.0f); + EXPECT_FLOAT_EQ(layout.bottomBarRect.height, 28.0f); + EXPECT_FLOAT_EQ(layout.surfaceRect.y, 60.0f); + EXPECT_FLOAT_EQ(layout.surfaceRect.height, 532.0f); + EXPECT_FLOAT_EQ(layout.requestedSurfaceSize.width, 800.0f); + EXPECT_FLOAT_EQ(layout.requestedSurfaceSize.height, 532.0f); + EXPECT_FLOAT_EQ(layout.textureRect.x, 10.0f); + EXPECT_FLOAT_EQ(layout.textureRect.y, 101.0f); + EXPECT_FLOAT_EQ(layout.textureRect.width, 800.0f); + EXPECT_FLOAT_EQ(layout.textureRect.height, 450.0f); +} + +TEST(UIEditorViewportSlotTest, ToolItemsAlignToEdgesAndTitleRectClampsBetweenToolBands) { + UIEditorViewportSlotChrome chrome = {}; + chrome.topBarHeight = 40.0f; + chrome.bottomBarHeight = 0.0f; + chrome.showBottomBar = false; + + const UIEditorViewportSlotLayout layout = + BuildUIEditorViewportSlotLayout( + UIRect(0.0f, 0.0f, 900.0f, 520.0f), + chrome, + UIEditorViewportSlotFrame{}, + BuildToolItems(), + {}); + + EXPECT_FLOAT_EQ(layout.toolItemRects[0].x, 12.0f); + EXPECT_FLOAT_EQ(layout.toolItemRects[0].width, 96.0f); + EXPECT_FLOAT_EQ(layout.toolItemRects[1].x, 762.0f); + EXPECT_FLOAT_EQ(layout.toolItemRects[2].x, 816.0f); + EXPECT_FLOAT_EQ(layout.titleRect.x, 118.0f); + EXPECT_FLOAT_EQ(layout.titleRect.width, 634.0f); +} + +TEST(UIEditorViewportSlotTest, HitTestPrioritizesToolThenStatusThenSurface) { + UIEditorViewportSlotChrome chrome = {}; + chrome.topBarHeight = 40.0f; + chrome.bottomBarHeight = 28.0f; + + const UIEditorViewportSlotLayout layout = + BuildUIEditorViewportSlotLayout( + UIRect(0.0f, 0.0f, 900.0f, 520.0f), + chrome, + UIEditorViewportSlotFrame{}, + BuildToolItems(), + BuildStatusSegments()); + + auto hit = HitTestUIEditorViewportSlot(layout, UIPoint(30.0f, 16.0f)); + EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::ToolItem); + EXPECT_EQ(hit.index, 0u); + + hit = HitTestUIEditorViewportSlot(layout, UIPoint(22.0f, 505.0f)); + EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::StatusSegment); + EXPECT_EQ(hit.index, 0u); + + hit = HitTestUIEditorViewportSlot(layout, UIPoint(450.0f, 240.0f)); + EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::Surface); + EXPECT_EQ(hit.index, UIEditorViewportSlotInvalidIndex); +} + +TEST(UIEditorViewportSlotTest, BackgroundAndForegroundEmitChromeAndImageBranchCommands) { + UIEditorViewportSlotChrome chrome = {}; + chrome.title = "Scene View"; + chrome.subtitle = "ViewportSlot shell"; + + UIEditorViewportSlotFrame frame = {}; + frame.hasTexture = true; + frame.texture = UITextureHandle{ 1u, 1280u, 720u }; + frame.presentedSize = UISize(1280.0f, 720.0f); + + UIEditorViewportSlotState state = {}; + state.focused = true; + state.surfaceHovered = true; + state.surfaceActive = true; + state.inputCaptured = true; + state.hoveredToolIndex = 0u; + state.activeToolIndex = 1u; + state.statusBarState.focused = true; + + const auto toolItems = BuildToolItems(); + const auto statusSegments = BuildStatusSegments(); + const UIEditorViewportSlotLayout layout = + BuildUIEditorViewportSlotLayout( + UIRect(12.0f, 16.0f, 900.0f, 520.0f), + chrome, + frame, + toolItems, + statusSegments); + + const UIEditorViewportSlotPalette palette = {}; + + UIDrawList background("ViewportSlotBackground"); + AppendUIEditorViewportSlotBackground( + background, + layout, + toolItems, + statusSegments, + state, + palette); + + const UIDrawCommand* surfaceBorder = + FindCommand(background, UIDrawCommandType::RectOutline, layout.inputRect); + ASSERT_NE(surfaceBorder, nullptr); + ExpectColorEq(surfaceBorder->color, palette.surfaceCapturedBorderColor); + + UIDrawList foreground("ViewportSlotForeground"); + AppendUIEditorViewportSlotForeground( + foreground, + layout, + chrome, + frame, + toolItems, + statusSegments, + state, + palette); + + EXPECT_TRUE(ContainsText(foreground, "Scene View")); + EXPECT_TRUE(ContainsText(foreground, "Perspective")); + EXPECT_TRUE(ContainsText(foreground, "Texture 1280x720")); + + bool foundImage = false; + for (const UIDrawCommand& command : foreground.GetCommands()) { + if (command.type == UIDrawCommandType::Image && + RectEq(command.rect, layout.textureRect)) { + foundImage = true; + break; + } + } + EXPECT_TRUE(foundImage); +} + +TEST(UIEditorViewportSlotTest, ForegroundFallsBackToStatusTextWhenTextureIsUnavailable) { + UIEditorViewportSlotChrome chrome = {}; + chrome.title = "Game View"; + + UIEditorViewportSlotFrame frame = {}; + frame.statusText = "Viewport is waiting for frame"; + + const auto toolItems = BuildToolItems(); + const auto statusSegments = BuildStatusSegments(); + const UIEditorViewportSlotLayout layout = + BuildUIEditorViewportSlotLayout( + UIRect(0.0f, 0.0f, 720.0f, 420.0f), + chrome, + frame, + toolItems, + statusSegments); + + UIDrawList foreground("ViewportSlotForegroundFallback"); + AppendUIEditorViewportSlotForeground( + foreground, + layout, + chrome, + frame, + toolItems, + statusSegments, + UIEditorViewportSlotState{}); + + EXPECT_TRUE(ContainsText(foreground, "Viewport is waiting for frame")); + EXPECT_FALSE(ContainsText(foreground, "Texture 1280x720")); +} + +} // namespace