Refine XCEditor docking and DPI rendering
This commit is contained in:
@@ -22,6 +22,7 @@ endfunction()
|
||||
set(XCUI_EDITOR_FOUNDATION_SOURCES
|
||||
src/Foundation/UIEditorCommandDispatcher.cpp
|
||||
src/Foundation/UIEditorCommandRegistry.cpp
|
||||
src/Foundation/UIEditorRuntimeTrace.cpp
|
||||
src/Foundation/UIEditorShortcutManager.cpp
|
||||
src/Foundation/UIEditorTheme.cpp
|
||||
)
|
||||
@@ -129,6 +130,7 @@ add_library(XCUIEditorHost STATIC
|
||||
target_include_directories(XCUIEditorHost
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ namespace XCEngine::UI::Editor::Host {
|
||||
|
||||
namespace {
|
||||
|
||||
D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect) {
|
||||
return D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
|
||||
}
|
||||
constexpr float kBaseDpi = 96.0f;
|
||||
constexpr float kDefaultFontSize = 16.0f;
|
||||
|
||||
std::string HrToString(const char* operation, HRESULT hr) {
|
||||
char buffer[128] = {};
|
||||
@@ -18,6 +17,27 @@ std::string HrToString(const char* operation, HRESULT hr) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
float ClampDpiScale(float dpiScale) {
|
||||
return dpiScale > 0.0f ? dpiScale : 1.0f;
|
||||
}
|
||||
|
||||
float ResolveFontSize(float fontSize) {
|
||||
return fontSize > 0.0f ? fontSize : kDefaultFontSize;
|
||||
}
|
||||
|
||||
float SnapToPixel(float value, float dpiScale) {
|
||||
const float scale = ClampDpiScale(dpiScale);
|
||||
return std::round(value * scale);
|
||||
}
|
||||
|
||||
D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect, float dpiScale) {
|
||||
const float left = SnapToPixel(rect.x, dpiScale);
|
||||
const float top = SnapToPixel(rect.y, dpiScale);
|
||||
const float right = SnapToPixel(rect.x + rect.width, dpiScale);
|
||||
const float bottom = SnapToPixel(rect.y + rect.height, dpiScale);
|
||||
return D2D1::RectF(left, top, right, bottom);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool NativeRenderer::Initialize(HWND hwnd) {
|
||||
@@ -64,6 +84,17 @@ void NativeRenderer::Shutdown() {
|
||||
m_hwnd = nullptr;
|
||||
}
|
||||
|
||||
void NativeRenderer::SetDpiScale(float dpiScale) {
|
||||
m_dpiScale = ClampDpiScale(dpiScale);
|
||||
if (m_renderTarget) {
|
||||
m_renderTarget->SetDpi(kBaseDpi, kBaseDpi);
|
||||
}
|
||||
}
|
||||
|
||||
float NativeRenderer::GetDpiScale() const {
|
||||
return m_dpiScale;
|
||||
}
|
||||
|
||||
void NativeRenderer::Resize(UINT width, UINT height) {
|
||||
if (!m_renderTarget || width == 0 || height == 0) {
|
||||
return;
|
||||
@@ -104,6 +135,52 @@ const std::string& NativeRenderer::GetLastRenderError() const {
|
||||
return m_lastRenderError;
|
||||
}
|
||||
|
||||
float NativeRenderer::MeasureTextWidth(
|
||||
const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const {
|
||||
if (!m_dwriteFactory || request.text.empty()) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const std::wstring text = Utf8ToWide(request.text);
|
||||
if (text.empty()) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float dpiScale = ClampDpiScale(m_dpiScale);
|
||||
const float scaledFontSize = ResolveFontSize(request.fontSize) * dpiScale;
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize);
|
||||
if (textFormat == nullptr) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteTextLayout> textLayout;
|
||||
HRESULT hr = m_dwriteFactory->CreateTextLayout(
|
||||
text.c_str(),
|
||||
static_cast<UINT32>(text.size()),
|
||||
textFormat,
|
||||
4096.0f,
|
||||
scaledFontSize * 2.0f,
|
||||
textLayout.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || !textLayout) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
DWRITE_TEXT_METRICS textMetrics = {};
|
||||
hr = textLayout->GetMetrics(&textMetrics);
|
||||
if (FAILED(hr)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
DWRITE_OVERHANG_METRICS overhangMetrics = {};
|
||||
float width = textMetrics.widthIncludingTrailingWhitespace;
|
||||
if (SUCCEEDED(textLayout->GetOverhangMetrics(&overhangMetrics))) {
|
||||
width += (std::max)(overhangMetrics.left, 0.0f);
|
||||
width += (std::max)(overhangMetrics.right, 0.0f);
|
||||
}
|
||||
|
||||
return std::ceil(width) / dpiScale;
|
||||
}
|
||||
|
||||
bool NativeRenderer::CaptureToPng(
|
||||
const ::XCEngine::UI::UIDrawData& drawData,
|
||||
UINT width,
|
||||
@@ -146,7 +223,9 @@ bool NativeRenderer::CaptureToPng(
|
||||
|
||||
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties(
|
||||
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED));
|
||||
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
|
||||
kBaseDpi,
|
||||
kBaseDpi);
|
||||
|
||||
Microsoft::WRL::ComPtr<ID2D1RenderTarget> offscreenRenderTarget;
|
||||
hr = m_d2dFactory->CreateWicBitmapRenderTarget(
|
||||
@@ -301,7 +380,11 @@ bool NativeRenderer::CreateDeviceResources() {
|
||||
const UINT width = static_cast<UINT>((std::max)(clientRect.right - clientRect.left, 1L));
|
||||
const UINT height = static_cast<UINT>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||
|
||||
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties();
|
||||
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties(
|
||||
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
|
||||
kBaseDpi,
|
||||
kBaseDpi);
|
||||
const D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties(
|
||||
m_hwnd,
|
||||
D2D1::SizeU(width, height));
|
||||
@@ -324,6 +407,7 @@ bool NativeRenderer::CreateDeviceResources() {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_renderTarget->SetDpi(kBaseDpi, kBaseDpi);
|
||||
m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
||||
m_lastRenderError.clear();
|
||||
return true;
|
||||
@@ -333,6 +417,7 @@ bool NativeRenderer::RenderToTarget(
|
||||
ID2D1RenderTarget& renderTarget,
|
||||
ID2D1SolidColorBrush& solidBrush,
|
||||
const ::XCEngine::UI::UIDrawData& drawData) {
|
||||
renderTarget.SetDpi(kBaseDpi, kBaseDpi);
|
||||
renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
||||
renderTarget.BeginDraw();
|
||||
renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f));
|
||||
@@ -358,13 +443,15 @@ void NativeRenderer::RenderCommand(
|
||||
const ::XCEngine::UI::UIDrawCommand& command,
|
||||
std::vector<D2D1_RECT_F>& clipStack) {
|
||||
solidBrush.SetColor(ToD2DColor(command.color));
|
||||
const float dpiScale = ClampDpiScale(m_dpiScale);
|
||||
|
||||
switch (command.type) {
|
||||
case ::XCEngine::UI::UIDrawCommandType::FilledRect: {
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f;
|
||||
if (command.rounding > 0.0f) {
|
||||
renderTarget.FillRoundedRectangle(
|
||||
D2D1::RoundedRect(rect, command.rounding, command.rounding),
|
||||
D2D1::RoundedRect(rect, rounding, rounding),
|
||||
&solidBrush);
|
||||
} else {
|
||||
renderTarget.FillRectangle(rect, &solidBrush);
|
||||
@@ -372,11 +459,12 @@ void NativeRenderer::RenderCommand(
|
||||
break;
|
||||
}
|
||||
case ::XCEngine::UI::UIDrawCommandType::RectOutline: {
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
||||
const float thickness = command.thickness > 0.0f ? command.thickness : 1.0f;
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale;
|
||||
const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f;
|
||||
if (command.rounding > 0.0f) {
|
||||
renderTarget.DrawRoundedRectangle(
|
||||
D2D1::RoundedRect(rect, command.rounding, command.rounding),
|
||||
D2D1::RoundedRect(rect, rounding, rounding),
|
||||
&solidBrush,
|
||||
thickness);
|
||||
} else {
|
||||
@@ -389,8 +477,9 @@ void NativeRenderer::RenderCommand(
|
||||
break;
|
||||
}
|
||||
|
||||
const float fontSize = command.fontSize > 0.0f ? command.fontSize : 16.0f;
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(fontSize);
|
||||
const float fontSize = ResolveFontSize(command.fontSize);
|
||||
const float scaledFontSize = fontSize * dpiScale;
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize);
|
||||
if (textFormat == nullptr) {
|
||||
break;
|
||||
}
|
||||
@@ -401,11 +490,14 @@ void NativeRenderer::RenderCommand(
|
||||
}
|
||||
|
||||
const D2D1_SIZE_F targetSize = renderTarget.GetSize();
|
||||
const float originX = SnapToPixel(command.position.x, dpiScale);
|
||||
const float originY = SnapToPixel(command.position.y, dpiScale);
|
||||
const float lineHeight = std::ceil(scaledFontSize * 1.6f);
|
||||
const D2D1_RECT_F layoutRect = D2D1::RectF(
|
||||
command.position.x,
|
||||
command.position.y,
|
||||
originX,
|
||||
originY,
|
||||
targetSize.width,
|
||||
command.position.y + fontSize * 1.8f);
|
||||
originY + lineHeight);
|
||||
renderTarget.DrawTextW(
|
||||
text.c_str(),
|
||||
static_cast<UINT32>(text.size()),
|
||||
@@ -413,7 +505,7 @@ void NativeRenderer::RenderCommand(
|
||||
layoutRect,
|
||||
&solidBrush,
|
||||
D2D1_DRAW_TEXT_OPTIONS_CLIP,
|
||||
DWRITE_MEASURING_MODE_NATURAL);
|
||||
DWRITE_MEASURING_MODE_GDI_NATURAL);
|
||||
break;
|
||||
}
|
||||
case ::XCEngine::UI::UIDrawCommandType::Image: {
|
||||
@@ -421,12 +513,12 @@ void NativeRenderer::RenderCommand(
|
||||
break;
|
||||
}
|
||||
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
|
||||
break;
|
||||
}
|
||||
case ::XCEngine::UI::UIDrawCommandType::PushClipRect: {
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
clipStack.push_back(rect);
|
||||
break;
|
||||
@@ -443,12 +535,13 @@ void NativeRenderer::RenderCommand(
|
||||
}
|
||||
}
|
||||
|
||||
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
|
||||
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const {
|
||||
if (!m_dwriteFactory) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int key = static_cast<int>(std::lround(fontSize * 10.0f));
|
||||
const float resolvedFontSize = ResolveFontSize(fontSize);
|
||||
const int key = static_cast<int>(std::lround(resolvedFontSize * 10.0f));
|
||||
const auto found = m_textFormats.find(key);
|
||||
if (found != m_textFormats.end()) {
|
||||
return found->second.Get();
|
||||
@@ -461,7 +554,7 @@ IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
|
||||
DWRITE_FONT_WEIGHT_REGULAR,
|
||||
DWRITE_FONT_STYLE_NORMAL,
|
||||
DWRITE_FONT_STRETCH_NORMAL,
|
||||
fontSize,
|
||||
resolvedFontSize,
|
||||
L"",
|
||||
textFormat.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <d2d1.h>
|
||||
@@ -20,13 +22,17 @@
|
||||
|
||||
namespace XCEngine::UI::Editor::Host {
|
||||
|
||||
class NativeRenderer {
|
||||
class NativeRenderer : public ::XCEngine::UI::Editor::UIEditorTextMeasurer {
|
||||
public:
|
||||
bool Initialize(HWND hwnd);
|
||||
void Shutdown();
|
||||
void SetDpiScale(float dpiScale);
|
||||
float GetDpiScale() const;
|
||||
void Resize(UINT width, UINT height);
|
||||
bool Render(const ::XCEngine::UI::UIDrawData& drawData);
|
||||
const std::string& GetLastRenderError() const;
|
||||
float MeasureTextWidth(
|
||||
const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const override;
|
||||
bool CaptureToPng(
|
||||
const ::XCEngine::UI::UIDrawData& drawData,
|
||||
UINT width,
|
||||
@@ -49,7 +55,7 @@ private:
|
||||
const ::XCEngine::UI::UIDrawCommand& command,
|
||||
std::vector<D2D1_RECT_F>& clipStack);
|
||||
|
||||
IDWriteTextFormat* GetTextFormat(float fontSize);
|
||||
IDWriteTextFormat* GetTextFormat(float fontSize) const;
|
||||
static D2D1_COLOR_F ToD2DColor(const ::XCEngine::UI::UIColor& color);
|
||||
static std::wstring Utf8ToWide(std::string_view text);
|
||||
|
||||
@@ -59,9 +65,10 @@ private:
|
||||
Microsoft::WRL::ComPtr<IWICImagingFactory> m_wicFactory;
|
||||
Microsoft::WRL::ComPtr<ID2D1HwndRenderTarget> m_renderTarget;
|
||||
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> m_solidBrush;
|
||||
std::unordered_map<int, Microsoft::WRL::ComPtr<IDWriteTextFormat>> m_textFormats;
|
||||
mutable std::unordered_map<int, Microsoft::WRL::ComPtr<IDWriteTextFormat>> m_textFormats;
|
||||
std::string m_lastRenderError = {};
|
||||
bool m_wicComInitialized = false;
|
||||
float m_dpiScale = 1.0f;
|
||||
};
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Host
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "Shell/ProductShellAsset.h"
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
@@ -10,8 +11,11 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <shellscalingapi.h>
|
||||
|
||||
#ifndef XCUIEDITOR_REPO_ROOT
|
||||
#define XCUIEDITOR_REPO_ROOT "."
|
||||
#endif
|
||||
@@ -34,11 +38,112 @@ using App::BuildProductShellInteractionDefinition;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost";
|
||||
constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor";
|
||||
constexpr UINT kDefaultDpi = 96u;
|
||||
constexpr float kBaseDpiScale = 96.0f;
|
||||
|
||||
Application* GetApplicationFromWindow(HWND hwnd) {
|
||||
return reinterpret_cast<Application*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
UINT QuerySystemDpi() {
|
||||
HDC screenDc = GetDC(nullptr);
|
||||
if (screenDc == nullptr) {
|
||||
return kDefaultDpi;
|
||||
}
|
||||
|
||||
const int dpiX = GetDeviceCaps(screenDc, LOGPIXELSX);
|
||||
ReleaseDC(nullptr, screenDc);
|
||||
return dpiX > 0 ? static_cast<UINT>(dpiX) : kDefaultDpi;
|
||||
}
|
||||
|
||||
UINT QueryWindowDpi(HWND hwnd) {
|
||||
if (hwnd != nullptr) {
|
||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (user32 != nullptr) {
|
||||
using GetDpiForWindowFn = UINT(WINAPI*)(HWND);
|
||||
const auto getDpiForWindow =
|
||||
reinterpret_cast<GetDpiForWindowFn>(GetProcAddress(user32, "GetDpiForWindow"));
|
||||
if (getDpiForWindow != nullptr) {
|
||||
const UINT dpi = getDpiForWindow(hwnd);
|
||||
if (dpi != 0u) {
|
||||
return dpi;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QuerySystemDpi();
|
||||
}
|
||||
|
||||
void EnableDpiAwareness() {
|
||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (user32 != nullptr) {
|
||||
using SetProcessDpiAwarenessContextFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT);
|
||||
const auto setProcessDpiAwarenessContext =
|
||||
reinterpret_cast<SetProcessDpiAwarenessContextFn>(
|
||||
GetProcAddress(user32, "SetProcessDpiAwarenessContext"));
|
||||
if (setProcessDpiAwarenessContext != nullptr) {
|
||||
if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) {
|
||||
return;
|
||||
}
|
||||
if (GetLastError() == ERROR_ACCESS_DENIED) {
|
||||
return;
|
||||
}
|
||||
if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) {
|
||||
return;
|
||||
}
|
||||
if (GetLastError() == ERROR_ACCESS_DENIED) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HMODULE shcore = LoadLibraryW(L"shcore.dll");
|
||||
if (shcore != nullptr) {
|
||||
using SetProcessDpiAwarenessFn = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS);
|
||||
const auto setProcessDpiAwareness =
|
||||
reinterpret_cast<SetProcessDpiAwarenessFn>(
|
||||
GetProcAddress(shcore, "SetProcessDpiAwareness"));
|
||||
if (setProcessDpiAwareness != nullptr) {
|
||||
const HRESULT hr = setProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
|
||||
FreeLibrary(shcore);
|
||||
if (SUCCEEDED(hr) || hr == E_ACCESSDENIED) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
FreeLibrary(shcore);
|
||||
}
|
||||
}
|
||||
|
||||
if (user32 != nullptr) {
|
||||
using SetProcessDPIAwareFn = BOOL(WINAPI*)();
|
||||
const auto setProcessDPIAware =
|
||||
reinterpret_cast<SetProcessDPIAwareFn>(GetProcAddress(user32, "SetProcessDPIAware"));
|
||||
if (setProcessDPIAware != nullptr) {
|
||||
setProcessDPIAware();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TryEnableNonClientDpiScaling(HWND hwnd) {
|
||||
if (hwnd == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (user32 == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
using EnableNonClientDpiScalingFn = BOOL(WINAPI*)(HWND);
|
||||
const auto enableNonClientDpiScaling =
|
||||
reinterpret_cast<EnableNonClientDpiScalingFn>(
|
||||
GetProcAddress(user32, "EnableNonClientDpiScaling"));
|
||||
if (enableNonClientDpiScaling != nullptr) {
|
||||
enableNonClientDpiScaling(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
std::string TruncateText(const std::string& text, std::size_t maxLength) {
|
||||
if (text.size() <= maxLength) {
|
||||
return text;
|
||||
@@ -146,6 +251,44 @@ bool IsRepeatKeyMessage(LPARAM lParam) {
|
||||
return (static_cast<unsigned long>(lParam) & (1ul << 30)) != 0ul;
|
||||
}
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
const DWORD copied = ::GetModuleFileNameW(
|
||||
nullptr,
|
||||
buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (copied == 0u) {
|
||||
return std::filesystem::current_path().lexically_normal();
|
||||
}
|
||||
|
||||
if (copied < buffer.size() - 1u) {
|
||||
return std::filesystem::path(std::wstring(buffer.data(), copied))
|
||||
.parent_path()
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
buffer.resize(buffer.size() * 2u);
|
||||
}
|
||||
}
|
||||
|
||||
std::string DescribeInputEventType(const UIInputEvent& event) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove: return "PointerMove";
|
||||
case UIInputEventType::PointerEnter: return "PointerEnter";
|
||||
case UIInputEventType::PointerLeave: return "PointerLeave";
|
||||
case UIInputEventType::PointerButtonDown: return "PointerDown";
|
||||
case UIInputEventType::PointerButtonUp: return "PointerUp";
|
||||
case UIInputEventType::PointerWheel: return "PointerWheel";
|
||||
case UIInputEventType::KeyDown: return "KeyDown";
|
||||
case UIInputEventType::KeyUp: return "KeyUp";
|
||||
case UIInputEventType::Character: return "Character";
|
||||
case UIInputEventType::FocusGained: return "FocusGained";
|
||||
case UIInputEventType::FocusLost: return "FocusLost";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
@@ -172,10 +315,17 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
|
||||
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
m_hInstance = hInstance;
|
||||
EnableDpiAwareness();
|
||||
const std::filesystem::path logRoot =
|
||||
GetExecutableDirectory() / "logs";
|
||||
InitializeUIEditorRuntimeTrace(logRoot);
|
||||
SetUnhandledExceptionFilter(&Application::HandleUnhandledException);
|
||||
LogRuntimeTrace("app", "initialize begin");
|
||||
m_shellAsset = BuildProductShellAsset(ResolveRepoRootPath());
|
||||
m_shellValidation = ValidateEditorShellAsset(m_shellAsset);
|
||||
m_validationMessage = m_shellValidation.message;
|
||||
if (!m_shellValidation.IsValid()) {
|
||||
LogRuntimeTrace("app", "shell asset validation failed: " + m_validationMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -188,8 +338,10 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
m_shellServices = {};
|
||||
m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher();
|
||||
m_shellServices.shortcutManager = &m_shortcutManager;
|
||||
m_shellServices.textMeasurer = &m_renderer;
|
||||
m_lastStatus = "Ready";
|
||||
m_lastMessage = "Old editor shell baseline loaded.";
|
||||
LogRuntimeTrace("app", "workspace initialized: " + DescribeWorkspaceState());
|
||||
|
||||
WNDCLASSEXW windowClass = {};
|
||||
windowClass.cbSize = sizeof(windowClass);
|
||||
@@ -200,6 +352,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
windowClass.lpszClassName = kWindowClassName;
|
||||
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||
if (m_windowClassAtom == 0) {
|
||||
LogRuntimeTrace("app", "window class registration failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -207,7 +360,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
0,
|
||||
kWindowClassName,
|
||||
kWindowTitle,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
1540,
|
||||
@@ -217,26 +370,37 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
hInstance,
|
||||
this);
|
||||
if (m_hwnd == nullptr) {
|
||||
LogRuntimeTrace("app", "window creation failed");
|
||||
return false;
|
||||
}
|
||||
m_windowDpi = QueryWindowDpi(m_hwnd);
|
||||
m_dpiScale = GetDpiScale();
|
||||
m_renderer.SetDpiScale(m_dpiScale);
|
||||
|
||||
std::ostringstream dpiTrace = {};
|
||||
dpiTrace << "initial dpi=" << m_windowDpi << " scale=" << m_dpiScale;
|
||||
LogRuntimeTrace("window", dpiTrace.str());
|
||||
|
||||
if (!m_renderer.Initialize(m_hwnd)) {
|
||||
LogRuntimeTrace("app", "renderer initialization failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
ShowWindow(m_hwnd, nCmdShow);
|
||||
UpdateWindow(m_hwnd);
|
||||
|
||||
if (!m_renderer.Initialize(m_hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_autoScreenshot.Initialize(m_shellAsset.captureRootPath);
|
||||
if (IsAutoCaptureOnStartupEnabled()) {
|
||||
m_autoScreenshot.RequestCapture("startup");
|
||||
m_lastStatus = "Capture";
|
||||
m_lastMessage = "Startup capture requested.";
|
||||
}
|
||||
LogRuntimeTrace("app", "initialize completed");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::Shutdown() {
|
||||
LogRuntimeTrace("app", "shutdown begin");
|
||||
if (GetCapture() == m_hwnd) {
|
||||
ReleaseCapture();
|
||||
}
|
||||
@@ -253,6 +417,9 @@ void Application::Shutdown() {
|
||||
UnregisterClassW(kWindowClassName, m_hInstance);
|
||||
m_windowClassAtom = 0;
|
||||
}
|
||||
|
||||
LogRuntimeTrace("app", "shutdown end");
|
||||
ShutdownUIEditorRuntimeTrace();
|
||||
}
|
||||
|
||||
void Application::RenderFrame() {
|
||||
@@ -262,8 +429,12 @@ void Application::RenderFrame() {
|
||||
|
||||
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));
|
||||
const unsigned int pixelWidth =
|
||||
static_cast<unsigned int>((std::max)(clientRect.right - clientRect.left, 1L));
|
||||
const unsigned int pixelHeight =
|
||||
static_cast<unsigned int>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||
const float width = PixelsToDips(static_cast<float>(pixelWidth));
|
||||
const float height = PixelsToDips(static_cast<float>(pixelHeight));
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell");
|
||||
@@ -277,6 +448,11 @@ void Application::RenderFrame() {
|
||||
const UIEditorShellInteractionDefinition definition = BuildShellDefinition();
|
||||
std::vector<UIInputEvent> frameEvents = std::move(m_pendingInputEvents);
|
||||
m_pendingInputEvents.clear();
|
||||
if (!frameEvents.empty()) {
|
||||
LogRuntimeTrace(
|
||||
"input",
|
||||
DescribeInputEvents(frameEvents) + " | " + DescribeWorkspaceState());
|
||||
}
|
||||
|
||||
m_shellFrame = UpdateUIEditorShellInteraction(
|
||||
m_shellInteractionState,
|
||||
@@ -286,6 +462,24 @@ void Application::RenderFrame() {
|
||||
frameEvents,
|
||||
m_shellServices,
|
||||
metrics);
|
||||
if (!frameEvents.empty() ||
|
||||
m_shellFrame.result.workspaceResult.dockHostResult.layoutChanged ||
|
||||
m_shellFrame.result.workspaceResult.dockHostResult.commandExecuted) {
|
||||
std::ostringstream frameTrace = {};
|
||||
frameTrace << "result consumed="
|
||||
<< (m_shellFrame.result.consumed ? "true" : "false")
|
||||
<< " layoutChanged="
|
||||
<< (m_shellFrame.result.workspaceResult.dockHostResult.layoutChanged ? "true" : "false")
|
||||
<< " commandExecuted="
|
||||
<< (m_shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false")
|
||||
<< " active="
|
||||
<< m_workspaceController.GetWorkspace().activePanelId
|
||||
<< " message="
|
||||
<< m_shellFrame.result.workspaceResult.dockHostResult.layoutResult.message;
|
||||
LogRuntimeTrace(
|
||||
"frame",
|
||||
frameTrace.str());
|
||||
}
|
||||
ApplyHostCaptureRequests(m_shellFrame.result);
|
||||
UpdateLastStatus(m_shellFrame.result);
|
||||
AppendUIEditorShellInteraction(
|
||||
@@ -311,11 +505,78 @@ void Application::RenderFrame() {
|
||||
m_autoScreenshot.CaptureIfRequested(
|
||||
m_renderer,
|
||||
drawData,
|
||||
static_cast<unsigned int>(width),
|
||||
static_cast<unsigned int>(height),
|
||||
pixelWidth,
|
||||
pixelHeight,
|
||||
framePresented);
|
||||
}
|
||||
|
||||
float Application::GetDpiScale() const {
|
||||
const UINT dpi = m_windowDpi == 0u ? kDefaultDpi : m_windowDpi;
|
||||
return static_cast<float>(dpi) / kBaseDpiScale;
|
||||
}
|
||||
|
||||
float Application::PixelsToDips(float pixels) const {
|
||||
const float dpiScale = GetDpiScale();
|
||||
return dpiScale > 0.0f ? pixels / dpiScale : pixels;
|
||||
}
|
||||
|
||||
UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const {
|
||||
return UIPoint(
|
||||
PixelsToDips(static_cast<float>(x)),
|
||||
PixelsToDips(static_cast<float>(y)));
|
||||
}
|
||||
|
||||
void Application::LogRuntimeTrace(
|
||||
std::string_view channel,
|
||||
std::string_view message) const {
|
||||
AppendUIEditorRuntimeTrace(channel, message);
|
||||
}
|
||||
|
||||
std::string Application::DescribeWorkspaceState() const {
|
||||
std::ostringstream stream = {};
|
||||
stream << "active=" << m_workspaceController.GetWorkspace().activePanelId;
|
||||
const auto visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(
|
||||
m_workspaceController.GetWorkspace(),
|
||||
m_workspaceController.GetSession());
|
||||
stream << " visible=[";
|
||||
for (std::size_t index = 0; index < visiblePanels.size(); ++index) {
|
||||
if (index > 0u) {
|
||||
stream << ',';
|
||||
}
|
||||
stream << visiblePanels[index].panelId;
|
||||
}
|
||||
stream << ']';
|
||||
|
||||
const auto& dockState =
|
||||
m_shellInteractionState.workspaceInteractionState.dockHostInteractionState;
|
||||
stream << " dragNode=" << dockState.activeTabDragNodeId;
|
||||
stream << " dragPanel=" << dockState.activeTabDragPanelId;
|
||||
if (dockState.dockHostState.dropPreview.visible) {
|
||||
stream << " dropTarget=" << dockState.dockHostState.dropPreview.targetNodeId;
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string Application::DescribeInputEvents(
|
||||
const std::vector<UIInputEvent>& events) const {
|
||||
std::ostringstream stream = {};
|
||||
stream << "events=[";
|
||||
for (std::size_t index = 0; index < events.size(); ++index) {
|
||||
if (index > 0u) {
|
||||
stream << " | ";
|
||||
}
|
||||
const UIInputEvent& event = events[index];
|
||||
stream << DescribeInputEventType(event)
|
||||
<< '@'
|
||||
<< static_cast<int>(event.position.x)
|
||||
<< ','
|
||||
<< static_cast<int>(event.position.y);
|
||||
}
|
||||
stream << ']';
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
void Application::OnResize(UINT width, UINT height) {
|
||||
if (width == 0 || height == 0) {
|
||||
return;
|
||||
@@ -324,6 +585,27 @@ void Application::OnResize(UINT width, UINT height) {
|
||||
m_renderer.Resize(width, height);
|
||||
}
|
||||
|
||||
void Application::OnDpiChanged(UINT dpi, const RECT& suggestedRect) {
|
||||
m_windowDpi = dpi == 0u ? kDefaultDpi : dpi;
|
||||
m_dpiScale = GetDpiScale();
|
||||
m_renderer.SetDpiScale(m_dpiScale);
|
||||
if (m_hwnd != nullptr) {
|
||||
SetWindowPos(
|
||||
m_hwnd,
|
||||
nullptr,
|
||||
suggestedRect.left,
|
||||
suggestedRect.top,
|
||||
suggestedRect.right - suggestedRect.left,
|
||||
suggestedRect.bottom - suggestedRect.top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
std::ostringstream trace = {};
|
||||
trace << "dpi changed to " << m_windowDpi << " scale=" << m_dpiScale;
|
||||
LogRuntimeTrace("window", trace.str());
|
||||
}
|
||||
|
||||
void Application::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) {
|
||||
if (result.requestPointerCapture && GetCapture() != m_hwnd) {
|
||||
SetCapture(m_hwnd);
|
||||
@@ -338,6 +620,10 @@ bool Application::HasInteractiveCaptureState() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!m_shellInteractionState.workspaceInteractionState.dockHostInteractionState.activeTabDragNodeId.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto& panelState : m_shellInteractionState.workspaceInteractionState.composeState.panelStates) {
|
||||
if (panelState.viewportShellState.inputBridgeState.captured) {
|
||||
return true;
|
||||
@@ -434,9 +720,9 @@ void Application::QueuePointerEvent(
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.pointerButton = button;
|
||||
event.position = UIPoint(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
event.position = ConvertClientPixelsToDips(
|
||||
GET_X_LPARAM(lParam),
|
||||
GET_Y_LPARAM(lParam));
|
||||
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
||||
m_pendingInputEvents.push_back(event);
|
||||
}
|
||||
@@ -448,9 +734,7 @@ void Application::QueuePointerLeaveEvent() {
|
||||
POINT clientPoint = {};
|
||||
GetCursorPos(&clientPoint);
|
||||
ScreenToClient(m_hwnd, &clientPoint);
|
||||
event.position = UIPoint(
|
||||
static_cast<float>(clientPoint.x),
|
||||
static_cast<float>(clientPoint.y));
|
||||
event.position = ConvertClientPixelsToDips(clientPoint.x, clientPoint.y);
|
||||
}
|
||||
m_pendingInputEvents.push_back(event);
|
||||
}
|
||||
@@ -468,9 +752,7 @@ void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerWheel;
|
||||
event.position = UIPoint(
|
||||
static_cast<float>(screenPoint.x),
|
||||
static_cast<float>(screenPoint.y));
|
||||
event.position = ConvertClientPixelsToDips(screenPoint.x, screenPoint.y);
|
||||
event.wheelDelta = static_cast<float>(wheelDelta);
|
||||
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
||||
m_pendingInputEvents.push_back(event);
|
||||
@@ -507,6 +789,19 @@ std::filesystem::path Application::ResolveRepoRootPath() {
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
LONG WINAPI Application::HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo) {
|
||||
if (exceptionInfo != nullptr &&
|
||||
exceptionInfo->ExceptionRecord != nullptr) {
|
||||
AppendUIEditorCrashTrace(
|
||||
exceptionInfo->ExceptionRecord->ExceptionCode,
|
||||
exceptionInfo->ExceptionRecord->ExceptionAddress);
|
||||
} else {
|
||||
AppendUIEditorCrashTrace(0u, nullptr);
|
||||
}
|
||||
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
UIEditorHostCommandEvaluationResult Application::EvaluateHostCommand(
|
||||
std::string_view commandId) const {
|
||||
UIEditorHostCommandEvaluationResult result = {};
|
||||
@@ -573,6 +868,7 @@ UIEditorHostCommandDispatchResult Application::DispatchHostCommand(
|
||||
|
||||
LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
if (message == WM_NCCREATE) {
|
||||
TryEnableNonClientDpiScaling(hwnd);
|
||||
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||
auto* application = reinterpret_cast<Application*>(createStruct->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(application));
|
||||
@@ -581,6 +877,14 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
|
||||
|
||||
Application* application = GetApplicationFromWindow(hwnd);
|
||||
switch (message) {
|
||||
case WM_DPICHANGED:
|
||||
if (application != nullptr && lParam != 0) {
|
||||
application->OnDpiChanged(
|
||||
static_cast<UINT>(LOWORD(wParam)),
|
||||
*reinterpret_cast<RECT*>(lParam));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_SIZE:
|
||||
if (application != nullptr && wParam != SIZE_MINIMIZED) {
|
||||
application->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||
|
||||
@@ -42,10 +42,18 @@ private:
|
||||
void Shutdown();
|
||||
void RenderFrame();
|
||||
void OnResize(UINT width, UINT height);
|
||||
void OnDpiChanged(UINT dpi, const RECT& suggestedRect);
|
||||
float GetDpiScale() const;
|
||||
float PixelsToDips(float pixels) const;
|
||||
::XCEngine::UI::UIPoint ConvertClientPixelsToDips(LONG x, LONG y) const;
|
||||
void LogRuntimeTrace(std::string_view channel, std::string_view message) const;
|
||||
void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result);
|
||||
bool HasInteractiveCaptureState() const;
|
||||
UIEditorShellInteractionDefinition BuildShellDefinition() const;
|
||||
void UpdateLastStatus(const UIEditorShellInteractionResult& result);
|
||||
std::string DescribeWorkspaceState() const;
|
||||
std::string DescribeInputEvents(
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& events) const;
|
||||
void QueuePointerEvent(
|
||||
::XCEngine::UI::UIInputEventType type,
|
||||
::XCEngine::UI::UIPointerButton button,
|
||||
@@ -57,6 +65,7 @@ private:
|
||||
void QueueCharacterEvent(WPARAM wParam, LPARAM lParam);
|
||||
void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type);
|
||||
static std::filesystem::path ResolveRepoRootPath();
|
||||
static LONG WINAPI HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo);
|
||||
|
||||
HWND m_hwnd = nullptr;
|
||||
HINSTANCE m_hInstance = nullptr;
|
||||
@@ -76,6 +85,8 @@ private:
|
||||
std::string m_validationMessage = {};
|
||||
std::string m_lastStatus = {};
|
||||
std::string m_lastMessage = {};
|
||||
UINT m_windowDpi = 96u;
|
||||
float m_dpiScale = 1.0f;
|
||||
};
|
||||
|
||||
int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow);
|
||||
|
||||
@@ -22,11 +22,21 @@ struct UIEditorTabStripItem {
|
||||
float desiredHeaderLabelWidth = 0.0f;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripReorderState {
|
||||
::XCEngine::UI::UIPoint pressPosition = {};
|
||||
std::size_t pressedIndex = UIEditorTabStripInvalidIndex;
|
||||
std::size_t sourceIndex = UIEditorTabStripInvalidIndex;
|
||||
std::size_t previewInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
bool armed = false;
|
||||
bool dragging = false;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripState {
|
||||
std::size_t selectedIndex = UIEditorTabStripInvalidIndex;
|
||||
std::size_t hoveredIndex = UIEditorTabStripInvalidIndex;
|
||||
std::size_t closeHoveredIndex = UIEditorTabStripInvalidIndex;
|
||||
bool focused = false;
|
||||
UIEditorTabStripReorderState reorder = {};
|
||||
};
|
||||
|
||||
struct UIEditorTabStripMetrics {
|
||||
@@ -38,21 +48,26 @@ struct UIEditorTabStripMetrics {
|
||||
float closeInsetRight = 6.0f;
|
||||
float closeInsetY = 0.0f;
|
||||
float labelInsetX = 8.0f;
|
||||
float labelInsetY = -2.5f;
|
||||
float labelInsetY = -0.5f;
|
||||
float baseBorderThickness = 1.0f;
|
||||
float selectedBorderThickness = 1.0f;
|
||||
float focusedBorderThickness = 1.0f;
|
||||
float reorderDragThreshold = 6.0f;
|
||||
float reorderPreviewThickness = 2.0f;
|
||||
float reorderPreviewInsetY = 3.0f;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripPalette {
|
||||
::XCEngine::UI::UIColor stripBackgroundColor =
|
||||
::XCEngine::UI::UIColor(0.16f, 0.16f, 0.16f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor headerBackgroundColor =
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor contentBackgroundColor =
|
||||
::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor stripBorderColor =
|
||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
::XCEngine::UI::UIColor headerContentSeparatorColor =
|
||||
::XCEngine::UI::UIColor(0.27f, 0.27f, 0.27f, 1.0f);
|
||||
::XCEngine::UI::UIColor focusedBorderColor =
|
||||
::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f);
|
||||
::XCEngine::UI::UIColor tabColor =
|
||||
@@ -60,7 +75,7 @@ struct UIEditorTabStripPalette {
|
||||
::XCEngine::UI::UIColor tabHoveredColor =
|
||||
::XCEngine::UI::UIColor(0.23f, 0.23f, 0.23f, 1.0f);
|
||||
::XCEngine::UI::UIColor tabSelectedColor =
|
||||
::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor tabBorderColor =
|
||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
::XCEngine::UI::UIColor tabHoveredBorderColor =
|
||||
@@ -81,6 +96,14 @@ struct UIEditorTabStripPalette {
|
||||
::XCEngine::UI::UIColor(0.30f, 0.30f, 0.30f, 1.0f);
|
||||
::XCEngine::UI::UIColor closeGlyphColor =
|
||||
::XCEngine::UI::UIColor(0.83f, 0.83f, 0.83f, 1.0f);
|
||||
::XCEngine::UI::UIColor reorderPreviewColor =
|
||||
::XCEngine::UI::UIColor(0.82f, 0.82f, 0.82f, 1.0f);
|
||||
};
|
||||
|
||||
struct UIEditorTabStripInsertionPreview {
|
||||
bool visible = false;
|
||||
std::size_t insertionIndex = UIEditorTabStripInvalidIndex;
|
||||
::XCEngine::UI::UIRect indicatorRect = {};
|
||||
};
|
||||
|
||||
struct UIEditorTabStripLayout {
|
||||
@@ -91,6 +114,7 @@ struct UIEditorTabStripLayout {
|
||||
std::vector<::XCEngine::UI::UIRect> closeButtonRects = {};
|
||||
std::vector<bool> showCloseButtons = {};
|
||||
std::size_t selectedIndex = UIEditorTabStripInvalidIndex;
|
||||
UIEditorTabStripInsertionPreview insertionPreview = {};
|
||||
};
|
||||
|
||||
enum class UIEditorTabStripHitTargetKind : std::uint8_t {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <XCEditor/Collections/UIEditorTabStrip.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIDragDropInteraction.h>
|
||||
#include <XCEngine/UI/Widgets/UITabStripModel.h>
|
||||
|
||||
#include <string>
|
||||
@@ -13,9 +14,13 @@ namespace XCEngine::UI::Editor {
|
||||
struct UIEditorTabStripInteractionState {
|
||||
Widgets::UIEditorTabStripState tabStripState = {};
|
||||
::XCEngine::UI::Widgets::UITabStripModel navigationModel = {};
|
||||
::XCEngine::UI::Widgets::UIDragDropState reorderDragState = {};
|
||||
Widgets::UIEditorTabStripHitTarget pressedTarget = {};
|
||||
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||
std::size_t reorderSourceIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::size_t reorderPreviewIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
bool hasPointerPosition = false;
|
||||
bool reorderCaptureActive = false;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripInteractionResult {
|
||||
@@ -23,11 +28,23 @@ struct UIEditorTabStripInteractionResult {
|
||||
bool selectionChanged = false;
|
||||
bool closeRequested = false;
|
||||
bool keyboardNavigated = false;
|
||||
bool requestPointerCapture = false;
|
||||
bool releasePointerCapture = false;
|
||||
bool dragStarted = false;
|
||||
bool dragEnded = false;
|
||||
bool dragCanceled = false;
|
||||
bool reorderRequested = false;
|
||||
bool reorderPreviewActive = false;
|
||||
Widgets::UIEditorTabStripHitTarget hitTarget = {};
|
||||
std::string selectedTabId = {};
|
||||
std::size_t selectedIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::string closedTabId = {};
|
||||
std::size_t closedIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::string draggedTabId = {};
|
||||
std::size_t dragSourceIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::size_t dropInsertionIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::size_t reorderToIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::size_t reorderPreviewIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripInteractionFrame {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
void InitializeUIEditorRuntimeTrace(const std::filesystem::path& logRoot);
|
||||
void ShutdownUIEditorRuntimeTrace();
|
||||
|
||||
void AppendUIEditorRuntimeTrace(
|
||||
std::string_view channel,
|
||||
std::string_view message);
|
||||
|
||||
void AppendUIEditorCrashTrace(
|
||||
std::uint32_t exceptionCode,
|
||||
const void* exceptionAddress);
|
||||
|
||||
std::filesystem::path GetUIEditorRuntimeTracePath();
|
||||
std::filesystem::path GetUIEditorCrashTracePath();
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct UIEditorTextMeasureRequest {
|
||||
std::string_view text = {};
|
||||
float fontSize = 0.0f;
|
||||
};
|
||||
|
||||
class UIEditorTextMeasurer {
|
||||
public:
|
||||
virtual ~UIEditorTextMeasurer() = default;
|
||||
|
||||
virtual float MeasureTextWidth(const UIEditorTextMeasureRequest& request) const = 0;
|
||||
};
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -40,16 +40,27 @@ struct UIEditorDockHostTabStripVisualState {
|
||||
UIEditorTabStripState state = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostDropPreviewState {
|
||||
bool visible = false;
|
||||
std::string sourceNodeId = {};
|
||||
std::string sourcePanelId = {};
|
||||
std::string targetNodeId = {};
|
||||
UIEditorWorkspaceDockPlacement placement =
|
||||
UIEditorWorkspaceDockPlacement::Center;
|
||||
std::size_t insertionIndex = UIEditorTabStripInvalidIndex;
|
||||
};
|
||||
|
||||
struct UIEditorDockHostState {
|
||||
bool focused = false;
|
||||
UIEditorDockHostHitTarget hoveredTarget = {};
|
||||
std::string activeSplitterNodeId = {};
|
||||
std::vector<UIEditorDockHostTabStripVisualState> tabStripStates = {};
|
||||
UIEditorDockHostDropPreviewState dropPreview = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostMetrics {
|
||||
::XCEngine::UI::Layout::UISplitterMetrics splitterMetrics =
|
||||
::XCEngine::UI::Layout::UISplitterMetrics{ 4.0f, 12.0f };
|
||||
::XCEngine::UI::Layout::UISplitterMetrics{ 1.0f, 10.0f };
|
||||
UIEditorTabStripMetrics tabStripMetrics = {};
|
||||
UIEditorPanelFrameMetrics panelFrameMetrics = {};
|
||||
::XCEngine::UI::UISize minimumStandalonePanelBodySize =
|
||||
@@ -75,6 +86,10 @@ struct UIEditorDockHostPalette {
|
||||
::XCEngine::UI::UIColor(0.70f, 0.72f, 0.74f, 1.0f);
|
||||
::XCEngine::UI::UIColor placeholderMutedColor =
|
||||
::XCEngine::UI::UIColor(0.58f, 0.59f, 0.62f, 1.0f);
|
||||
::XCEngine::UI::UIColor dropPreviewFillColor =
|
||||
::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 0.14f);
|
||||
::XCEngine::UI::UIColor dropPreviewBorderColor =
|
||||
::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 0.78f);
|
||||
};
|
||||
|
||||
struct UIEditorDockHostTabItemLayout {
|
||||
@@ -107,10 +122,20 @@ struct UIEditorDockHostTabStackLayout {
|
||||
UIEditorPanelFrameLayout contentFrameLayout = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostDropPreviewLayout {
|
||||
bool visible = false;
|
||||
std::string targetNodeId = {};
|
||||
UIEditorWorkspaceDockPlacement placement =
|
||||
UIEditorWorkspaceDockPlacement::Center;
|
||||
std::size_t insertionIndex = UIEditorTabStripInvalidIndex;
|
||||
::XCEngine::UI::UIRect previewRect = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostLayout {
|
||||
::XCEngine::UI::UIRect bounds = {};
|
||||
std::vector<UIEditorDockHostSplitterLayout> splitters = {};
|
||||
std::vector<UIEditorDockHostTabStackLayout> tabStacks = {};
|
||||
UIEditorDockHostDropPreviewLayout dropPreview = {};
|
||||
};
|
||||
|
||||
// Allows higher-level compose to own panel body presentation while DockHost
|
||||
|
||||
@@ -21,6 +21,8 @@ struct UIEditorDockHostInteractionState {
|
||||
Widgets::UIEditorDockHostState dockHostState = {};
|
||||
::XCEngine::UI::Widgets::UISplitterDragState splitterDragState = {};
|
||||
std::vector<UIEditorDockHostTabStripInteractionEntry> tabStripInteractions = {};
|
||||
std::string activeTabDragNodeId = {};
|
||||
std::string activeTabDragPanelId = {};
|
||||
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||
bool hasPointerPosition = false;
|
||||
};
|
||||
|
||||
@@ -29,12 +29,13 @@ struct UIEditorMenuBarMetrics {
|
||||
float barHeight = 24.0f;
|
||||
float horizontalInset = 0.0f;
|
||||
float verticalInset = 2.0f;
|
||||
float buttonGap = 2.0f;
|
||||
float buttonPaddingX = 10.0f;
|
||||
float buttonGap = 1.0f;
|
||||
float buttonPaddingX = 4.0f;
|
||||
float labelFontSize = 13.0f;
|
||||
float estimatedGlyphWidth = 6.5f;
|
||||
float labelInsetY = -1.5f;
|
||||
float barCornerRounding = 0.0f;
|
||||
float buttonCornerRounding = 0.0f;
|
||||
float buttonCornerRounding = 0.75f;
|
||||
float baseBorderThickness = 1.0f;
|
||||
float focusedBorderThickness = 1.0f;
|
||||
float openBorderThickness = 1.0f;
|
||||
@@ -42,23 +43,23 @@ struct UIEditorMenuBarMetrics {
|
||||
|
||||
struct UIEditorMenuBarPalette {
|
||||
::XCEngine::UI::UIColor barColor =
|
||||
::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f);
|
||||
::XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonColor =
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
::XCEngine::UI::UIColor buttonHoveredColor =
|
||||
::XCEngine::UI::UIColor(0.23f, 0.23f, 0.23f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonOpenColor =
|
||||
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f);
|
||||
::XCEngine::UI::UIColor borderColor =
|
||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
::XCEngine::UI::UIColor focusedBorderColor =
|
||||
::XCEngine::UI::UIColor(0.38f, 0.38f, 0.38f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
::XCEngine::UI::UIColor openBorderColor =
|
||||
::XCEngine::UI::UIColor(0.34f, 0.34f, 0.34f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
::XCEngine::UI::UIColor textPrimary =
|
||||
::XCEngine::UI::UIColor(0.85f, 0.85f, 0.85f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.08f, 0.08f, 0.08f, 1.0f);
|
||||
::XCEngine::UI::UIColor textMuted =
|
||||
::XCEngine::UI::UIColor(0.67f, 0.67f, 0.67f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
::XCEngine::UI::UIColor textDisabled =
|
||||
::XCEngine::UI::UIColor(0.50f, 0.50f, 0.50f, 1.0f);
|
||||
};
|
||||
|
||||
@@ -34,19 +34,19 @@ struct UIEditorMenuPopupState {
|
||||
};
|
||||
|
||||
struct UIEditorMenuPopupMetrics {
|
||||
float contentPaddingX = 6.0f;
|
||||
float contentPaddingX = 2.0f;
|
||||
float contentPaddingY = 6.0f;
|
||||
float itemHeight = 28.0f;
|
||||
float separatorHeight = 9.0f;
|
||||
float checkColumnWidth = 18.0f;
|
||||
float shortcutGap = 20.0f;
|
||||
float submenuIndicatorWidth = 14.0f;
|
||||
float rowCornerRounding = 5.0f;
|
||||
float popupCornerRounding = 8.0f;
|
||||
float labelInsetX = 14.0f;
|
||||
float checkColumnWidth = 12.0f;
|
||||
float shortcutGap = 14.0f;
|
||||
float submenuIndicatorWidth = 10.0f;
|
||||
float rowCornerRounding = 2.5f;
|
||||
float popupCornerRounding = 4.0f;
|
||||
float labelInsetX = 4.0f;
|
||||
float labelInsetY = -1.0f;
|
||||
float labelFontSize = 13.0f;
|
||||
float shortcutInsetRight = 24.0f;
|
||||
float shortcutInsetRight = 8.0f;
|
||||
float estimatedGlyphWidth = 7.0f;
|
||||
float glyphFontSize = 12.0f;
|
||||
float separatorThickness = 1.0f;
|
||||
|
||||
@@ -110,6 +110,12 @@ struct UIEditorShellComposeFrame {
|
||||
UIEditorWorkspaceComposeFrame workspaceFrame = {};
|
||||
};
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
const std::vector<Widgets::UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorShellComposeMetrics& metrics = {});
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
||||
#include <XCEditor/Shell/UIEditorMenuModel.h>
|
||||
#include <XCEditor/Shell/UIEditorMenuSession.h>
|
||||
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
||||
@@ -50,6 +51,7 @@ struct UIEditorShellInteractionPalette {
|
||||
struct UIEditorShellInteractionServices {
|
||||
const UIEditorCommandDispatcher* commandDispatcher = nullptr;
|
||||
const UIEditorShortcutManager* shortcutManager = nullptr;
|
||||
const UIEditorTextMeasurer* textMeasurer = nullptr;
|
||||
};
|
||||
|
||||
struct UIEditorShellInteractionMenuButtonRequest {
|
||||
|
||||
@@ -105,6 +105,21 @@ public:
|
||||
UIEditorWorkspaceLayoutOperationResult SetSplitRatio(
|
||||
std::string_view nodeId,
|
||||
float splitRatio);
|
||||
UIEditorWorkspaceLayoutOperationResult ReorderTab(
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
UIEditorWorkspaceLayoutOperationResult MoveTabToStack(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
UIEditorWorkspaceLayoutOperationResult DockTabRelative(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio = 0.5f);
|
||||
UIEditorWorkspaceCommandResult Dispatch(const UIEditorWorkspaceCommand& command);
|
||||
|
||||
private:
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct UIEditorWorkspaceSession;
|
||||
|
||||
enum class UIEditorWorkspaceNodeKind : std::uint8_t {
|
||||
Panel = 0,
|
||||
TabStack,
|
||||
@@ -19,6 +21,14 @@ enum class UIEditorWorkspaceSplitAxis : std::uint8_t {
|
||||
Vertical
|
||||
};
|
||||
|
||||
enum class UIEditorWorkspaceDockPlacement : std::uint8_t {
|
||||
Center = 0,
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom
|
||||
};
|
||||
|
||||
struct UIEditorWorkspacePanelState {
|
||||
std::string panelId = {};
|
||||
std::string title = {};
|
||||
@@ -133,4 +143,28 @@ bool TrySetUIEditorWorkspaceSplitRatio(
|
||||
std::string_view nodeId,
|
||||
float splitRatio);
|
||||
|
||||
bool TryReorderUIEditorWorkspaceTab(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
|
||||
bool TryMoveUIEditorWorkspaceTabToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
|
||||
bool TryDockUIEditorWorkspaceTabRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio = 0.5f);
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
|
||||
@@ -18,8 +18,7 @@ using ::XCEngine::UI::Layout::MeasureUITabStripHeaderWidth;
|
||||
|
||||
constexpr float kTabRounding = 0.0f;
|
||||
constexpr float kStripRounding = 0.0f;
|
||||
constexpr float kHeaderFontSize = 11.0f;
|
||||
constexpr float kCloseFontSize = 10.0f;
|
||||
constexpr float kHeaderFontSize = 13.0f;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
@@ -44,10 +43,6 @@ float ResolveTabRounding(const UIEditorTabStripMetrics& metrics) {
|
||||
return kTabRounding;
|
||||
}
|
||||
|
||||
float ResolveCloseButtonRounding(const UIEditorTabStripMetrics& metrics) {
|
||||
return (std::min)(ClampNonNegative(metrics.closeButtonExtent) * 0.2f, 2.0f);
|
||||
}
|
||||
|
||||
std::size_t ResolveSelectedIndex(
|
||||
std::size_t itemCount,
|
||||
std::size_t selectedIndex) {
|
||||
@@ -78,20 +73,18 @@ float ResolveTabTextTop(
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kHeaderFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
}
|
||||
|
||||
float ResolveCloseTextTop(const UIRect& rect) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kCloseFontSize) * 0.5f) - 0.5f;
|
||||
}
|
||||
|
||||
UIColor ResolveStripBorderColor(
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
return state.focused ? palette.focusedBorderColor : palette.stripBorderColor;
|
||||
(void)state;
|
||||
return palette.stripBorderColor;
|
||||
}
|
||||
|
||||
float ResolveStripBorderThickness(
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
return state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness;
|
||||
(void)state;
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
UIColor ResolveTabFillColor(
|
||||
@@ -115,7 +108,8 @@ UIColor ResolveTabBorderColor(
|
||||
bool focused,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
if (selected) {
|
||||
return focused ? palette.focusedBorderColor : palette.tabSelectedBorderColor;
|
||||
(void)focused;
|
||||
return palette.tabSelectedBorderColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
@@ -125,33 +119,121 @@ UIColor ResolveTabBorderColor(
|
||||
return palette.tabBorderColor;
|
||||
}
|
||||
|
||||
float ResolveTabBorderThickness(
|
||||
bool selected,
|
||||
bool focused,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
float ResolveTabBorderThickness(bool selected, bool focused, const UIEditorTabStripMetrics& metrics) {
|
||||
if (selected) {
|
||||
return focused ? metrics.focusedBorderThickness : metrics.selectedBorderThickness;
|
||||
(void)focused;
|
||||
return metrics.selectedBorderThickness;
|
||||
}
|
||||
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
UIRect BuildCloseButtonRect(
|
||||
const UIRect& headerRect,
|
||||
void AppendHeaderContentSeparator(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorTabStripLayout& layout,
|
||||
const UIEditorTabStripPalette& palette,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
const float insetY = ClampNonNegative(metrics.closeInsetY);
|
||||
const float extent = (std::min)(
|
||||
ClampNonNegative(metrics.closeButtonExtent),
|
||||
(std::max)(headerRect.height - insetY * 2.0f, 0.0f));
|
||||
if (extent <= 0.0f) {
|
||||
return {};
|
||||
if (layout.headerRect.width <= 0.0f ||
|
||||
layout.headerRect.height <= 0.0f ||
|
||||
layout.contentRect.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
return UIRect(
|
||||
headerRect.x + headerRect.width - ClampNonNegative(metrics.closeInsetRight) - extent,
|
||||
headerRect.y + insetY + (std::max)(0.0f, headerRect.height - insetY * 2.0f - extent) * 0.5f,
|
||||
extent,
|
||||
extent);
|
||||
const float thickness = (std::max)(ClampNonNegative(metrics.baseBorderThickness), 1.0f);
|
||||
const float separatorY = layout.contentRect.y;
|
||||
const float separatorLeft = layout.headerRect.x;
|
||||
const float separatorRight = layout.headerRect.x + layout.headerRect.width;
|
||||
|
||||
if (layout.selectedIndex == UIEditorTabStripInvalidIndex ||
|
||||
layout.selectedIndex >= layout.tabHeaderRects.size()) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(
|
||||
separatorLeft,
|
||||
separatorY,
|
||||
(std::max)(separatorRight - separatorLeft, 0.0f),
|
||||
thickness),
|
||||
palette.headerContentSeparatorColor);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIRect& selectedRect = layout.tabHeaderRects[layout.selectedIndex];
|
||||
const float gapLeft = (std::max)(separatorLeft, selectedRect.x + 1.0f);
|
||||
const float gapRight = (std::min)(separatorRight, selectedRect.x + selectedRect.width - 1.0f);
|
||||
|
||||
if (gapLeft > separatorLeft) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(
|
||||
separatorLeft,
|
||||
separatorY,
|
||||
(std::max)(gapLeft - separatorLeft, 0.0f),
|
||||
thickness),
|
||||
palette.headerContentSeparatorColor);
|
||||
}
|
||||
|
||||
if (gapRight < separatorRight) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(
|
||||
gapRight,
|
||||
separatorY,
|
||||
(std::max)(separatorRight - gapRight, 0.0f),
|
||||
thickness),
|
||||
palette.headerContentSeparatorColor);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendSelectedTabBottomBorderMask(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
float thickness,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
if (rect.width <= 0.0f || rect.height <= 0.0f || thickness <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float maskHeight = (std::max)(1.0f, thickness + 1.0f);
|
||||
drawList.AddFilledRect(
|
||||
UIRect(
|
||||
rect.x + 1.0f,
|
||||
rect.y + rect.height - maskHeight,
|
||||
(std::max)(rect.width - 2.0f, 0.0f),
|
||||
maskHeight + 1.0f),
|
||||
palette.contentBackgroundColor,
|
||||
0.0f);
|
||||
}
|
||||
|
||||
UIEditorTabStripInsertionPreview BuildInsertionPreview(
|
||||
const UIEditorTabStripLayout& layout,
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
UIEditorTabStripInsertionPreview preview = {};
|
||||
if (!state.reorder.dragging ||
|
||||
state.reorder.previewInsertionIndex == UIEditorTabStripInvalidIndex ||
|
||||
layout.tabHeaderRects.empty() ||
|
||||
state.reorder.previewInsertionIndex > layout.tabHeaderRects.size() ||
|
||||
layout.headerRect.height <= 0.0f) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
float indicatorX = layout.tabHeaderRects.front().x;
|
||||
if (state.reorder.previewInsertionIndex >= layout.tabHeaderRects.size()) {
|
||||
const UIRect& lastRect = layout.tabHeaderRects.back();
|
||||
indicatorX = lastRect.x + lastRect.width;
|
||||
} else {
|
||||
indicatorX = layout.tabHeaderRects[state.reorder.previewInsertionIndex].x;
|
||||
}
|
||||
|
||||
const float thickness = (std::max)(ClampNonNegative(metrics.reorderPreviewThickness), 1.0f);
|
||||
const float insetY = ClampNonNegative(metrics.reorderPreviewInsetY);
|
||||
const float indicatorHeight = (std::max)(layout.headerRect.height - insetY * 2.0f, thickness);
|
||||
|
||||
preview.visible = true;
|
||||
preview.insertionIndex = state.reorder.previewInsertionIndex;
|
||||
preview.indicatorRect = UIRect(
|
||||
indicatorX - thickness * 0.5f,
|
||||
layout.headerRect.y + insetY,
|
||||
thickness,
|
||||
indicatorHeight);
|
||||
return preview;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -162,13 +244,8 @@ float ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
const float labelWidth = ResolveEstimatedLabelWidth(item, metrics);
|
||||
const float horizontalPadding = ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||
const float extraLeftInset = (std::max)(ClampNonNegative(metrics.labelInsetX) - horizontalPadding, 0.0f);
|
||||
const float extraRightInset = (std::max)(ClampNonNegative(metrics.closeInsetRight) - horizontalPadding, 0.0f);
|
||||
const float closeBudget = item.closable
|
||||
? ClampNonNegative(metrics.closeButtonExtent) +
|
||||
ClampNonNegative(metrics.closeButtonGap) +
|
||||
extraRightInset
|
||||
: 0.0f;
|
||||
return labelWidth + extraLeftInset + closeBudget;
|
||||
(void)item;
|
||||
return labelWidth + extraLeftInset;
|
||||
}
|
||||
|
||||
std::size_t ResolveUIEditorTabStripSelectedIndex(
|
||||
@@ -188,34 +265,6 @@ std::size_t ResolveUIEditorTabStripSelectedIndex(
|
||||
return ResolveSelectedIndex(items.size(), fallbackIndex);
|
||||
}
|
||||
|
||||
std::size_t ResolveUIEditorTabStripSelectedIndexAfterClose(
|
||||
std::size_t selectedIndex,
|
||||
std::size_t closedIndex,
|
||||
std::size_t itemCountBeforeClose) {
|
||||
if (itemCountBeforeClose == 0u || closedIndex >= itemCountBeforeClose) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
const std::size_t itemCountAfterClose = itemCountBeforeClose - 1u;
|
||||
if (itemCountAfterClose == 0u) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
if (selectedIndex == UIEditorTabStripInvalidIndex || selectedIndex >= itemCountBeforeClose) {
|
||||
return (std::min)(closedIndex, itemCountAfterClose - 1u);
|
||||
}
|
||||
|
||||
if (closedIndex < selectedIndex) {
|
||||
return selectedIndex - 1u;
|
||||
}
|
||||
|
||||
if (closedIndex > selectedIndex) {
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
return (std::min)(selectedIndex, itemCountAfterClose - 1u);
|
||||
}
|
||||
|
||||
UIEditorTabStripLayout BuildUIEditorTabStripLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorTabStripItem>& items,
|
||||
@@ -245,17 +294,7 @@ UIEditorTabStripLayout BuildUIEditorTabStripLayout(
|
||||
layout.tabHeaderRects = arranged.tabHeaderRects;
|
||||
layout.closeButtonRects.resize(items.size());
|
||||
layout.showCloseButtons.resize(items.size(), false);
|
||||
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
if (!items[index].closable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
layout.closeButtonRects[index] = BuildCloseButtonRect(layout.tabHeaderRects[index], metrics);
|
||||
layout.showCloseButtons[index] =
|
||||
layout.closeButtonRects[index].width > 0.0f && layout.closeButtonRects[index].height > 0.0f;
|
||||
}
|
||||
|
||||
layout.insertionPreview = BuildInsertionPreview(layout, state, metrics);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -269,15 +308,6 @@ UIEditorTabStripHitTarget HitTestUIEditorTabStrip(
|
||||
return target;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.closeButtonRects.size(); ++index) {
|
||||
if (layout.showCloseButtons[index] &&
|
||||
IsPointInsideRect(layout.closeButtonRects[index], point)) {
|
||||
target.kind = UIEditorTabStripHitTargetKind::CloseButton;
|
||||
target.index = index;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
||||
if (IsPointInsideRect(layout.tabHeaderRects[index], point)) {
|
||||
target.kind = UIEditorTabStripHitTargetKind::Tab;
|
||||
@@ -322,7 +352,7 @@ void AppendUIEditorTabStripBackground(
|
||||
|
||||
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
||||
const bool selected = layout.selectedIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index || state.closeHoveredIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
drawList.AddFilledRect(
|
||||
layout.tabHeaderRects[index],
|
||||
ResolveTabFillColor(selected, hovered, palette),
|
||||
@@ -332,6 +362,20 @@ void AppendUIEditorTabStripBackground(
|
||||
ResolveTabBorderColor(selected, hovered, state.focused, palette),
|
||||
ResolveTabBorderThickness(selected, state.focused, metrics),
|
||||
tabRounding);
|
||||
if (selected) {
|
||||
AppendSelectedTabBottomBorderMask(
|
||||
drawList,
|
||||
layout.tabHeaderRects[index],
|
||||
ResolveTabBorderThickness(selected, state.focused, metrics),
|
||||
palette);
|
||||
}
|
||||
}
|
||||
|
||||
if (layout.insertionPreview.visible) {
|
||||
drawList.AddFilledRect(
|
||||
layout.insertionPreview.indicatorRect,
|
||||
palette.reorderPreviewColor,
|
||||
0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +386,8 @@ void AppendUIEditorTabStripForeground(
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripPalette& palette,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
AppendHeaderContentSeparator(drawList, layout, palette, metrics);
|
||||
|
||||
const float leftInset = (std::max)(
|
||||
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
|
||||
ClampNonNegative(metrics.labelInsetX));
|
||||
@@ -349,12 +395,9 @@ void AppendUIEditorTabStripForeground(
|
||||
for (std::size_t index = 0; index < items.size() && index < layout.tabHeaderRects.size(); ++index) {
|
||||
const UIRect& tabRect = layout.tabHeaderRects[index];
|
||||
const bool selected = layout.selectedIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index || state.closeHoveredIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
const float textLeft = tabRect.x + leftInset;
|
||||
float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||
if (layout.showCloseButtons[index]) {
|
||||
textRight = layout.closeButtonRects[index].x - ClampNonNegative(metrics.closeButtonGap);
|
||||
}
|
||||
const float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||
|
||||
if (textRight > textLeft) {
|
||||
const UIRect clipRect(
|
||||
@@ -370,30 +413,6 @@ void AppendUIEditorTabStripForeground(
|
||||
kHeaderFontSize);
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
|
||||
if (!layout.showCloseButtons[index]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool closeHovered = state.closeHoveredIndex == index;
|
||||
const UIRect& closeRect = layout.closeButtonRects[index];
|
||||
const float closeRounding = ResolveCloseButtonRounding(metrics);
|
||||
drawList.AddFilledRect(
|
||||
closeRect,
|
||||
closeHovered ? palette.closeButtonHoveredColor : palette.closeButtonColor,
|
||||
closeRounding);
|
||||
drawList.AddRectOutline(
|
||||
closeRect,
|
||||
palette.closeButtonBorderColor,
|
||||
1.0f,
|
||||
closeRounding);
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
closeRect.x + (std::max)(0.0f, (closeRect.width - 7.0f) * 0.5f),
|
||||
ResolveCloseTextTop(closeRect)),
|
||||
"X",
|
||||
palette.closeGlyphColor,
|
||||
kCloseFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
@@ -12,6 +13,16 @@ using ::XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::Widgets::BeginUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::CancelUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::EndUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropOperation;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropPayload;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropResult;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropSourceDescriptor;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropTargetDescriptor;
|
||||
using ::XCEngine::UI::Widgets::UpdateUIDragDropPointer;
|
||||
using ::XCEngine::UI::Widgets::UpdateUIDragDropTarget;
|
||||
using Widgets::BuildUIEditorTabStripLayout;
|
||||
using Widgets::HitTestUIEditorTabStrip;
|
||||
using Widgets::ResolveUIEditorTabStripSelectedIndex;
|
||||
@@ -19,6 +30,9 @@ using Widgets::UIEditorTabStripHitTarget;
|
||||
using Widgets::UIEditorTabStripHitTargetKind;
|
||||
using Widgets::UIEditorTabStripInvalidIndex;
|
||||
|
||||
constexpr ::XCEngine::UI::UIElementId kTabStripDragOwnerId = 0x58435441u;
|
||||
constexpr std::string_view kTabStripDragPayloadType = "xc.editor.tab";
|
||||
|
||||
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
@@ -53,6 +67,14 @@ void ClearHoverState(UIEditorTabStripInteractionState& state) {
|
||||
state.tabStripState.closeHoveredIndex = UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
void ClearReorderState(UIEditorTabStripInteractionState& state) {
|
||||
state.tabStripState.reorder = {};
|
||||
state.reorderDragState = {};
|
||||
state.reorderSourceIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderCaptureActive = false;
|
||||
}
|
||||
|
||||
void SyncHoverTarget(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
const Widgets::UIEditorTabStripLayout& layout) {
|
||||
@@ -68,11 +90,6 @@ void SyncHoverTarget(
|
||||
}
|
||||
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorTabStripHitTargetKind::CloseButton:
|
||||
state.tabStripState.hoveredIndex = hitTarget.index;
|
||||
state.tabStripState.closeHoveredIndex = hitTarget.index;
|
||||
break;
|
||||
|
||||
case UIEditorTabStripHitTargetKind::Tab:
|
||||
state.tabStripState.hoveredIndex = hitTarget.index;
|
||||
break;
|
||||
@@ -145,6 +162,205 @@ bool ApplyKeyboardNavigation(
|
||||
}
|
||||
}
|
||||
|
||||
bool HasReorderInteraction(const UIEditorTabStripInteractionState& state) {
|
||||
return state.reorderDragState.armed || state.reorderDragState.active;
|
||||
}
|
||||
|
||||
std::size_t ResolveVisibleDropInsertionIndex(
|
||||
const Widgets::UIEditorTabStripLayout& layout,
|
||||
const ::XCEngine::UI::UIPoint& pointerPosition) {
|
||||
if (!IsPointInside(layout.headerRect, pointerPosition)) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t insertionIndex = 0u;
|
||||
for (const ::XCEngine::UI::UIRect& rect : layout.tabHeaderRects) {
|
||||
const float midpoint = rect.x + rect.width * 0.5f;
|
||||
if (pointerPosition.x > midpoint) {
|
||||
++insertionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return insertionIndex;
|
||||
}
|
||||
|
||||
std::size_t ResolveReorderTargetIndex(
|
||||
std::size_t sourceIndex,
|
||||
std::size_t dropInsertionIndex,
|
||||
std::size_t itemCount) {
|
||||
if (sourceIndex >= itemCount || dropInsertionIndex > itemCount) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
if (dropInsertionIndex <= sourceIndex) {
|
||||
return dropInsertionIndex;
|
||||
}
|
||||
|
||||
return dropInsertionIndex - 1u;
|
||||
}
|
||||
|
||||
std::size_t ResolveDropInsertionIndexFromPreviewTargetIndex(
|
||||
std::size_t sourceIndex,
|
||||
std::size_t previewTargetIndex) {
|
||||
if (sourceIndex == UIEditorTabStripInvalidIndex ||
|
||||
previewTargetIndex == UIEditorTabStripInvalidIndex) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
return previewTargetIndex <= sourceIndex
|
||||
? previewTargetIndex
|
||||
: previewTargetIndex + 1u;
|
||||
}
|
||||
|
||||
std::size_t ResolveCommittedReorderInsertionIndex(
|
||||
const UIEditorTabStripInteractionState& state,
|
||||
const Widgets::UIEditorTabStripLayout& layout) {
|
||||
const std::size_t previewInsertionIndex =
|
||||
ResolveDropInsertionIndexFromPreviewTargetIndex(
|
||||
state.reorderSourceIndex,
|
||||
state.reorderPreviewIndex);
|
||||
if (previewInsertionIndex != UIEditorTabStripInvalidIndex) {
|
||||
return previewInsertionIndex;
|
||||
}
|
||||
|
||||
if (state.tabStripState.reorder.previewInsertionIndex != UIEditorTabStripInvalidIndex) {
|
||||
return state.tabStripState.reorder.previewInsertionIndex;
|
||||
}
|
||||
|
||||
if (!state.hasPointerPosition) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
return ResolveVisibleDropInsertionIndex(layout, state.pointerPosition);
|
||||
}
|
||||
|
||||
void SyncReorderPreview(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
const Widgets::UIEditorTabStripLayout& layout,
|
||||
const std::vector<Widgets::UIEditorTabStripItem>& items,
|
||||
UIEditorTabStripInteractionResult& result) {
|
||||
state.tabStripState.reorder.armed = state.reorderDragState.armed;
|
||||
state.tabStripState.reorder.dragging = state.reorderDragState.active;
|
||||
state.tabStripState.reorder.sourceIndex = state.reorderSourceIndex;
|
||||
state.tabStripState.reorder.pressedIndex = state.reorderSourceIndex;
|
||||
state.tabStripState.reorder.pressPosition = state.reorderDragState.pointerDownPosition;
|
||||
result.dragSourceIndex = state.reorderSourceIndex;
|
||||
if (state.reorderSourceIndex < items.size()) {
|
||||
result.draggedTabId = items[state.reorderSourceIndex].tabId;
|
||||
}
|
||||
|
||||
if (!state.reorderDragState.active || !state.hasPointerPosition || items.size() < 2u) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
UpdateUIDragDropTarget(state.reorderDragState, nullptr, &dragDropResult);
|
||||
state.tabStripState.reorder.previewInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
result.dropInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
result.reorderToIndex = UIEditorTabStripInvalidIndex;
|
||||
result.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t dropInsertionIndex =
|
||||
ResolveVisibleDropInsertionIndex(layout, state.pointerPosition);
|
||||
if (dropInsertionIndex == UIEditorTabStripInvalidIndex) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
UpdateUIDragDropTarget(state.reorderDragState, nullptr, &dragDropResult);
|
||||
state.tabStripState.reorder.previewInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
result.dropInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
result.reorderToIndex = UIEditorTabStripInvalidIndex;
|
||||
result.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t reorderToIndex =
|
||||
ResolveReorderTargetIndex(
|
||||
state.reorderSourceIndex,
|
||||
dropInsertionIndex,
|
||||
items.size());
|
||||
if (reorderToIndex == UIEditorTabStripInvalidIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr std::array<std::string_view, 1> kAcceptedPayloadTypes = {
|
||||
kTabStripDragPayloadType
|
||||
};
|
||||
const std::string targetId = "insert:" + std::to_string(dropInsertionIndex);
|
||||
UIDragDropTargetDescriptor target = {};
|
||||
target.ownerId = kTabStripDragOwnerId;
|
||||
target.targetId = targetId;
|
||||
target.acceptedPayloadTypes = kAcceptedPayloadTypes;
|
||||
target.acceptedOperations = UIDragDropOperation::Move;
|
||||
target.preferredOperation = UIDragDropOperation::Move;
|
||||
|
||||
UIDragDropResult dragDropResult = {};
|
||||
UpdateUIDragDropTarget(state.reorderDragState, &target, &dragDropResult);
|
||||
|
||||
state.tabStripState.reorder.previewInsertionIndex = dropInsertionIndex;
|
||||
state.reorderPreviewIndex = reorderToIndex;
|
||||
result.reorderPreviewActive = true;
|
||||
result.dropInsertionIndex = dropInsertionIndex;
|
||||
result.reorderToIndex = reorderToIndex;
|
||||
result.reorderPreviewIndex = reorderToIndex;
|
||||
}
|
||||
|
||||
void BeginTabReorder(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
const std::vector<Widgets::UIEditorTabStripItem>& items,
|
||||
std::size_t sourceIndex) {
|
||||
if (sourceIndex >= items.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIDragDropSourceDescriptor source = {};
|
||||
source.ownerId = kTabStripDragOwnerId;
|
||||
source.sourceId = items[sourceIndex].tabId;
|
||||
source.pointerDownPosition = state.pointerPosition;
|
||||
source.payload = UIDragDropPayload{
|
||||
std::string(kTabStripDragPayloadType),
|
||||
items[sourceIndex].tabId,
|
||||
items[sourceIndex].title
|
||||
};
|
||||
source.allowedOperations = UIDragDropOperation::Move;
|
||||
source.activationDistance = 4.0f;
|
||||
|
||||
UIDragDropResult dragDropResult = {};
|
||||
if (BeginUIDragDrop(source, state.reorderDragState, &dragDropResult)) {
|
||||
state.tabStripState.reorder.pressPosition = state.pointerPosition;
|
||||
state.tabStripState.reorder.pressedIndex = sourceIndex;
|
||||
state.tabStripState.reorder.sourceIndex = sourceIndex;
|
||||
state.tabStripState.reorder.previewInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
state.tabStripState.reorder.armed = true;
|
||||
state.tabStripState.reorder.dragging = false;
|
||||
state.reorderSourceIndex = sourceIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void CancelTabReorder(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
UIEditorTabStripInteractionResult& result,
|
||||
const std::vector<Widgets::UIEditorTabStripItem>& items) {
|
||||
if (!HasReorderInteraction(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.dragCanceled = true;
|
||||
result.consumed = state.reorderCaptureActive || state.reorderDragState.active;
|
||||
result.dragSourceIndex = state.reorderSourceIndex;
|
||||
result.reorderPreviewIndex = state.reorderPreviewIndex;
|
||||
if (state.reorderSourceIndex < items.size()) {
|
||||
result.draggedTabId = items[state.reorderSourceIndex].tabId;
|
||||
}
|
||||
if (state.reorderCaptureActive) {
|
||||
result.releasePointerCapture = true;
|
||||
}
|
||||
|
||||
UIDragDropResult dragDropResult = {};
|
||||
CancelUIDragDrop(state.reorderDragState, &dragDropResult);
|
||||
ClearReorderState(state);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
@@ -184,11 +400,33 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
state.hasPointerPosition = false;
|
||||
state.pressedTarget = {};
|
||||
ClearHoverState(state);
|
||||
CancelTabReorder(state, eventResult, items);
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
if (HasReorderInteraction(state)) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
UpdateUIDragDropPointer(state.reorderDragState, state.pointerPosition, &dragDropResult);
|
||||
if (dragDropResult.activated && !state.reorderCaptureActive) {
|
||||
state.reorderCaptureActive = true;
|
||||
eventResult.requestPointerCapture = true;
|
||||
eventResult.dragStarted = true;
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
|
||||
SyncReorderPreview(state, layout, items, eventResult);
|
||||
if (state.reorderDragState.active) {
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerLeave:
|
||||
if (state.reorderDragState.active) {
|
||||
SyncReorderPreview(state, layout, items, eventResult);
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonDown: {
|
||||
@@ -201,6 +439,9 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
(state.hasPointerPosition && IsPointInside(layout.bounds, state.pointerPosition))) {
|
||||
state.tabStripState.focused = true;
|
||||
eventResult.consumed = true;
|
||||
if (eventResult.hitTarget.kind == UIEditorTabStripHitTargetKind::Tab) {
|
||||
BeginTabReorder(state, items, eventResult.hitTarget.index);
|
||||
}
|
||||
} else {
|
||||
state.tabStripState.focused = false;
|
||||
state.pressedTarget = {};
|
||||
@@ -218,22 +459,49 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
const bool matchedPressedTarget =
|
||||
AreEquivalentTargets(state.pressedTarget, eventResult.hitTarget);
|
||||
|
||||
if (state.reorderDragState.active || state.reorderCaptureActive) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
EndUIDragDrop(state.reorderDragState, dragDropResult);
|
||||
|
||||
eventResult.dragEnded = state.reorderCaptureActive;
|
||||
eventResult.releasePointerCapture = state.reorderCaptureActive;
|
||||
eventResult.consumed = true;
|
||||
eventResult.dragSourceIndex = state.reorderSourceIndex;
|
||||
if (state.reorderSourceIndex < items.size()) {
|
||||
eventResult.draggedTabId = items[state.reorderSourceIndex].tabId;
|
||||
}
|
||||
eventResult.dropInsertionIndex =
|
||||
ResolveCommittedReorderInsertionIndex(state, layout);
|
||||
eventResult.reorderToIndex =
|
||||
ResolveReorderTargetIndex(
|
||||
state.reorderSourceIndex,
|
||||
eventResult.dropInsertionIndex,
|
||||
items.size());
|
||||
eventResult.reorderPreviewIndex = state.reorderPreviewIndex;
|
||||
if (dragDropResult.completed &&
|
||||
eventResult.dropInsertionIndex != UIEditorTabStripInvalidIndex &&
|
||||
eventResult.reorderToIndex != UIEditorTabStripInvalidIndex) {
|
||||
eventResult.reorderRequested =
|
||||
eventResult.dropInsertionIndex != state.reorderSourceIndex &&
|
||||
eventResult.dropInsertionIndex != state.reorderSourceIndex + 1u;
|
||||
} else {
|
||||
eventResult.dragCanceled = true;
|
||||
}
|
||||
|
||||
ClearReorderState(state);
|
||||
state.pressedTarget = {};
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.reorderDragState.armed) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
CancelUIDragDrop(state.reorderDragState, &dragDropResult);
|
||||
state.reorderSourceIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
if (matchedPressedTarget) {
|
||||
switch (eventResult.hitTarget.kind) {
|
||||
case UIEditorTabStripHitTargetKind::CloseButton:
|
||||
if (eventResult.hitTarget.index < items.size() &&
|
||||
items[eventResult.hitTarget.index].closable) {
|
||||
eventResult.closeRequested = true;
|
||||
eventResult.closedTabId = items[eventResult.hitTarget.index].tabId;
|
||||
eventResult.closedIndex = eventResult.hitTarget.index;
|
||||
eventResult.consumed = true;
|
||||
state.tabStripState.focused = true;
|
||||
} else if (insideStrip) {
|
||||
state.tabStripState.focused = true;
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIEditorTabStripHitTargetKind::Tab:
|
||||
SelectTab(
|
||||
state,
|
||||
@@ -271,6 +539,12 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
}
|
||||
|
||||
case UIInputEventType::KeyDown:
|
||||
if (HasReorderInteraction(state) &&
|
||||
static_cast<KeyCode>(event.keyCode) == KeyCode::Escape) {
|
||||
CancelTabReorder(state, eventResult, items);
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.tabStripState.focused &&
|
||||
!HasNavigationModifiers(event.modifiers) &&
|
||||
ApplyKeyboardNavigation(state, event.keyCode) &&
|
||||
@@ -299,13 +573,25 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition);
|
||||
}
|
||||
|
||||
if (state.reorderDragState.active) {
|
||||
SyncReorderPreview(state, layout, items, eventResult);
|
||||
}
|
||||
|
||||
if (eventResult.consumed ||
|
||||
eventResult.selectionChanged ||
|
||||
eventResult.closeRequested ||
|
||||
eventResult.keyboardNavigated ||
|
||||
eventResult.requestPointerCapture ||
|
||||
eventResult.releasePointerCapture ||
|
||||
eventResult.dragStarted ||
|
||||
eventResult.dragEnded ||
|
||||
eventResult.dragCanceled ||
|
||||
eventResult.reorderRequested ||
|
||||
eventResult.reorderPreviewActive ||
|
||||
eventResult.hitTarget.kind != UIEditorTabStripHitTargetKind::None ||
|
||||
!eventResult.selectedTabId.empty() ||
|
||||
!eventResult.closedTabId.empty()) {
|
||||
!eventResult.closedTabId.empty() ||
|
||||
!eventResult.draggedTabId.empty()) {
|
||||
interactionResult = std::move(eventResult);
|
||||
}
|
||||
}
|
||||
@@ -318,6 +604,9 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
interactionResult.hitTarget =
|
||||
HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition);
|
||||
}
|
||||
if (state.reorderDragState.active) {
|
||||
SyncReorderPreview(state, layout, items, interactionResult);
|
||||
}
|
||||
|
||||
return {
|
||||
std::move(layout),
|
||||
|
||||
130
new_editor/src/Foundation/UIEditorRuntimeTrace.cpp
Normal file
130
new_editor/src/Foundation/UIEditorRuntimeTrace.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex g_traceMutex = {};
|
||||
std::filesystem::path g_logRoot = {};
|
||||
std::filesystem::path g_runtimeTracePath = {};
|
||||
std::filesystem::path g_crashTracePath = {};
|
||||
bool g_traceInitialized = false;
|
||||
|
||||
std::string BuildTimestampString() {
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const std::time_t currentTime = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm localTime = {};
|
||||
localtime_s(&localTime, ¤tTime);
|
||||
|
||||
const auto milliseconds =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch()) %
|
||||
1000;
|
||||
|
||||
std::ostringstream stream = {};
|
||||
stream << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S")
|
||||
<< '.'
|
||||
<< std::setw(3)
|
||||
<< std::setfill('0')
|
||||
<< milliseconds.count();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
void AppendTraceLine(
|
||||
const std::filesystem::path& path,
|
||||
std::string_view channel,
|
||||
std::string_view message) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(path.parent_path(), errorCode);
|
||||
std::ofstream stream(path, std::ios::out | std::ios::app);
|
||||
if (!stream.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
stream << '['
|
||||
<< BuildTimestampString()
|
||||
<< "] ["
|
||||
<< channel
|
||||
<< "] "
|
||||
<< message
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void InitializeUIEditorRuntimeTrace(const std::filesystem::path& logRoot) {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
g_logRoot = logRoot.lexically_normal();
|
||||
g_runtimeTracePath = (g_logRoot / "runtime.log").lexically_normal();
|
||||
g_crashTracePath = (g_logRoot / "crash.log").lexically_normal();
|
||||
g_traceInitialized = true;
|
||||
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(g_logRoot, errorCode);
|
||||
AppendTraceLine(g_runtimeTracePath, "trace", "trace session started");
|
||||
}
|
||||
|
||||
void ShutdownUIEditorRuntimeTrace() {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
if (!g_traceInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppendTraceLine(g_runtimeTracePath, "trace", "trace session ended");
|
||||
g_traceInitialized = false;
|
||||
}
|
||||
|
||||
void AppendUIEditorRuntimeTrace(
|
||||
std::string_view channel,
|
||||
std::string_view message) {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
if (!g_traceInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppendTraceLine(g_runtimeTracePath, channel, message);
|
||||
}
|
||||
|
||||
void AppendUIEditorCrashTrace(
|
||||
std::uint32_t exceptionCode,
|
||||
const void* exceptionAddress) {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
if (!g_traceInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[128] = {};
|
||||
std::snprintf(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
"Unhandled exception code=0x%08X address=%p",
|
||||
exceptionCode,
|
||||
exceptionAddress);
|
||||
AppendTraceLine(g_crashTracePath, "crash", buffer);
|
||||
AppendTraceLine(g_runtimeTracePath, "crash", buffer);
|
||||
}
|
||||
|
||||
std::filesystem::path GetUIEditorRuntimeTracePath() {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
return g_runtimeTracePath;
|
||||
}
|
||||
|
||||
std::filesystem::path GetUIEditorCrashTracePath() {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
return g_crashTracePath;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -501,6 +501,91 @@ UIColor ResolveSplitterColor(const UIEditorDockHostSplitterLayout& splitter, con
|
||||
return palette.splitterColor;
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackLayoutByNodeId(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIRect InsetRect(const UIRect& rect, float inset) {
|
||||
const float clampedInset = ClampNonNegative(inset);
|
||||
const float insetX = (std::min)(clampedInset, rect.width * 0.5f);
|
||||
const float insetY = (std::min)(clampedInset, rect.height * 0.5f);
|
||||
return UIRect(
|
||||
rect.x + insetX,
|
||||
rect.y + insetY,
|
||||
(std::max)(0.0f, rect.width - insetX * 2.0f),
|
||||
(std::max)(0.0f, rect.height - insetY * 2.0f));
|
||||
}
|
||||
|
||||
UIRect ResolveDropPreviewRect(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
UIEditorWorkspaceDockPlacement placement) {
|
||||
const UIRect bounds = tabStack.bounds;
|
||||
switch (placement) {
|
||||
case UIEditorWorkspaceDockPlacement::Left:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width * 0.35f,
|
||||
bounds.height);
|
||||
case UIEditorWorkspaceDockPlacement::Right:
|
||||
return UIRect(
|
||||
bounds.x + bounds.width * 0.65f,
|
||||
bounds.y,
|
||||
bounds.width * 0.35f,
|
||||
bounds.height);
|
||||
case UIEditorWorkspaceDockPlacement::Top:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width,
|
||||
bounds.height * 0.35f);
|
||||
case UIEditorWorkspaceDockPlacement::Bottom:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y + bounds.height * 0.65f,
|
||||
bounds.width,
|
||||
bounds.height * 0.35f);
|
||||
case UIEditorWorkspaceDockPlacement::Center:
|
||||
default:
|
||||
return InsetRect(bounds, 4.0f);
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorDockHostDropPreviewLayout ResolveDropPreviewLayout(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostState& state) {
|
||||
UIEditorDockHostDropPreviewLayout preview = {};
|
||||
if (!state.dropPreview.visible || state.dropPreview.targetNodeId.empty()) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* targetTabStack =
|
||||
FindTabStackLayoutByNodeId(layout, state.dropPreview.targetNodeId);
|
||||
if (targetTabStack == nullptr) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
preview.visible = true;
|
||||
preview.targetNodeId = state.dropPreview.targetNodeId;
|
||||
preview.placement = state.dropPreview.placement;
|
||||
preview.insertionIndex = state.dropPreview.insertionIndex;
|
||||
preview.previewRect =
|
||||
ResolveDropPreviewRect(*targetTabStack, state.dropPreview.placement);
|
||||
if (preview.previewRect.width <= 0.0f || preview.previewRect.height <= 0.0f) {
|
||||
preview = {};
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
|
||||
@@ -539,6 +624,7 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
|
||||
state,
|
||||
metrics,
|
||||
layout);
|
||||
layout.dropPreview = ResolveDropPreviewLayout(layout, state);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -607,8 +693,6 @@ void AppendUIEditorDockHostBackground(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
||||
BuildTabContentFrameMetrics(metrics);
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||
tabItems.reserve(tabStack.items.size());
|
||||
@@ -626,12 +710,6 @@ void AppendUIEditorDockHostBackground(
|
||||
tabStack.tabStripState,
|
||||
palette.tabStripPalette,
|
||||
metrics.tabStripMetrics);
|
||||
AppendUIEditorPanelFrameBackground(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
palette.panelFramePalette,
|
||||
tabContentFrameMetrics);
|
||||
}
|
||||
|
||||
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
|
||||
@@ -648,8 +726,6 @@ void AppendUIEditorDockHostForeground(
|
||||
const UIEditorDockHostForegroundOptions& options,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
||||
BuildTabContentFrameMetrics(metrics);
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||
tabItems.reserve(tabStack.items.size());
|
||||
@@ -668,18 +744,21 @@ void AppendUIEditorDockHostForeground(
|
||||
tabStack.tabStripState,
|
||||
palette.tabStripPalette,
|
||||
metrics.tabStripMetrics);
|
||||
AppendUIEditorPanelFrameForeground(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
{},
|
||||
palette.panelFramePalette,
|
||||
tabContentFrameMetrics);
|
||||
|
||||
if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (layout.dropPreview.visible) {
|
||||
drawList.AddFilledRect(
|
||||
layout.dropPreview.previewRect,
|
||||
palette.dropPreviewFillColor);
|
||||
drawList.AddRectOutline(
|
||||
layout.dropPreview.previewRect,
|
||||
palette.dropPreviewBorderColor,
|
||||
1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorDockHostForeground(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -13,6 +15,7 @@ namespace {
|
||||
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Widgets::BeginUISplitterDrag;
|
||||
using ::XCEngine::UI::Widgets::EndUISplitterDrag;
|
||||
@@ -30,8 +33,17 @@ using Widgets::UIEditorTabStripItem;
|
||||
struct DockHostTabStripEventResult {
|
||||
bool consumed = false;
|
||||
bool commandRequested = false;
|
||||
bool reorderRequested = false;
|
||||
bool dragStarted = false;
|
||||
bool dragEnded = false;
|
||||
bool dragCanceled = false;
|
||||
bool requestPointerCapture = false;
|
||||
bool releasePointerCapture = false;
|
||||
UIEditorWorkspaceCommandKind commandKind = UIEditorWorkspaceCommandKind::ActivatePanel;
|
||||
std::size_t dropInsertionIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::string panelId = {};
|
||||
std::string nodeId = {};
|
||||
std::string draggedTabId = {};
|
||||
UIEditorDockHostHitTarget hitTarget = {};
|
||||
int priority = 0;
|
||||
};
|
||||
@@ -130,6 +142,13 @@ void PruneTabStripInteractionEntries(
|
||||
return !isVisibleNodeId(entry.nodeId);
|
||||
}),
|
||||
state.dockHostState.tabStripStates.end());
|
||||
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
!isVisibleNodeId(state.activeTabDragNodeId)) {
|
||||
state.activeTabDragNodeId.clear();
|
||||
state.activeTabDragPanelId.clear();
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
}
|
||||
|
||||
void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state) {
|
||||
@@ -152,6 +171,33 @@ bool HasFocusedTabStrip(const UIEditorDockHostInteractionState& state) {
|
||||
}) != state.tabStripInteractions.end();
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackLayoutByNodeId(
|
||||
const Widgets::UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsPointInsideRect(
|
||||
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;
|
||||
}
|
||||
|
||||
void ClearTabDockDragState(UIEditorDockHostInteractionState& state) {
|
||||
state.activeTabDragNodeId.clear();
|
||||
state.activeTabDragPanelId.clear();
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
|
||||
std::vector<UIEditorTabStripItem> BuildTabStripItems(
|
||||
const UIEditorDockHostTabStackLayout& tabStack) {
|
||||
std::vector<UIEditorTabStripItem> items = {};
|
||||
@@ -198,6 +244,13 @@ UIEditorDockHostHitTarget MapTabStripHitTarget(
|
||||
}
|
||||
|
||||
int ResolveTabStripPriority(const UIEditorTabStripInteractionResult& result) {
|
||||
if (result.reorderRequested ||
|
||||
result.dragStarted ||
|
||||
result.dragEnded ||
|
||||
result.dragCanceled) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (result.closeRequested) {
|
||||
return 4;
|
||||
}
|
||||
@@ -221,6 +274,11 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
DockHostTabStripEventResult resolved = {};
|
||||
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
tabStack.nodeId != state.activeTabDragNodeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorDockHostTabStripInteractionEntry& entry =
|
||||
FindOrCreateTabStripInteractionEntry(state, tabStack.nodeId);
|
||||
std::string selectedTabId = tabStack.selectedPanelId;
|
||||
@@ -238,7 +296,15 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
continue;
|
||||
}
|
||||
|
||||
resolved.nodeId = tabStack.nodeId;
|
||||
resolved.hitTarget = MapTabStripHitTarget(tabStack, frame.result);
|
||||
resolved.requestPointerCapture = frame.result.requestPointerCapture;
|
||||
resolved.releasePointerCapture = frame.result.releasePointerCapture;
|
||||
resolved.dragStarted = frame.result.dragStarted;
|
||||
resolved.dragEnded = frame.result.dragEnded;
|
||||
resolved.dragCanceled = frame.result.dragCanceled;
|
||||
resolved.dropInsertionIndex = frame.result.dropInsertionIndex;
|
||||
resolved.draggedTabId = frame.result.draggedTabId;
|
||||
if ((frame.result.closeRequested && !frame.result.closedTabId.empty()) ||
|
||||
(event.type == UIInputEventType::PointerButtonUp &&
|
||||
frame.result.consumed &&
|
||||
@@ -250,6 +316,12 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
!frame.result.closedTabId.empty()
|
||||
? frame.result.closedTabId
|
||||
: resolved.hitTarget.panelId;
|
||||
} else if (frame.result.reorderRequested &&
|
||||
!frame.result.draggedTabId.empty() &&
|
||||
frame.result.dropInsertionIndex != Widgets::UIEditorTabStripInvalidIndex) {
|
||||
resolved.reorderRequested = true;
|
||||
resolved.panelId = frame.result.draggedTabId;
|
||||
resolved.dropInsertionIndex = frame.result.dropInsertionIndex;
|
||||
} else if ((frame.result.selectionChanged ||
|
||||
frame.result.keyboardNavigated ||
|
||||
(event.type == UIInputEventType::PointerButtonUp &&
|
||||
@@ -266,6 +338,7 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
continue;
|
||||
} else {
|
||||
resolved.commandRequested = false;
|
||||
resolved.reorderRequested = false;
|
||||
resolved.panelId.clear();
|
||||
}
|
||||
|
||||
@@ -277,6 +350,103 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
return resolved;
|
||||
}
|
||||
|
||||
std::size_t ResolveTabHeaderDropInsertionIndex(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
const UIPoint& point) {
|
||||
if (!IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) {
|
||||
return Widgets::UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t insertionIndex = 0u;
|
||||
for (const UIRect& rect : tabStack.tabStripLayout.tabHeaderRects) {
|
||||
const float midpoint = rect.x + rect.width * 0.5f;
|
||||
if (point.x > midpoint) {
|
||||
++insertionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return insertionIndex;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceDockPlacement ResolveDockPlacement(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
const UIPoint& point) {
|
||||
if (IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) {
|
||||
return UIEditorWorkspaceDockPlacement::Center;
|
||||
}
|
||||
|
||||
const float leftDistance = point.x - tabStack.bounds.x;
|
||||
const float rightDistance =
|
||||
tabStack.bounds.x + tabStack.bounds.width - point.x;
|
||||
const float topDistance = point.y - tabStack.bounds.y;
|
||||
const float bottomDistance =
|
||||
tabStack.bounds.y + tabStack.bounds.height - point.y;
|
||||
const float minHorizontalThreshold = tabStack.bounds.width * 0.25f;
|
||||
const float minVerticalThreshold = tabStack.bounds.height * 0.25f;
|
||||
const float nearestEdge =
|
||||
(std::min)((std::min)(leftDistance, rightDistance), (std::min)(topDistance, bottomDistance));
|
||||
|
||||
if (nearestEdge == leftDistance && leftDistance <= minHorizontalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Left;
|
||||
}
|
||||
if (nearestEdge == rightDistance && rightDistance <= minHorizontalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Right;
|
||||
}
|
||||
if (nearestEdge == topDistance && topDistance <= minVerticalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Top;
|
||||
}
|
||||
if (nearestEdge == bottomDistance && bottomDistance <= minVerticalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Bottom;
|
||||
}
|
||||
|
||||
return UIEditorWorkspaceDockPlacement::Center;
|
||||
}
|
||||
|
||||
void SyncDockPreview(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout) {
|
||||
state.dockHostState.dropPreview = {};
|
||||
if (state.activeTabDragNodeId.empty() ||
|
||||
state.activeTabDragPanelId.empty() ||
|
||||
!state.hasPointerPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (!IsPointInsideRect(tabStack.bounds, state.pointerPosition)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceDockPlacement placement =
|
||||
ResolveDockPlacement(tabStack, state.pointerPosition);
|
||||
if (tabStack.nodeId == state.activeTabDragNodeId &&
|
||||
placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabStack.nodeId == state.activeTabDragNodeId &&
|
||||
tabStack.items.size() <= 1u) {
|
||||
return;
|
||||
}
|
||||
|
||||
Widgets::UIEditorDockHostDropPreviewState preview = {};
|
||||
preview.visible = true;
|
||||
preview.sourceNodeId = state.activeTabDragNodeId;
|
||||
preview.sourcePanelId = state.activeTabDragPanelId;
|
||||
preview.targetNodeId = tabStack.nodeId;
|
||||
preview.placement = placement;
|
||||
if (placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
preview.insertionIndex =
|
||||
ResolveTabHeaderDropInsertionIndex(tabStack, state.pointerPosition);
|
||||
if (preview.insertionIndex == Widgets::UIEditorTabStripInvalidIndex) {
|
||||
preview.insertionIndex = tabStack.items.size();
|
||||
}
|
||||
}
|
||||
state.dockHostState.dropPreview = std::move(preview);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SyncHoverTarget(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout) {
|
||||
@@ -290,6 +460,11 @@ void SyncHoverTarget(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.activeTabDragNodeId.empty()) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.hasPointerPosition) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
return;
|
||||
@@ -340,6 +515,20 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
||||
? ProcessTabStripEvent(state, layout, event, metrics)
|
||||
: DockHostTabStripEventResult {};
|
||||
eventResult.requestPointerCapture = tabStripResult.requestPointerCapture;
|
||||
eventResult.releasePointerCapture = tabStripResult.releasePointerCapture;
|
||||
if (!tabStripResult.draggedTabId.empty() &&
|
||||
!state.activeTabDragNodeId.empty()) {
|
||||
state.activeTabDragPanelId = tabStripResult.draggedTabId;
|
||||
}
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
!state.activeTabDragPanelId.empty() &&
|
||||
!state.splitterDragState.active) {
|
||||
SyncDockPreview(state, layout);
|
||||
} else if (event.type == UIInputEventType::PointerLeave ||
|
||||
state.activeTabDragNodeId.empty()) {
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case UIInputEventType::FocusGained:
|
||||
@@ -355,6 +544,12 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture = true;
|
||||
}
|
||||
if (!state.activeTabDragNodeId.empty() || tabStripResult.dragCanceled) {
|
||||
ClearTabDockDragState(state);
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture =
|
||||
eventResult.releasePointerCapture || tabStripResult.releasePointerCapture;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
@@ -381,6 +576,21 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
eventResult.hitTarget.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
|
||||
eventResult.hitTarget.nodeId = state.dockHostState.activeSplitterNodeId;
|
||||
}
|
||||
} else if (tabStripResult.priority > 0) {
|
||||
eventResult.consumed = tabStripResult.consumed || tabStripResult.dragStarted;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
if (tabStripResult.dragStarted) {
|
||||
state.activeTabDragNodeId = tabStripResult.nodeId;
|
||||
state.activeTabDragPanelId = tabStripResult.draggedTabId;
|
||||
SyncDockPreview(state, layout);
|
||||
}
|
||||
if (tabStripResult.dragEnded || tabStripResult.dragCanceled) {
|
||||
ClearTabDockDragState(state);
|
||||
}
|
||||
if (eventResult.consumed ||
|
||||
eventResult.hitTarget.kind != UIEditorDockHostHitTargetKind::None) {
|
||||
state.dockHostState.focused = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -388,6 +598,7 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
if (!state.splitterDragState.active) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
}
|
||||
state.dockHostState.dropPreview = {};
|
||||
if (!HasFocusedTabStrip(state)) {
|
||||
state.dockHostState.focused = false;
|
||||
}
|
||||
@@ -464,6 +675,96 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.reorderRequested &&
|
||||
!tabStripResult.nodeId.empty() &&
|
||||
!tabStripResult.draggedTabId.empty() &&
|
||||
tabStripResult.dropInsertionIndex != Widgets::UIEditorTabStripInvalidIndex) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "same-stack reorder node=" << tabStripResult.nodeId
|
||||
<< " panel=" << tabStripResult.draggedTabId
|
||||
<< " insertion=" << tabStripResult.dropInsertionIndex;
|
||||
AppendUIEditorRuntimeTrace("dock", trace.str());
|
||||
}
|
||||
eventResult.layoutResult = controller.ReorderTab(
|
||||
tabStripResult.nodeId,
|
||||
tabStripResult.draggedTabId,
|
||||
tabStripResult.dropInsertionIndex);
|
||||
eventResult.layoutChanged =
|
||||
eventResult.layoutResult.status ==
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.dockHostState.dropPreview.visible &&
|
||||
!state.activeTabDragNodeId.empty() &&
|
||||
!state.activeTabDragPanelId.empty()) {
|
||||
const Widgets::UIEditorDockHostDropPreviewState preview =
|
||||
state.dockHostState.dropPreview;
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "drop commit sourceNode=" << state.activeTabDragNodeId
|
||||
<< " panel=" << state.activeTabDragPanelId
|
||||
<< " targetNode=" << preview.targetNodeId
|
||||
<< " placement=" << static_cast<int>(preview.placement)
|
||||
<< " insertion=" << preview.insertionIndex;
|
||||
AppendUIEditorRuntimeTrace("dock", trace.str());
|
||||
}
|
||||
if (preview.placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
std::size_t insertionIndex = preview.insertionIndex;
|
||||
if (insertionIndex == Widgets::UIEditorTabStripInvalidIndex) {
|
||||
if (const UIEditorDockHostTabStackLayout* targetTabStack =
|
||||
FindTabStackLayoutByNodeId(layout, preview.targetNodeId);
|
||||
targetTabStack != nullptr) {
|
||||
insertionIndex = targetTabStack->items.size();
|
||||
} else {
|
||||
insertionIndex = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
eventResult.layoutResult = controller.MoveTabToStack(
|
||||
state.activeTabDragNodeId,
|
||||
state.activeTabDragPanelId,
|
||||
preview.targetNodeId,
|
||||
insertionIndex);
|
||||
} else {
|
||||
eventResult.layoutResult = controller.DockTabRelative(
|
||||
state.activeTabDragNodeId,
|
||||
state.activeTabDragPanelId,
|
||||
preview.targetNodeId,
|
||||
preview.placement);
|
||||
}
|
||||
eventResult.layoutChanged =
|
||||
eventResult.layoutResult.status ==
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
AppendUIEditorRuntimeTrace(
|
||||
"dock",
|
||||
"drop result status=" +
|
||||
std::string(
|
||||
GetUIEditorWorkspaceLayoutOperationStatusName(
|
||||
eventResult.layoutResult.status)) +
|
||||
" message=" + eventResult.layoutResult.message);
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget.nodeId = preview.targetNodeId;
|
||||
eventResult.releasePointerCapture =
|
||||
eventResult.releasePointerCapture || tabStripResult.releasePointerCapture;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.dragEnded || tabStripResult.dragCanceled) {
|
||||
eventResult.consumed = tabStripResult.consumed;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
@@ -534,6 +835,14 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
break;
|
||||
|
||||
case UIInputEventType::KeyDown:
|
||||
if (tabStripResult.dragCanceled) {
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
|
||||
@@ -11,8 +11,6 @@ using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
constexpr float kMenuBarFontSize = 13.0f;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
@@ -24,6 +22,10 @@ bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) {
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool IsVisibleColor(const UIColor& color) {
|
||||
return color.a > 0.0f;
|
||||
}
|
||||
|
||||
float ResolveEstimatedLabelWidth(
|
||||
const UIEditorMenuBarItem& item,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
@@ -35,7 +37,9 @@ float ResolveEstimatedLabelWidth(
|
||||
}
|
||||
|
||||
float ResolveLabelTop(const UIRect& rect, const UIEditorMenuBarMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kMenuBarFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
return rect.y +
|
||||
(std::max)(0.0f, (rect.height - ClampNonNegative(metrics.labelFontSize)) * 0.5f) +
|
||||
metrics.labelInsetY;
|
||||
}
|
||||
|
||||
bool IsButtonFocused(
|
||||
@@ -164,27 +168,40 @@ void AppendUIEditorMenuBarBackground(
|
||||
const UIEditorMenuBarPalette& palette,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.barColor, metrics.barCornerRounding);
|
||||
const UIColor barBorderColor =
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor;
|
||||
const float barBorderThickness =
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness;
|
||||
if (IsVisibleColor(barBorderColor) && barBorderThickness > 0.0f) {
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness,
|
||||
barBorderColor,
|
||||
barBorderThickness,
|
||||
metrics.barCornerRounding);
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
|
||||
const bool open = state.openIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
const bool focused = IsButtonFocused(state, index);
|
||||
const UIColor buttonFillColor = ResolveButtonFillColor(open, hovered, palette);
|
||||
const UIColor buttonBorderColor = ResolveButtonBorderColor(open, focused, palette);
|
||||
const float buttonBorderThickness = ResolveButtonBorderThickness(open, focused, metrics);
|
||||
if (IsVisibleColor(buttonFillColor)) {
|
||||
drawList.AddFilledRect(
|
||||
layout.buttonRects[index],
|
||||
ResolveButtonFillColor(open, hovered, palette),
|
||||
buttonFillColor,
|
||||
metrics.buttonCornerRounding);
|
||||
}
|
||||
if (IsVisibleColor(buttonBorderColor) && buttonBorderThickness > 0.0f) {
|
||||
drawList.AddRectOutline(
|
||||
layout.buttonRects[index],
|
||||
ResolveButtonBorderColor(open, focused, palette),
|
||||
ResolveButtonBorderThickness(open, focused, metrics),
|
||||
buttonBorderColor,
|
||||
buttonBorderThickness,
|
||||
metrics.buttonCornerRounding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuBarForeground(
|
||||
UIDrawList& drawList,
|
||||
@@ -206,7 +223,7 @@ void AppendUIEditorMenuBarForeground(
|
||||
UIPoint(textLeft, ResolveLabelTop(rect, metrics)),
|
||||
items[index].label,
|
||||
items[index].enabled ? palette.textPrimary : palette.textDisabled,
|
||||
kMenuBarFontSize);
|
||||
ClampNonNegative(metrics.labelFontSize));
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +171,14 @@ void AppendUIEditorMenuPopupBackground(
|
||||
const UIRect& rect = layout.itemRects[index];
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
const float lineY = rect.y + rect.height * 0.5f;
|
||||
const float separatorInset =
|
||||
ClampNonNegative(metrics.contentPaddingX) + 3.0f;
|
||||
drawList.AddFilledRect(
|
||||
UIRect(rect.x + 8.0f, lineY, (std::max)(rect.width - 16.0f, 0.0f), metrics.separatorThickness),
|
||||
UIRect(
|
||||
rect.x + separatorInset,
|
||||
lineY,
|
||||
(std::max)(rect.width - separatorInset * 2.0f, 0.0f),
|
||||
metrics.separatorThickness),
|
||||
palette.separatorColor);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -191,6 +191,19 @@ void AppendUIEditorShellToolbar(
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
const std::vector<Widgets::UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorShellComposeMetrics& metrics) {
|
||||
return BuildUIEditorShellComposeLayout(
|
||||
bounds,
|
||||
menuBarItems,
|
||||
{},
|
||||
statusSegments,
|
||||
metrics);
|
||||
}
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
|
||||
@@ -148,7 +148,9 @@ const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(
|
||||
}
|
||||
|
||||
std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
||||
const UIEditorResolvedMenuModel& model) {
|
||||
const UIEditorResolvedMenuModel& model,
|
||||
const UIEditorShellInteractionServices& services,
|
||||
const Widgets::UIEditorMenuBarMetrics& metrics) {
|
||||
std::vector<Widgets::UIEditorMenuBarItem> items = {};
|
||||
items.reserve(model.menus.size());
|
||||
|
||||
@@ -157,6 +159,10 @@ std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
||||
item.menuId = menu.menuId;
|
||||
item.label = menu.label;
|
||||
item.enabled = !menu.items.empty();
|
||||
if (services.textMeasurer != nullptr && !item.label.empty()) {
|
||||
item.desiredLabelWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { item.label, metrics.labelFontSize });
|
||||
}
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
|
||||
@@ -175,7 +181,9 @@ UIEditorShellComposeModel BuildShellComposeModel(
|
||||
}
|
||||
|
||||
std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
||||
const std::vector<UIEditorResolvedMenuItem>& items) {
|
||||
const std::vector<UIEditorResolvedMenuItem>& items,
|
||||
const UIEditorShellInteractionServices& services,
|
||||
const Widgets::UIEditorMenuPopupMetrics& metrics) {
|
||||
std::vector<Widgets::UIEditorMenuPopupItem> widgetItems = {};
|
||||
widgetItems.reserve(items.size());
|
||||
|
||||
@@ -188,6 +196,16 @@ std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
||||
widgetItem.enabled = item.enabled;
|
||||
widgetItem.checked = item.checked;
|
||||
widgetItem.hasSubmenu = item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty();
|
||||
if (services.textMeasurer != nullptr) {
|
||||
if (!widgetItem.label.empty()) {
|
||||
widgetItem.desiredLabelWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { widgetItem.label, metrics.labelFontSize });
|
||||
}
|
||||
if (!widgetItem.shortcutText.empty()) {
|
||||
widgetItem.desiredShortcutWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { widgetItem.shortcutText, metrics.labelFontSize });
|
||||
}
|
||||
}
|
||||
widgetItems.push_back(std::move(widgetItem));
|
||||
}
|
||||
|
||||
@@ -244,10 +262,14 @@ BuildRequestOutput BuildRequest(
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShellInteractionModel& model,
|
||||
const UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionMetrics& metrics) {
|
||||
const UIEditorShellInteractionMetrics& metrics,
|
||||
const UIEditorShellInteractionServices& services) {
|
||||
BuildRequestOutput output = {};
|
||||
UIEditorShellInteractionRequest& request = output.request;
|
||||
request.menuBarItems = BuildMenuBarItems(model.resolvedMenuModel);
|
||||
request.menuBarItems = BuildMenuBarItems(
|
||||
model.resolvedMenuModel,
|
||||
services,
|
||||
metrics.shellMetrics.menuBarMetrics);
|
||||
|
||||
const UIEditorShellComposeModel shellModel =
|
||||
BuildShellComposeModel(model, request.menuBarItems);
|
||||
@@ -293,7 +315,10 @@ BuildRequestOutput BuildRequest(
|
||||
popupRequest.sourceItemId = popupState.itemId;
|
||||
popupRequest.overlayEntry = *overlayEntry;
|
||||
popupRequest.resolvedItems = *resolvedItems;
|
||||
popupRequest.widgetItems = BuildPopupWidgetItems(popupRequest.resolvedItems);
|
||||
popupRequest.widgetItems = BuildPopupWidgetItems(
|
||||
popupRequest.resolvedItems,
|
||||
services,
|
||||
metrics.popupMetrics);
|
||||
|
||||
const float popupWidth =
|
||||
ResolveUIEditorMenuPopupDesiredWidth(popupRequest.widgetItems, metrics.popupMetrics);
|
||||
@@ -506,7 +531,8 @@ UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
@@ -525,7 +551,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics);
|
||||
metrics,
|
||||
services);
|
||||
UIEditorShellInteractionRequest request = std::move(requestBuild.request);
|
||||
|
||||
if (requestBuild.hadInvalidPopupState && state.menuSession.HasOpenMenu()) {
|
||||
@@ -539,7 +566,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics);
|
||||
metrics,
|
||||
services);
|
||||
request = std::move(requestBuild.request);
|
||||
}
|
||||
|
||||
@@ -691,7 +719,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +743,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
|
||||
const RequestHit finalHit =
|
||||
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
|
||||
|
||||
@@ -110,21 +110,13 @@ UIColor ResolveSurfaceBorderColor(
|
||||
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) {
|
||||
if (state.inputCaptured) {
|
||||
return metrics.focusedSurfaceBorderThickness;
|
||||
}
|
||||
|
||||
@@ -356,8 +348,8 @@ void AppendUIEditorViewportSlotBackground(
|
||||
drawList.AddFilledRect(layout.bounds, palette.frameColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.outerBorderThickness,
|
||||
palette.borderColor,
|
||||
metrics.outerBorderThickness,
|
||||
metrics.cornerRounding);
|
||||
|
||||
if (layout.hasTopBar) {
|
||||
@@ -366,12 +358,6 @@ void AppendUIEditorViewportSlotBackground(
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
FindUIEditorPanelSessionState(session, panelId);
|
||||
return panelState != nullptr && panelState->open && panelState->visible;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
@@ -22,6 +32,58 @@ std::vector<std::string> CollectVisiblePanelIds(
|
||||
return ids;
|
||||
}
|
||||
|
||||
struct VisibleTabStackInfo {
|
||||
bool panelExists = false;
|
||||
bool panelVisible = false;
|
||||
std::size_t currentVisibleIndex = 0u;
|
||||
std::size_t visibleTabCount = 0u;
|
||||
};
|
||||
|
||||
VisibleTabStackInfo ResolveVisibleTabStackInfo(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
VisibleTabStackInfo info = {};
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool visible = IsPanelOpenAndVisible(session, child.panel.panelId);
|
||||
if (child.panel.panelId == panelId) {
|
||||
info.panelExists = true;
|
||||
info.panelVisible = visible;
|
||||
if (visible) {
|
||||
info.currentVisibleIndex = info.visibleTabCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
++info.visibleTabCount;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::size_t CountVisibleTabs(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
std::size_t visibleCount = 0u;
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisible(session, child.panel.panelId)) {
|
||||
++visibleCount;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleCount;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind) {
|
||||
@@ -297,6 +359,290 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRati
|
||||
"Split ratio updated.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::ReorderTab(
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (nodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab requires a tab stack node id.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab requires a panel id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* tabStack = FindUIEditorWorkspaceNode(m_workspace, nodeId);
|
||||
if (tabStack == nullptr || tabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo tabInfo =
|
||||
ResolveVisibleTabStackInfo(*tabStack, m_session, panelId);
|
||||
if (!tabInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target panel is missing from the specified tab stack.");
|
||||
}
|
||||
|
||||
if (!tabInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex > tabInfo.visibleTabCount) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target visible insertion index is out of range.");
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == tabInfo.currentVisibleIndex ||
|
||||
targetVisibleInsertionIndex == tabInfo.currentVisibleIndex + 1u) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Visible tab order already matches the requested insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryReorderUIEditorWorkspaceTab(
|
||||
m_workspace,
|
||||
m_session,
|
||||
nodeId,
|
||||
panelId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Tab reorder rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Visible tab order already matches the requested insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Tab reorder produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab reordered.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::MoveTabToStack(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "MoveTabToStack begin sourceNode=" << sourceNodeId
|
||||
<< " panel=" << panelId
|
||||
<< " targetNode=" << targetNodeId
|
||||
<< " insertion=" << targetVisibleInsertionIndex;
|
||||
AppendUIEditorRuntimeTrace("workspace", trace.str());
|
||||
}
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() || targetNodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires both source and target tab stack ids.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires a panel id.");
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId) {
|
||||
return ReorderTab(sourceNodeId, panelId, targetVisibleInsertionIndex);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, targetNodeId);
|
||||
if (sourceTabStack == nullptr ||
|
||||
targetTabStack == nullptr ||
|
||||
sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack source or target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo sourceInfo =
|
||||
ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId);
|
||||
if (!sourceInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack target panel is missing from the source tab stack.");
|
||||
}
|
||||
|
||||
if (!sourceInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
const std::size_t visibleTargetCount = CountVisibleTabs(*targetTabStack, m_session);
|
||||
if (targetVisibleInsertionIndex > visibleTargetCount) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack target visible insertion index is out of range.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryMoveUIEditorWorkspaceTabToStack(
|
||||
m_workspace,
|
||||
m_session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Tab already matches the requested target stack insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab moved to target stack.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::DockTabRelative(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "DockTabRelative begin sourceNode=" << sourceNodeId
|
||||
<< " panel=" << panelId
|
||||
<< " targetNode=" << targetNodeId
|
||||
<< " placement=" << static_cast<int>(placement)
|
||||
<< " splitRatio=" << splitRatio;
|
||||
AppendUIEditorRuntimeTrace("workspace", trace.str());
|
||||
}
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() || targetNodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative requires both source and target tab stack ids.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative requires a panel id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, targetNodeId);
|
||||
if (sourceTabStack == nullptr ||
|
||||
targetTabStack == nullptr ||
|
||||
sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative source or target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo sourceInfo =
|
||||
ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId);
|
||||
if (!sourceInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative target panel is missing from the source tab stack.");
|
||||
}
|
||||
|
||||
if (!sourceInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryDockUIEditorWorkspaceTabRelative(
|
||||
m_workspace,
|
||||
m_session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Dock layout already matches the requested placement.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab docked relative to target stack.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
|
||||
const UIEditorWorkspaceCommand& command) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
@@ -43,6 +44,19 @@ UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelN
|
||||
return tabStack;
|
||||
}
|
||||
|
||||
void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::Split ||
|
||||
node.children.size() != 1u) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the remaining child through a temporary object first. Assigning
|
||||
// directly from node.children.front() aliases a subobject of node and can
|
||||
// trigger use-after-move when the vector storage is torn down.
|
||||
UIEditorWorkspaceNode remainingChild = std::move(node.children.front());
|
||||
node = std::move(remainingChild);
|
||||
}
|
||||
|
||||
void CanonicalizeNodeRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
bool allowStandalonePanelLeaf) {
|
||||
@@ -67,7 +81,7 @@ void CanonicalizeNodeRecursive(
|
||||
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
||||
node.children.size() == 1u) {
|
||||
node = std::move(node.children.front());
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +145,256 @@ UIEditorWorkspaceNode* FindMutableNodeRecursive(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FindNodePathRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId,
|
||||
std::vector<std::size_t>& path) {
|
||||
if (node.nodeId == nodeId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
path.push_back(index);
|
||||
if (FindNodePathRecursive(node.children[index], nodeId, path)) {
|
||||
return true;
|
||||
}
|
||||
path.pop_back();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* ResolveMutableNodeByPath(
|
||||
UIEditorWorkspaceNode& node,
|
||||
const std::vector<std::size_t>& path) {
|
||||
UIEditorWorkspaceNode* current = &node;
|
||||
for (const std::size_t childIndex : path) {
|
||||
if (childIndex >= current->children.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
current = ¤t->children[childIndex];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
bool IsPanelOpenAndVisibleInSession(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId);
|
||||
return state != nullptr && state->open && state->visible;
|
||||
}
|
||||
|
||||
std::size_t CountVisibleChildren(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
std::size_t visibleCount = 0u;
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
++visibleCount;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleCount;
|
||||
}
|
||||
|
||||
std::size_t ResolveActualInsertionIndexForVisibleInsertion(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
std::vector<std::size_t> visibleIndices = {};
|
||||
visibleIndices.reserve(node.children.size());
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = node.children[index];
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
visibleIndices.push_back(index);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == 0u) {
|
||||
return visibleIndices.empty() ? 0u : visibleIndices.front();
|
||||
}
|
||||
|
||||
if (visibleIndices.empty()) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex >= visibleIndices.size()) {
|
||||
return visibleIndices.back() + 1u;
|
||||
}
|
||||
|
||||
return visibleIndices[targetVisibleInsertionIndex];
|
||||
}
|
||||
|
||||
void FixTabStackSelectedIndex(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view preferredPanelId) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack || node.children.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
if (node.children[index].kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
node.children[index].panel.panelId == preferredPanelId) {
|
||||
node.selectedTabIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.selectedTabIndex >= node.children.size()) {
|
||||
node.selectedTabIndex = node.children.size() - 1u;
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoveNodeByIdRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
if (node.children[index].nodeId == nodeId) {
|
||||
node.children.erase(node.children.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (node.children.size() == 1u) {
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (UIEditorWorkspaceNode& child : node.children) {
|
||||
if (RemoveNodeByIdRecursive(child, nodeId)) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
||||
node.children.size() == 1u) {
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
float ClampDockSplitRatio(float value) {
|
||||
constexpr float kMinRatio = 0.1f;
|
||||
constexpr float kMaxRatio = 0.9f;
|
||||
return (std::min)(kMaxRatio, (std::max)(kMinRatio, value));
|
||||
}
|
||||
|
||||
bool IsLeadingDockPlacement(UIEditorWorkspaceDockPlacement placement) {
|
||||
return placement == UIEditorWorkspaceDockPlacement::Left ||
|
||||
placement == UIEditorWorkspaceDockPlacement::Top;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSplitAxis ResolveDockSplitAxis(UIEditorWorkspaceDockPlacement placement) {
|
||||
return placement == UIEditorWorkspaceDockPlacement::Left ||
|
||||
placement == UIEditorWorkspaceDockPlacement::Right
|
||||
? UIEditorWorkspaceSplitAxis::Horizontal
|
||||
: UIEditorWorkspaceSplitAxis::Vertical;
|
||||
}
|
||||
|
||||
std::string MakeUniqueNodeId(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string base) {
|
||||
if (base.empty()) {
|
||||
base = "workspace-node";
|
||||
}
|
||||
|
||||
if (FindUIEditorWorkspaceNode(workspace, base) == nullptr) {
|
||||
return base;
|
||||
}
|
||||
|
||||
for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) {
|
||||
const std::string candidate = base + "-" + std::to_string(suffix);
|
||||
if (FindUIEditorWorkspaceNode(workspace, candidate) == nullptr) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return base + "-overflow";
|
||||
}
|
||||
|
||||
bool TryExtractVisiblePanelFromTabStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceNode& extractedPanel) {
|
||||
std::vector<std::size_t> sourcePath = {};
|
||||
if (!FindNodePathRecursive(workspace.root, sourceNodeId, sourcePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* sourceStack =
|
||||
ResolveMutableNodeByPath(workspace.root, sourcePath);
|
||||
if (sourceStack == nullptr ||
|
||||
sourceStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t panelIndex = sourceStack->children.size();
|
||||
for (std::size_t index = 0; index < sourceStack->children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = sourceStack->children[index];
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (child.panel.panelId == panelId) {
|
||||
if (!IsPanelOpenAndVisibleInSession(session, panelId)) {
|
||||
return false;
|
||||
}
|
||||
panelIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (panelIndex >= sourceStack->children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourcePath.empty() && sourceStack->children.size() == 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string fallbackSelectedPanelId = {};
|
||||
if (sourceStack->selectedTabIndex < sourceStack->children.size()) {
|
||||
fallbackSelectedPanelId =
|
||||
sourceStack->children[sourceStack->selectedTabIndex].panel.panelId;
|
||||
}
|
||||
|
||||
extractedPanel = std::move(sourceStack->children[panelIndex]);
|
||||
sourceStack->children.erase(
|
||||
sourceStack->children.begin() + static_cast<std::ptrdiff_t>(panelIndex));
|
||||
|
||||
if (sourceStack->children.empty()) {
|
||||
if (sourcePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RemoveNodeByIdRecursive(workspace.root, sourceNodeId)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (fallbackSelectedPanelId == panelId) {
|
||||
const std::size_t nextIndex =
|
||||
(std::min)(panelIndex, sourceStack->children.size() - 1u);
|
||||
fallbackSelectedPanelId =
|
||||
sourceStack->children[nextIndex].panel.panelId;
|
||||
}
|
||||
FixTabStackSelectedIndex(*sourceStack, fallbackSelectedPanelId);
|
||||
}
|
||||
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryActivateRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId) {
|
||||
@@ -484,4 +748,266 @@ bool TrySetUIEditorWorkspaceSplitRatio(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryReorderUIEditorWorkspaceTab(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
UIEditorWorkspaceNode* node = FindMutableNodeRecursive(workspace.root, nodeId);
|
||||
if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::size_t> visibleChildIndices = {};
|
||||
std::vector<UIEditorWorkspaceNode> reorderedVisibleChildren = {};
|
||||
visibleChildIndices.reserve(node->children.size());
|
||||
reorderedVisibleChildren.reserve(node->children.size());
|
||||
|
||||
std::size_t sourceVisibleIndex = node->children.size();
|
||||
for (std::size_t index = 0; index < node->children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = node->children[index];
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child.panel.panelId == panelId) {
|
||||
sourceVisibleIndex = visibleChildIndices.size();
|
||||
}
|
||||
|
||||
visibleChildIndices.push_back(index);
|
||||
reorderedVisibleChildren.push_back(child);
|
||||
}
|
||||
|
||||
if (sourceVisibleIndex >= reorderedVisibleChildren.size() ||
|
||||
targetVisibleInsertionIndex > reorderedVisibleChildren.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == sourceVisibleIndex ||
|
||||
targetVisibleInsertionIndex == sourceVisibleIndex + 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode movedChild =
|
||||
std::move(reorderedVisibleChildren[sourceVisibleIndex]);
|
||||
reorderedVisibleChildren.erase(
|
||||
reorderedVisibleChildren.begin() + static_cast<std::ptrdiff_t>(sourceVisibleIndex));
|
||||
|
||||
std::size_t adjustedInsertionIndex = targetVisibleInsertionIndex;
|
||||
if (adjustedInsertionIndex > sourceVisibleIndex) {
|
||||
--adjustedInsertionIndex;
|
||||
}
|
||||
if (adjustedInsertionIndex > reorderedVisibleChildren.size()) {
|
||||
adjustedInsertionIndex = reorderedVisibleChildren.size();
|
||||
}
|
||||
|
||||
reorderedVisibleChildren.insert(
|
||||
reorderedVisibleChildren.begin() +
|
||||
static_cast<std::ptrdiff_t>(adjustedInsertionIndex),
|
||||
std::move(movedChild));
|
||||
|
||||
std::string selectedPanelId = {};
|
||||
if (node->selectedTabIndex < node->children.size()) {
|
||||
selectedPanelId = node->children[node->selectedTabIndex].panel.panelId;
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceNode> originalChildren = node->children;
|
||||
std::size_t nextVisibleIndex = 0u;
|
||||
for (std::size_t index = 0; index < originalChildren.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& originalChild = originalChildren[index];
|
||||
if (!IsPanelOpenAndVisibleInSession(session, originalChild.panel.panelId)) {
|
||||
node->children[index] = originalChild;
|
||||
continue;
|
||||
}
|
||||
|
||||
node->children[index] = reorderedVisibleChildren[nextVisibleIndex];
|
||||
++nextVisibleIndex;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node->children.size(); ++index) {
|
||||
if (node->children[index].panel.panelId == selectedPanelId) {
|
||||
node->selectedTabIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMoveUIEditorWorkspaceTabToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
if (sourceNodeId.empty() ||
|
||||
panelId.empty() ||
|
||||
targetNodeId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId) {
|
||||
return TryReorderUIEditorWorkspaceTab(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetVisibleInsertionIndex);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (targetNode == nullptr ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex > CountVisibleChildren(*targetNode, session)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode extractedPanel = {};
|
||||
if (!TryExtractVisiblePanelFromTabStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t actualInsertionIndex =
|
||||
ResolveActualInsertionIndexForVisibleInsertion(
|
||||
*targetStack,
|
||||
session,
|
||||
targetVisibleInsertionIndex);
|
||||
if (actualInsertionIndex > targetStack->children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
targetStack->children.insert(
|
||||
targetStack->children.begin() +
|
||||
static_cast<std::ptrdiff_t>(actualInsertionIndex),
|
||||
std::move(extractedPanel));
|
||||
targetStack->selectedTabIndex = actualInsertionIndex;
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryDockUIEditorWorkspaceTabRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
if (placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (targetNode == nullptr ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryMoveUIEditorWorkspaceTabToStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
CountVisibleChildren(*targetNode, session));
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() ||
|
||||
panelId.empty() ||
|
||||
targetNodeId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceNode =
|
||||
FindUIEditorWorkspaceNode(workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (sourceNode == nullptr ||
|
||||
targetNode == nullptr ||
|
||||
sourceNode->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId &&
|
||||
sourceNode->children.size() <= 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode extractedPanel = {};
|
||||
if (!TryExtractVisiblePanelFromTabStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string movedStackNodeId = MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_" + std::string(panelId) + "_stack");
|
||||
UIEditorWorkspaceNode movedStack = {};
|
||||
movedStack.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
movedStack.nodeId = movedStackNodeId;
|
||||
movedStack.selectedTabIndex = 0u;
|
||||
movedStack.children.push_back(std::move(extractedPanel));
|
||||
|
||||
UIEditorWorkspaceNode existingTarget = std::move(*targetStack);
|
||||
UIEditorWorkspaceNode primary = {};
|
||||
UIEditorWorkspaceNode secondary = {};
|
||||
if (IsLeadingDockPlacement(placement)) {
|
||||
primary = std::move(movedStack);
|
||||
secondary = std::move(existingTarget);
|
||||
} else {
|
||||
primary = std::move(existingTarget);
|
||||
secondary = std::move(movedStack);
|
||||
}
|
||||
|
||||
const float requestedRatio = ClampDockSplitRatio(splitRatio);
|
||||
const float resolvedSplitRatio =
|
||||
IsLeadingDockPlacement(placement)
|
||||
? requestedRatio
|
||||
: (1.0f - requestedRatio);
|
||||
*targetStack = BuildUIEditorWorkspaceSplit(
|
||||
MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_split"),
|
||||
ResolveDockSplitAxis(placement),
|
||||
resolvedSplitRatio,
|
||||
std::move(primary),
|
||||
std::move(secondary));
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
|
||||
@@ -148,6 +148,11 @@ if(TARGET editor_ui_scroll_view_basic_validation)
|
||||
editor_ui_scroll_view_basic_validation)
|
||||
endif()
|
||||
|
||||
if(TARGET editor_ui_dock_tab_reorder_same_stack_validation)
|
||||
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
|
||||
editor_ui_dock_tab_reorder_same_stack_validation)
|
||||
endif()
|
||||
|
||||
add_custom_target(editor_ui_integration_tests
|
||||
DEPENDS
|
||||
${EDITOR_UI_INTEGRATION_TARGETS}
|
||||
|
||||
@@ -13,6 +13,9 @@ endif()
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/dock_host_basic/CMakeLists.txt")
|
||||
add_subdirectory(dock_host_basic)
|
||||
endif()
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/dock_tab_reorder_same_stack/CMakeLists.txt")
|
||||
add_subdirectory(dock_tab_reorder_same_stack)
|
||||
endif()
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/panel_content_host_basic/CMakeLists.txt")
|
||||
add_subdirectory(panel_content_host_basic)
|
||||
endif()
|
||||
|
||||
@@ -491,7 +491,7 @@ private:
|
||||
if (action == ActionId::Reset) {
|
||||
ResetScenario();
|
||||
m_lastStatus = "Ready";
|
||||
m_lastMessage = "场景状态已重置。请重新检查 splitter drag / tab close / panel close / active panel sync。";
|
||||
m_lastMessage = "场景状态已重置。请重新检查 splitter drag / tab activate / panel close / active panel sync。";
|
||||
m_lastColor = kWarning;
|
||||
return;
|
||||
}
|
||||
@@ -586,11 +586,11 @@ private:
|
||||
|
||||
DrawCard(drawList, m_introRect, "这个测试验证什么功能?", "只验证 DockHost 基础交互 contract,不做 editor 业务面板。");
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f), "1. 验证 splitter drag 是否只通过 DockHostInteraction + WorkspaceController 完成。", kTextPrimary, 12.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验证 unified dock:tab activate / tab close / single-tab body activate。", kTextPrimary, 12.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验证 unified dock:tab activate / single-tab body activate / panel close。", kTextPrimary, 12.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f), "3. 验证 active panel、visible panels、split ratio 是否统一收口到 controller。", kTextPrimary, 12.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f), "4. 验证 pointer capture / release 请求是否通过 contract 明确返回。", kTextPrimary, 12.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f), "建议操作:先拖中间 splitter,再点 Document A。", kTextWeak, 11.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 180.0f), "然后关闭 Document B,最后点 Details 或 Console 的 X。", kTextWeak, 11.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 180.0f), "然后切换 Document A / B,最后点 Details 或 Console 的 X。", kTextWeak, 11.0f);
|
||||
|
||||
DrawCard(drawList, m_controlsRect, "操作", "这里只保留当前场景必要按钮。");
|
||||
for (const ButtonState& button : m_buttons) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
add_executable(editor_ui_dock_tab_reorder_same_stack_validation WIN32
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_include_directories(editor_ui_dock_tab_reorder_same_stack_validation PRIVATE
|
||||
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/include
|
||||
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(editor_ui_dock_tab_reorder_same_stack_validation PRIVATE
|
||||
UNICODE
|
||||
_UNICODE
|
||||
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(editor_ui_dock_tab_reorder_same_stack_validation PRIVATE /utf-8 /FS)
|
||||
set_property(TARGET editor_ui_dock_tab_reorder_same_stack_validation PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_ui_dock_tab_reorder_same_stack_validation PRIVATE
|
||||
XCUIEditorLib
|
||||
XCUIEditorHost
|
||||
)
|
||||
|
||||
set_target_properties(editor_ui_dock_tab_reorder_same_stack_validation PROPERTIES
|
||||
OUTPUT_NAME "XCUIEditorDockTabReorderSameStackValidation"
|
||||
)
|
||||
@@ -0,0 +1,680 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Shell/UIEditorDockHost.h>
|
||||
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIDrawData;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::Host::AutoScreenshotController;
|
||||
using XCEngine::UI::Editor::Host::NativeRenderer;
|
||||
using XCEngine::UI::Editor::UIEditorDockHostInteractionFrame;
|
||||
using XCEngine::UI::Editor::UIEditorDockHostInteractionResult;
|
||||
using XCEngine::UI::Editor::UIEditorDockHostInteractionState;
|
||||
using XCEngine::UI::Editor::UIEditorDockHostTabStripInteractionEntry;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorDockHostInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostForeground;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostTabStackLayout;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCUIEditorDockTabReorderSameStackValidation";
|
||||
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Dock Tab Reorder Same Stack";
|
||||
|
||||
constexpr UIColor kWindowBg(0.10f, 0.10f, 0.10f, 1.0f);
|
||||
constexpr UIColor kCardBg(0.16f, 0.16f, 0.16f, 1.0f);
|
||||
constexpr UIColor kCardBorder(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
constexpr UIColor kPreviewBg(0.12f, 0.12f, 0.12f, 1.0f);
|
||||
constexpr UIColor kTextPrimary(0.92f, 0.92f, 0.92f, 1.0f);
|
||||
constexpr UIColor kTextMuted(0.74f, 0.74f, 0.74f, 1.0f);
|
||||
constexpr UIColor kTextWeak(0.57f, 0.57f, 0.57f, 1.0f);
|
||||
constexpr UIColor kButtonBg(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
constexpr UIColor kButtonHover(0.31f, 0.31f, 0.31f, 1.0f);
|
||||
constexpr UIColor kButtonBorder(0.44f, 0.44f, 0.44f, 1.0f);
|
||||
constexpr UIColor kOk(0.47f, 0.71f, 0.50f, 1.0f);
|
||||
constexpr UIColor kWarn(0.85f, 0.68f, 0.36f, 1.0f);
|
||||
|
||||
enum class ActionId : unsigned char {
|
||||
Reset = 0,
|
||||
Capture
|
||||
};
|
||||
|
||||
struct ButtonState {
|
||||
ActionId action = ActionId::Reset;
|
||||
std::string label = {};
|
||||
UIRect rect = {};
|
||||
bool hovered = false;
|
||||
};
|
||||
|
||||
std::filesystem::path ResolveRepoRootPath() {
|
||||
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||
root = root.substr(1u, root.size() - 2u);
|
||||
}
|
||||
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
return x >= rect.x &&
|
||||
x <= rect.x + rect.width &&
|
||||
y >= rect.y &&
|
||||
y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
std::string FormatBool(bool value) {
|
||||
return value ? "是" : "否";
|
||||
}
|
||||
|
||||
std::string FormatOptionalIndex(std::size_t index) {
|
||||
if (index == XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex) {
|
||||
return "无";
|
||||
}
|
||||
|
||||
return std::to_string(index);
|
||||
}
|
||||
|
||||
std::string DescribeHitTarget(const UIEditorDockHostHitTarget& target) {
|
||||
switch (target.kind) {
|
||||
case UIEditorDockHostHitTargetKind::Tab:
|
||||
return "标签: " + target.panelId;
|
||||
case UIEditorDockHostHitTargetKind::TabStripBackground:
|
||||
return "标签栏空白";
|
||||
case UIEditorDockHostHitTargetKind::PanelBody:
|
||||
return "面板主体: " + target.panelId;
|
||||
case UIEditorDockHostHitTargetKind::SplitterHandle:
|
||||
return "分割条: " + target.nodeId;
|
||||
case UIEditorDockHostHitTargetKind::None:
|
||||
default:
|
||||
return "无";
|
||||
}
|
||||
}
|
||||
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
std::string_view title,
|
||||
std::string_view subtitle = {}) {
|
||||
drawList.AddFilledRect(rect, kCardBg, 10.0f);
|
||||
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 38.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawButton(UIDrawList& drawList, const ButtonState& button) {
|
||||
drawList.AddFilledRect(button.rect, button.hovered ? kButtonHover : kButtonBg, 8.0f);
|
||||
drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f);
|
||||
drawList.AddText(UIPoint(button.rect.x + 14.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
|
||||
}
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "doc-c", "Document C", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.72f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
|
||||
BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
std::string CollectDocumentTabOrder(const UIEditorWorkspaceController& controller) {
|
||||
const auto* node = FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
if (node == nullptr) {
|
||||
return "缺失";
|
||||
}
|
||||
|
||||
std::ostringstream stream = {};
|
||||
for (std::size_t index = 0; index < node->children.size(); ++index) {
|
||||
if (index > 0u) {
|
||||
stream << " | ";
|
||||
}
|
||||
stream << node->children[index].panel.panelId;
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
const XCEngine::UI::Editor::Widgets::UIEditorDockHostTabStackLayout* FindDocumentTabStackLayout(
|
||||
const UIEditorDockHostInteractionFrame& frame) {
|
||||
for (const auto& tabStack : frame.layout.tabStacks) {
|
||||
if (tabStack.nodeId == "document-tabs") {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStripInteractionEntry* FindDocumentTabStripInteractionEntry(
|
||||
const UIEditorDockHostInteractionState& state) {
|
||||
for (const UIEditorDockHostTabStripInteractionEntry& entry : state.tabStripInteractions) {
|
||||
if (entry.nodeId == "document-tabs") {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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->m_renderer.Resize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||
}
|
||||
return 0;
|
||||
case WM_PAINT:
|
||||
if (app != nullptr) {
|
||||
PAINTSTRUCT paintStruct = {};
|
||||
BeginPaint(hwnd, &paintStruct);
|
||||
app->RenderFrame();
|
||||
EndPaint(hwnd, &paintStruct);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_MOUSEMOVE:
|
||||
if (app != nullptr) {
|
||||
if (!app->m_trackingMouseLeave) {
|
||||
TRACKMOUSEEVENT trackMouseEvent = {};
|
||||
trackMouseEvent.cbSize = sizeof(trackMouseEvent);
|
||||
trackMouseEvent.dwFlags = TME_LEAVE;
|
||||
trackMouseEvent.hwndTrack = hwnd;
|
||||
if (TrackMouseEvent(&trackMouseEvent)) {
|
||||
app->m_trackingMouseLeave = true;
|
||||
}
|
||||
}
|
||||
app->HandleMouseMove(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_MOUSELEAVE:
|
||||
if (app != nullptr) {
|
||||
app->m_trackingMouseLeave = false;
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerLeave;
|
||||
app->m_pendingInputEvents.push_back(event);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_LBUTTONDOWN:
|
||||
if (app != nullptr) {
|
||||
SetFocus(hwnd);
|
||||
app->HandleLeftButtonDown(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_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_SETFOCUS:
|
||||
if (app != nullptr) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusGained;
|
||||
app->m_pendingInputEvents.push_back(event);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_KILLFOCUS:
|
||||
if (app != nullptr) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
app->m_pendingInputEvents.push_back(event);
|
||||
app->m_lastInputCause = "focus_lost";
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_CAPTURECHANGED:
|
||||
if (app != nullptr &&
|
||||
!app->m_interactionState.activeTabDragNodeId.empty() &&
|
||||
reinterpret_cast<HWND>(lParam) != hwnd) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::FocusLost;
|
||||
app->m_pendingInputEvents.push_back(event);
|
||||
app->m_lastInputCause = "focus_lost";
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
if (app != nullptr) {
|
||||
if (wParam == VK_F12) {
|
||||
app->m_autoScreenshot.RequestCapture("manual_f12");
|
||||
return 0;
|
||||
}
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(wParam == VK_ESCAPE ? KeyCode::Escape : KeyCode::None);
|
||||
app->m_pendingInputEvents.push_back(event);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
m_captureRoot =
|
||||
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/dock_tab_reorder_same_stack/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,
|
||||
1500,
|
||||
920,
|
||||
nullptr,
|
||||
nullptr,
|
||||
hInstance,
|
||||
this);
|
||||
if (m_hwnd == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ShowWindow(m_hwnd, nCmdShow);
|
||||
if (!m_renderer.Initialize(m_hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ResetScenario();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
if (GetCapture() == m_hwnd) {
|
||||
ReleaseCapture();
|
||||
}
|
||||
m_autoScreenshot.Shutdown();
|
||||
m_renderer.Shutdown();
|
||||
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||
DestroyWindow(m_hwnd);
|
||||
}
|
||||
if (m_windowClassAtom != 0) {
|
||||
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
void ResetScenario() {
|
||||
if (GetCapture() == m_hwnd) {
|
||||
ReleaseCapture();
|
||||
}
|
||||
|
||||
m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
m_interactionState = {};
|
||||
m_cachedFrame = {};
|
||||
m_pendingInputEvents.clear();
|
||||
m_lastInputCause.clear();
|
||||
m_hoverText = "无";
|
||||
m_lastResult = "等待验证:请在同一标签栏内拖拽标签。";
|
||||
m_lastResultColor = kWarn;
|
||||
}
|
||||
|
||||
void UpdateLayout() {
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
|
||||
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||
constexpr float padding = 20.0f;
|
||||
constexpr float sidebarWidth = 360.0f;
|
||||
|
||||
m_introRect = UIRect(padding, padding, width - padding * 2.0f, 168.0f);
|
||||
m_previewRect = UIRect(
|
||||
padding,
|
||||
m_introRect.y + m_introRect.height + 16.0f,
|
||||
width - sidebarWidth - padding * 3.0f,
|
||||
height - m_introRect.height - padding * 3.0f);
|
||||
m_stateRect = UIRect(
|
||||
m_previewRect.x + m_previewRect.width + 20.0f,
|
||||
m_previewRect.y,
|
||||
sidebarWidth,
|
||||
m_previewRect.height);
|
||||
m_dockHostRect = UIRect(
|
||||
m_previewRect.x + 16.0f,
|
||||
m_previewRect.y + 64.0f,
|
||||
m_previewRect.width - 32.0f,
|
||||
m_previewRect.height - 80.0f);
|
||||
|
||||
const float buttonWidth = (m_stateRect.width - 44.0f) * 0.5f;
|
||||
const float buttonY = m_stateRect.y + m_stateRect.height - 52.0f;
|
||||
m_buttons = {
|
||||
{ ActionId::Reset, "重置", UIRect(m_stateRect.x + 16.0f, buttonY, buttonWidth, 36.0f), false },
|
||||
{ ActionId::Capture, "截图(F12)", UIRect(m_stateRect.x + 28.0f + buttonWidth, buttonY, buttonWidth, 36.0f), false }
|
||||
};
|
||||
}
|
||||
|
||||
void HandleMouseMove(float x, float y) {
|
||||
UpdateLayout();
|
||||
for (ButtonState& button : m_buttons) {
|
||||
button.hovered = ContainsPoint(button.rect, x, y);
|
||||
}
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(x, y);
|
||||
m_pendingInputEvents.push_back(event);
|
||||
}
|
||||
|
||||
void HandleLeftButtonDown(float x, float y) {
|
||||
UpdateLayout();
|
||||
for (const ButtonState& button : m_buttons) {
|
||||
if (ContainsPoint(button.rect, x, y)) {
|
||||
ExecuteAction(button.action);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonDown;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
m_pendingInputEvents.push_back(event);
|
||||
}
|
||||
|
||||
void HandleLeftButtonUp(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerButtonUp;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = UIPointerButton::Left;
|
||||
m_pendingInputEvents.push_back(event);
|
||||
}
|
||||
|
||||
void ExecuteAction(ActionId action) {
|
||||
if (action == ActionId::Reset) {
|
||||
ResetScenario();
|
||||
m_lastResult = "已重置:请重新检查提交、取消和失焦取消。";
|
||||
m_lastResultColor = kWarn;
|
||||
return;
|
||||
}
|
||||
|
||||
m_autoScreenshot.RequestCapture("manual_button");
|
||||
m_lastResult = "截图已排队:输出到 captures/。";
|
||||
m_lastResultColor = kWarn;
|
||||
}
|
||||
|
||||
void ApplyHostCaptureRequests(const UIEditorDockHostInteractionResult& result) {
|
||||
if (result.requestPointerCapture && GetCapture() != m_hwnd) {
|
||||
SetCapture(m_hwnd);
|
||||
}
|
||||
if (result.releasePointerCapture && GetCapture() == m_hwnd) {
|
||||
ReleaseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateLastResult(const UIEditorDockHostInteractionResult& result) {
|
||||
if (result.layoutResult.status == UIEditorWorkspaceLayoutOperationStatus::Changed) {
|
||||
m_lastResult = "结果:同一标签栏内的标签重排已提交。";
|
||||
m_lastResultColor = kOk;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.requestPointerCapture) {
|
||||
m_lastResult = "结果:开始拖拽,宿主已拿到 pointer capture。";
|
||||
m_lastResultColor = kOk;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.releasePointerCapture && !result.layoutChanged && !result.commandExecuted) {
|
||||
if (m_lastInputCause == "focus_lost") {
|
||||
m_lastResult = "结果:窗口失焦,本次重排已取消。";
|
||||
} else {
|
||||
m_lastResult = "结果:拖拽已取消。拖到 body 区再松开时,标签顺序应保持不变。";
|
||||
}
|
||||
m_lastResultColor = kWarn;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderFrame() {
|
||||
UpdateLayout();
|
||||
m_cachedFrame = UpdateUIEditorDockHostInteraction(
|
||||
m_interactionState,
|
||||
m_controller,
|
||||
m_dockHostRect,
|
||||
m_pendingInputEvents);
|
||||
m_pendingInputEvents.clear();
|
||||
ApplyHostCaptureRequests(m_cachedFrame.result);
|
||||
UpdateLastResult(m_cachedFrame.result);
|
||||
m_lastInputCause.clear();
|
||||
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
|
||||
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("DockTabReorderSameStack");
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
|
||||
|
||||
DrawCard(drawList, m_introRect, "这个测试验证什么功能?", "只验证同一标签栏内 tab 重排的基础 contract,不做 editor 业务。");
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 70.0f), "1. 拖动 Document A / B / C 的 tab header 到同一行其他位置。", kTextPrimary, 12.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 92.0f), "2. 在 header 区松开:应提交重排,并更新右侧标签顺序。", kTextPrimary, 12.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 114.0f), "3. 拖到 body 区再松开:应取消,本次标签顺序不能变化。", kTextPrimary, 12.0f);
|
||||
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 136.0f), "4. 拖拽中切走窗口或按 Esc:应释放 capture,并取消这次重排。", kTextWeak, 11.0f);
|
||||
|
||||
DrawCard(drawList, m_previewRect, "预览区", "这里只保留同栏重排所需的最小工作区。");
|
||||
drawList.AddFilledRect(m_dockHostRect, kPreviewBg, 8.0f);
|
||||
AppendUIEditorDockHostBackground(drawList, m_cachedFrame.layout);
|
||||
AppendUIEditorDockHostForeground(drawList, m_cachedFrame.layout);
|
||||
|
||||
DrawCard(drawList, m_stateRect, "状态", "拖拽时重点观察右侧状态是否和画面一致。");
|
||||
float y = m_stateRect.y + 68.0f;
|
||||
auto addLine = [&](std::string text, const UIColor& color = kTextPrimary, float fontSize = 12.0f) mutable {
|
||||
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, y), std::move(text), color, fontSize);
|
||||
y += 22.0f;
|
||||
};
|
||||
|
||||
const auto* documentTabStack = FindDocumentTabStackLayout(m_cachedFrame);
|
||||
const bool previewVisible =
|
||||
documentTabStack != nullptr &&
|
||||
documentTabStack->tabStripLayout.insertionPreview.visible;
|
||||
const std::size_t previewInsertionIndex =
|
||||
documentTabStack != nullptr
|
||||
? documentTabStack->tabStripLayout.insertionPreview.insertionIndex
|
||||
: XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||
|
||||
addLine("当前标签顺序: " + CollectDocumentTabOrder(m_controller), kTextPrimary, 11.0f);
|
||||
addLine("活动面板: " + m_controller.GetWorkspace().activePanelId, kTextPrimary, 11.0f);
|
||||
addLine("已聚焦: " + FormatBool(m_cachedFrame.focused), m_cachedFrame.focused ? kOk : kTextMuted, 11.0f);
|
||||
addLine("宿主捕获: " + FormatBool(GetCapture() == m_hwnd), GetCapture() == m_hwnd ? kOk : kTextMuted, 11.0f);
|
||||
addLine(
|
||||
"拖拽标签栈: " +
|
||||
(m_interactionState.activeTabDragNodeId.empty()
|
||||
? std::string("无")
|
||||
: m_interactionState.activeTabDragNodeId),
|
||||
kTextWeak,
|
||||
11.0f);
|
||||
addLine("预览插入位激活: " + FormatBool(previewVisible), previewVisible ? kOk : kTextWeak, 11.0f);
|
||||
addLine("预览插入索引: " + FormatOptionalIndex(previewInsertionIndex), kTextWeak, 11.0f);
|
||||
addLine("当前 Hover: " + m_hoverText, kTextWeak, 11.0f);
|
||||
|
||||
drawList.AddText(
|
||||
UIPoint(m_stateRect.x + 16.0f, y + 8.0f),
|
||||
"最近结果",
|
||||
kTextPrimary,
|
||||
13.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(m_stateRect.x + 16.0f, y + 32.0f),
|
||||
m_lastResult,
|
||||
m_lastResultColor,
|
||||
12.0f);
|
||||
|
||||
const std::string captureSummary =
|
||||
m_autoScreenshot.HasPendingCapture()
|
||||
? "截图排队中..."
|
||||
: (m_autoScreenshot.GetLastCaptureSummary().empty()
|
||||
? "F12 或按钮 -> captures/"
|
||||
: m_autoScreenshot.GetLastCaptureSummary());
|
||||
drawList.AddText(
|
||||
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + m_stateRect.height - 86.0f),
|
||||
captureSummary,
|
||||
kTextWeak,
|
||||
11.0f);
|
||||
for (const ButtonState& button : m_buttons) {
|
||||
DrawButton(drawList, button);
|
||||
}
|
||||
|
||||
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 = {};
|
||||
UIEditorWorkspaceController m_controller = {};
|
||||
UIEditorDockHostInteractionState m_interactionState = {};
|
||||
UIEditorDockHostInteractionFrame m_cachedFrame = {};
|
||||
std::vector<UIInputEvent> m_pendingInputEvents = {};
|
||||
std::vector<ButtonState> m_buttons = {};
|
||||
UIRect m_introRect = {};
|
||||
UIRect m_previewRect = {};
|
||||
UIRect m_stateRect = {};
|
||||
UIRect m_dockHostRect = {};
|
||||
bool m_trackingMouseLeave = false;
|
||||
std::string m_lastInputCause = {};
|
||||
std::string m_hoverText = {};
|
||||
std::string m_lastResult = {};
|
||||
UIColor m_lastResultColor = kTextMuted;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||
return ScenarioApp().Run(hInstance, nCmdShow);
|
||||
}
|
||||
@@ -196,11 +196,6 @@ std::string DescribeHitTarget(
|
||||
return "Tab: " + items[target.index].title;
|
||||
}
|
||||
return "Tab";
|
||||
case UIEditorTabStripHitTargetKind::CloseButton:
|
||||
if (target.index < items.size()) {
|
||||
return "CloseButton: " + items[target.index].title;
|
||||
}
|
||||
return "CloseButton";
|
||||
case UIEditorTabStripHitTargetKind::None:
|
||||
default:
|
||||
return "None";
|
||||
@@ -218,9 +213,6 @@ std::string JoinTabTitles(const std::vector<UIEditorTabStripItem>& items) {
|
||||
stream << " | ";
|
||||
}
|
||||
stream << items[index].title;
|
||||
if (!items[index].closable) {
|
||||
stream << " (locked)";
|
||||
}
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
@@ -502,15 +494,6 @@ private:
|
||||
void ApplyInteractionResult(
|
||||
const UIEditorTabStripInteractionResult& result,
|
||||
std::string_view source) {
|
||||
if (result.closeRequested && !result.closedTabId.empty()) {
|
||||
DispatchCommand(
|
||||
UIEditorWorkspaceCommandKind::ClosePanel,
|
||||
result.closedTabId,
|
||||
std::string(source) + " Close -> " + result.closedTabId);
|
||||
PumpTabStripEvents({});
|
||||
return;
|
||||
}
|
||||
|
||||
if ((result.selectionChanged || result.keyboardNavigated) &&
|
||||
!result.selectedTabId.empty()) {
|
||||
DispatchCommand(
|
||||
@@ -609,7 +592,7 @@ private:
|
||||
drawList,
|
||||
introRect,
|
||||
"这个测试验证什么功能?",
|
||||
"验证 TabStrip 的 header 命中、选中切换、关闭请求和键盘导航,不接业务面板。");
|
||||
"验证 TabStrip 的 header 命中、选中切换和键盘导航,不接业务面板。");
|
||||
drawList.AddText(
|
||||
UIPoint(introRect.x + 16.0f, introRect.y + 68.0f),
|
||||
"1. 点击 tab,检查 selected / active panel 是否同步。",
|
||||
@@ -617,7 +600,7 @@ private:
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(introRect.x + 16.0f, introRect.y + 92.0f),
|
||||
"2. 点击 X,只验证关闭请求;Document C 没有关闭按钮。",
|
||||
"2. 所有 tab 都没有关闭按钮;这里只验证命中、focus 和选中同步。",
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
@@ -719,7 +702,7 @@ private:
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(m_layout.contentRect.x + 20.0f, m_layout.contentRect.y + 100.0f),
|
||||
"可点击 Document B 切换,或点击 Document C 验证不可关闭 tab。",
|
||||
"可点击 Document B 切换,再用 Left / Right / Home / End 验证键盘导航。",
|
||||
kTextWeak,
|
||||
12.0f);
|
||||
|
||||
|
||||
@@ -606,7 +606,7 @@ private:
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 92.0f),
|
||||
"2. tab 激活/关闭、panel 激活/关闭都必须统一回到 controller。",
|
||||
"2. tab 激活、panel 激活/关闭都必须统一回到 controller。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
|
||||
@@ -98,8 +98,8 @@ TEST(UIEditorDockHostTest, LayoutComposesOnlyUnifiedTabStacksFromWorkspaceTree)
|
||||
|
||||
const auto* rootSplitter = FindUIEditorDockHostSplitterLayout(layout, "root-split");
|
||||
ASSERT_NE(rootSplitter, nullptr);
|
||||
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.x, 395.0f);
|
||||
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.width, 10.0f);
|
||||
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.x, 399.5f);
|
||||
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.width, 1.0f);
|
||||
|
||||
const auto& tabStack = layout.tabStacks.front();
|
||||
EXPECT_EQ(tabStack.nodeId, "document-tabs");
|
||||
@@ -138,7 +138,7 @@ TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds)
|
||||
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.height, 480.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabCloseThenPanelBody) {
|
||||
TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabThenPanelBody) {
|
||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
const UIEditorWorkspaceSession session =
|
||||
@@ -160,14 +160,14 @@ TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabCloseThenPanelBody)
|
||||
EXPECT_EQ(splitterHit.nodeId, "root-split");
|
||||
|
||||
ASSERT_EQ(layout.tabStacks.size(), 3u);
|
||||
const auto& closeRect = layout.tabStacks.front().tabStripLayout.closeButtonRects[1];
|
||||
const auto tabCloseHit = HitTestUIEditorDockHost(
|
||||
const auto& tabRect = layout.tabStacks.front().tabStripLayout.tabHeaderRects[1];
|
||||
const auto tabHit = HitTestUIEditorDockHost(
|
||||
layout,
|
||||
UIPoint(closeRect.x + closeRect.width * 0.5f, closeRect.y + closeRect.height * 0.5f));
|
||||
EXPECT_EQ(tabCloseHit.kind, UIEditorDockHostHitTargetKind::TabCloseButton);
|
||||
EXPECT_EQ(tabCloseHit.nodeId, "document-tabs");
|
||||
EXPECT_EQ(tabCloseHit.panelId, "doc-b");
|
||||
EXPECT_EQ(tabCloseHit.index, 1u);
|
||||
UIPoint(tabRect.x + tabRect.width - 6.0f, tabRect.y + tabRect.height * 0.5f));
|
||||
EXPECT_EQ(tabHit.kind, UIEditorDockHostHitTargetKind::Tab);
|
||||
EXPECT_EQ(tabHit.nodeId, "document-tabs");
|
||||
EXPECT_EQ(tabHit.panelId, "doc-b");
|
||||
EXPECT_EQ(tabHit.index, 1u);
|
||||
|
||||
const auto panelBodyHit = HitTestUIEditorDockHost(
|
||||
layout,
|
||||
@@ -186,7 +186,7 @@ TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) {
|
||||
UIEditorDockHostState state = {};
|
||||
state.focused = true;
|
||||
state.hoveredTarget = UIEditorDockHostHitTarget{
|
||||
UIEditorDockHostHitTargetKind::TabCloseButton,
|
||||
UIEditorDockHostHitTargetKind::Tab,
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
1u
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
@@ -19,6 +22,7 @@ using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::UIEditorDockHostInteractionState;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
@@ -33,6 +37,7 @@ UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "doc-c", "Document C", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true },
|
||||
{ "console", "Console", {}, true, true, true }
|
||||
};
|
||||
@@ -62,6 +67,30 @@ UIEditorWorkspaceModel BuildWorkspace() {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildThreeDocumentWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.5f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
|
||||
BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true)
|
||||
},
|
||||
2u),
|
||||
BuildUIEditorWorkspaceSplit(
|
||||
"right-split",
|
||||
UIEditorWorkspaceSplitAxis::Vertical,
|
||||
0.6f,
|
||||
BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true),
|
||||
BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true)));
|
||||
workspace.activePanelId = "doc-c";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerMove(float x, float y) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
@@ -230,60 +259,6 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
|
||||
EXPECT_EQ(documentStack->selectedPanelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughController) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIRect closeRect = documentStack->tabStripLayout.closeButtonRects[1];
|
||||
const UIPoint closeCenter = RectCenter(closeRect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(closeCenter.x, closeCenter.y) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::TabCloseButton);
|
||||
EXPECT_EQ(frame.result.hitTarget.panelId, "doc-b");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(closeCenter.x, closeCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_FALSE(frame.result.commandExecuted);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(closeCenter.x, closeCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_FALSE(frame.result.commandExecuted);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(closeCenter.x, closeCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.commandExecuted);
|
||||
|
||||
const auto* panelState = FindUIEditorPanelSessionState(controller.GetSession(), "doc-b");
|
||||
ASSERT_NE(panelState, nullptr);
|
||||
EXPECT_FALSE(panelState->open);
|
||||
EXPECT_FALSE(panelState->visible);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationThroughTabStripInteraction) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
@@ -399,7 +374,7 @@ TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackBodyActivatesTargetP
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackTabCloseClosesPanelThroughController) {
|
||||
TEST(UIEditorDockHostInteractionTest, DraggingTabWithinSameStackRequestsCaptureAndCommitsReorder) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
@@ -409,29 +384,411 @@ TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackTabCloseClosesPanelT
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* consoleStack = FindTabStackByNodeId(frame.layout, "console-node");
|
||||
ASSERT_NE(consoleStack, nullptr);
|
||||
const UIRect closeRect = consoleStack->tabStripLayout.closeButtonRects[0];
|
||||
const UIPoint closeCenter = RectCenter(closeRect);
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint dropPoint(
|
||||
documentStack->tabStripLayout.headerRect.x +
|
||||
documentStack->tabStripLayout.headerRect.width - 2.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(closeCenter.x, closeCenter.y) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::TabCloseButton);
|
||||
EXPECT_EQ(frame.result.hitTarget.panelId, "console");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(closeCenter.x, closeCenter.y) });
|
||||
{ MakePointerDown(sourceCenter.x, sourceCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.commandExecuted);
|
||||
|
||||
const auto* panelState = FindUIEditorPanelSessionState(controller.GetSession(), "console");
|
||||
ASSERT_NE(panelState, nullptr);
|
||||
EXPECT_FALSE(panelState->open);
|
||||
EXPECT_FALSE(panelState->visible);
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(dropPoint.x, dropPoint.y) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(dropPoint.x, dropPoint.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.layoutChanged)
|
||||
<< " status=" << static_cast<int>(frame.result.layoutResult.status)
|
||||
<< " message=" << frame.result.layoutResult.message
|
||||
<< " commandExecuted=" << frame.result.commandExecuted;
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 2u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(documentTabs->children[1].panel.panelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, DraggingRightmostTabIntoMiddleCommitsSameStackReorder) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildThreeDocumentWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[2]);
|
||||
const UIPoint dropPoint(
|
||||
documentStack->tabStripLayout.tabHeaderRects[1].x + 4.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(sourceCenter.x, sourceCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(dropPoint.x, dropPoint.y) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(dropPoint.x, dropPoint.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.layoutChanged)
|
||||
<< " status=" << static_cast<int>(frame.result.layoutResult.status)
|
||||
<< " message=" << frame.result.layoutResult.message
|
||||
<< " commandExecuted=" << frame.result.commandExecuted;
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 3u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(documentTabs->children[1].panel.panelId, "doc-c");
|
||||
EXPECT_EQ(documentTabs->children[2].panel.panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, DraggingRightmostTabToFrontCommitsSameStackReorder) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildThreeDocumentWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[2]);
|
||||
const UIPoint dropPoint(
|
||||
documentStack->tabStripLayout.tabHeaderRects[0].x + 4.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(sourceCenter.x, sourceCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(dropPoint.x, dropPoint.y) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(dropPoint.x, dropPoint.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 3u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-c");
|
||||
EXPECT_EQ(documentTabs->children[1].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(documentTabs->children[2].panel.panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, SameStackReorderCommitsFromLastVisiblePreviewEvenIfReleaseDropsBelowHeader) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint previewPoint(
|
||||
documentStack->tabStripLayout.headerRect.x +
|
||||
documentStack->tabStripLayout.headerRect.width - 2.0f,
|
||||
sourceCenter.y);
|
||||
const UIPoint releasePoint(
|
||||
previewPoint.x,
|
||||
documentStack->tabStripLayout.headerRect.y +
|
||||
documentStack->tabStripLayout.headerRect.height + 8.0f);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(sourceCenter.x, sourceCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(previewPoint.x, previewPoint.y) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
const auto tabStripStateIt = std::find_if(
|
||||
state.dockHostState.tabStripStates.begin(),
|
||||
state.dockHostState.tabStripStates.end(),
|
||||
[](const auto& tabStripState) {
|
||||
return tabStripState.nodeId == "document-tabs";
|
||||
});
|
||||
ASSERT_NE(tabStripStateIt, state.dockHostState.tabStripStates.end());
|
||||
ASSERT_NE(
|
||||
tabStripStateIt->state.reorder.previewInsertionIndex,
|
||||
XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(releasePoint.x, releasePoint.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 2u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(documentTabs->children[1].panel.panelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ReleasingDraggedTabOutsideHeaderCancelsWithoutChangingOrder) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint activatePoint(
|
||||
documentStack->tabStripLayout.headerRect.x +
|
||||
documentStack->tabStripLayout.headerRect.width - 2.0f,
|
||||
sourceCenter.y);
|
||||
const UIPoint cancelPoint = RectCenter(documentStack->contentFrameLayout.bodyRect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(activatePoint.x, activatePoint.y),
|
||||
MakePointerMove(cancelPoint.x, cancelPoint.y),
|
||||
MakePointerUp(cancelPoint.x, cancelPoint.y)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(frame.result.layoutChanged);
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 2u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(documentTabs->children[1].panel.panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, DraggingTabOntoAnotherStackHeaderMergesIntoTargetStack) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
const auto* detailsStack = FindTabStackByNodeId(frame.layout, "details-node");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
ASSERT_NE(detailsStack, nullptr);
|
||||
const UIPoint sourceCenter =
|
||||
RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint targetHeaderCenter =
|
||||
RectCenter(detailsStack->tabStripLayout.headerRect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(sourceCenter.x, sourceCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(targetHeaderCenter.x, targetHeaderCenter.y) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(state.dockHostState.dropPreview.visible);
|
||||
EXPECT_EQ(state.dockHostState.dropPreview.targetNodeId, "details-node");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(targetHeaderCenter.x, targetHeaderCenter.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
const auto* detailsTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "details-node");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_NE(detailsTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 1u);
|
||||
ASSERT_EQ(detailsTabs->children.size(), 2u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(detailsTabs->children[1].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, DraggingTabOntoPanelBodyEdgeCreatesDockSplit) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
const auto* detailsStack = FindTabStackByNodeId(frame.layout, "details-node");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
ASSERT_NE(detailsStack, nullptr);
|
||||
const UIPoint sourceCenter =
|
||||
RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint targetBottomCenter(
|
||||
detailsStack->contentFrameLayout.bodyRect.x +
|
||||
detailsStack->contentFrameLayout.bodyRect.width * 0.5f,
|
||||
detailsStack->contentFrameLayout.bodyRect.y +
|
||||
detailsStack->contentFrameLayout.bodyRect.height - 4.0f);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(sourceCenter.x, sourceCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(targetBottomCenter.x, targetBottomCenter.y) });
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(state.dockHostState.dropPreview.visible);
|
||||
EXPECT_EQ(
|
||||
state.dockHostState.dropPreview.placement,
|
||||
XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement::Bottom);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(targetBottomCenter.x, targetBottomCenter.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
|
||||
bool foundDockedTab = false;
|
||||
for (const std::string candidate : { "details-node__dock_doc-a_stack", "details-node__dock_doc-a_stack-1" }) {
|
||||
const auto* docked =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), candidate);
|
||||
if (docked != nullptr &&
|
||||
docked->children.size() == 1u &&
|
||||
docked->children[0].panel.panelId == "doc-a") {
|
||||
foundDockedTab = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(foundDockedTab);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, FocusLostWhileDraggingTabCancelsAndReleasesCapture) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||
const UIPoint activatePoint(
|
||||
documentStack->tabStripLayout.headerRect.x +
|
||||
documentStack->tabStripLayout.headerRect.width - 2.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(activatePoint.x, activatePoint.y)
|
||||
});
|
||||
ASSERT_TRUE(frame.result.requestPointerCapture);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakeFocusLost() });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(frame.result.layoutChanged);
|
||||
|
||||
const auto* documentTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(documentTabs, nullptr);
|
||||
ASSERT_EQ(documentTabs->children.size(), 2u);
|
||||
EXPECT_EQ(documentTabs->children[0].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(documentTabs->children[1].panel.panelId, "doc-b");
|
||||
}
|
||||
|
||||
@@ -96,12 +96,11 @@ TEST(UIEditorMenuBarTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
|
||||
UIDrawList background("MenuBarBackground");
|
||||
AppendUIEditorMenuBarBackground(background, layout, items, state);
|
||||
ASSERT_EQ(background.GetCommandCount(), 6u);
|
||||
ASSERT_EQ(background.GetCommandCount(), 3u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[1].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[2].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[5].type, UIDrawCommandType::RectOutline);
|
||||
|
||||
UIDrawList foreground("MenuBarForeground");
|
||||
AppendUIEditorMenuBarForeground(foreground, layout, items, state);
|
||||
|
||||
@@ -41,6 +41,8 @@ using XCEngine::UI::Editor::UIEditorShellInteractionModel;
|
||||
using XCEngine::UI::Editor::UIEditorShellInteractionPopupItemRequest;
|
||||
using XCEngine::UI::Editor::UIEditorShellInteractionServices;
|
||||
using XCEngine::UI::Editor::UIEditorShellInteractionState;
|
||||
using XCEngine::UI::Editor::UIEditorTextMeasureRequest;
|
||||
using XCEngine::UI::Editor::UIEditorTextMeasurer;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||
@@ -52,6 +54,13 @@ using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
|
||||
using XCEngine::UI::Widgets::UIPopupDismissReason;
|
||||
|
||||
class StubTextMeasurer final : public UIEditorTextMeasurer {
|
||||
public:
|
||||
float MeasureTextWidth(const UIEditorTextMeasureRequest& request) const override {
|
||||
return static_cast<float>(request.text.size()) * (request.fontSize * 0.6f);
|
||||
}
|
||||
};
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
@@ -219,6 +228,76 @@ UIEditorWorkspaceController BuildController() {
|
||||
return BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
}
|
||||
|
||||
UIPoint RectCenter(const UIRect& rect);
|
||||
UIInputEvent MakeLeftPointerDown(const UIPoint& position);
|
||||
|
||||
TEST(UIEditorShellInteractionTest, RequestUsesTextMeasurerForMenuBarWidths) {
|
||||
UIEditorWorkspaceController controller = BuildController();
|
||||
const UIEditorShellInteractionModel model = BuildInteractionModel();
|
||||
StubTextMeasurer textMeasurer = {};
|
||||
UIEditorShellInteractionServices services = {};
|
||||
services.textMeasurer = &textMeasurer;
|
||||
|
||||
const auto request = ResolveUIEditorShellInteractionRequest(
|
||||
UIRect(0.0f, 0.0f, 480.0f, 320.0f),
|
||||
controller,
|
||||
model,
|
||||
{},
|
||||
{},
|
||||
services);
|
||||
|
||||
ASSERT_EQ(request.menuBarItems.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(
|
||||
request.menuBarItems[0].desiredLabelWidth,
|
||||
textMeasurer.MeasureTextWidth(UIEditorTextMeasureRequest { "File", 13.0f }));
|
||||
EXPECT_FLOAT_EQ(
|
||||
request.menuBarItems[1].desiredLabelWidth,
|
||||
textMeasurer.MeasureTextWidth(UIEditorTextMeasureRequest { "Window", 13.0f }));
|
||||
EXPECT_GT(request.menuButtons[1].rect.width, request.menuButtons[0].rect.width);
|
||||
}
|
||||
|
||||
TEST(UIEditorShellInteractionTest, PopupWidgetWidthsUseTextMeasurerForLabelAndShortcut) {
|
||||
UIEditorWorkspaceController controller = BuildController();
|
||||
const UIEditorShellInteractionModel model = BuildInteractionModel();
|
||||
StubTextMeasurer textMeasurer = {};
|
||||
UIEditorShellInteractionServices services = {};
|
||||
services.textMeasurer = &textMeasurer;
|
||||
|
||||
UIEditorShellInteractionState state = {};
|
||||
const auto request = ResolveUIEditorShellInteractionRequest(
|
||||
UIRect(0.0f, 0.0f, 480.0f, 320.0f),
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
{},
|
||||
services);
|
||||
ASSERT_FALSE(request.menuButtons.empty());
|
||||
|
||||
UIInputEvent openMenuEvent = {};
|
||||
openMenuEvent.type = UIInputEventType::PointerButtonDown;
|
||||
openMenuEvent.pointerButton = UIPointerButton::Left;
|
||||
openMenuEvent.position = UIPoint(
|
||||
request.menuButtons.front().rect.x + request.menuButtons.front().rect.width * 0.5f,
|
||||
request.menuButtons.front().rect.y + request.menuButtons.front().rect.height * 0.5f);
|
||||
|
||||
const auto frame = UpdateUIEditorShellInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 480.0f, 320.0f),
|
||||
model,
|
||||
std::vector<UIInputEvent> { openMenuEvent },
|
||||
services);
|
||||
|
||||
ASSERT_EQ(frame.request.popupRequests.size(), 1u);
|
||||
ASSERT_GE(frame.request.popupRequests.front().widgetItems.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(
|
||||
frame.request.popupRequests.front().widgetItems[0].desiredLabelWidth,
|
||||
textMeasurer.MeasureTextWidth(UIEditorTextMeasureRequest { "Workspace Tools", 13.0f }));
|
||||
EXPECT_FLOAT_EQ(
|
||||
frame.request.popupRequests.front().widgetItems[1].desiredShortcutWidth,
|
||||
textMeasurer.MeasureTextWidth(UIEditorTextMeasureRequest { "Ctrl+W", 13.0f }));
|
||||
}
|
||||
|
||||
const UIEditorShellInteractionMenuButtonRequest* FindMenuButton(
|
||||
const UIEditorShellInteractionFrame& frame,
|
||||
std::string_view menuId) {
|
||||
|
||||
@@ -15,7 +15,6 @@ using XCEngine::UI::Editor::Widgets::BuildUIEditorTabStripLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTabStrip;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripDesiredHeaderLabelWidth;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndex;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndexAfterClose;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
|
||||
@@ -23,7 +22,7 @@ using XCEngine::UI::Editor::Widgets::UIEditorTabStripLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripMetrics;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripState;
|
||||
|
||||
TEST(UIEditorTabStripTest, DesiredHeaderWidthReservesCloseButtonBudget) {
|
||||
TEST(UIEditorTabStripTest, DesiredHeaderWidthUsesLabelWidthAndLeftInsetOnly) {
|
||||
UIEditorTabStripMetrics metrics = {};
|
||||
metrics.layoutMetrics.tabHorizontalPadding = 10.0f;
|
||||
metrics.estimatedGlyphWidth = 8.0f;
|
||||
@@ -32,14 +31,14 @@ TEST(UIEditorTabStripTest, DesiredHeaderWidthReservesCloseButtonBudget) {
|
||||
metrics.closeInsetRight = 14.0f;
|
||||
metrics.labelInsetX = 12.0f;
|
||||
|
||||
const float closableWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
const float measuredWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
UIEditorTabStripItem{ "doc-a", "ABCD", true, 0.0f },
|
||||
metrics);
|
||||
const float fixedWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
UIEditorTabStripItem{ "doc-b", "Ignored", false, 42.0f },
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(closableWidth, 58.0f);
|
||||
EXPECT_FLOAT_EQ(measuredWidth, 34.0f);
|
||||
EXPECT_FLOAT_EQ(fixedWidth, 44.0f);
|
||||
}
|
||||
|
||||
@@ -58,16 +57,7 @@ TEST(UIEditorTabStripTest, SelectedIndexResolvesByTabIdAndFallsBackToValidRange)
|
||||
UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, ClosingTabsResolvesSelectionFallbackFromClosedIndex) {
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndexAfterClose(1u, 1u, 3u), 1u);
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndexAfterClose(2u, 2u, 3u), 1u);
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndexAfterClose(2u, 0u, 3u), 1u);
|
||||
EXPECT_EQ(
|
||||
ResolveUIEditorTabStripSelectedIndexAfterClose(0u, 0u, 1u),
|
||||
UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, LayoutUsesCoreTabArrangementAndBuildsCloseRects) {
|
||||
TEST(UIEditorTabStripTest, LayoutUsesCoreTabArrangementWithoutCloseButtons) {
|
||||
UIEditorTabStripMetrics metrics = {};
|
||||
metrics.layoutMetrics.headerHeight = 30.0f;
|
||||
metrics.layoutMetrics.tabMinWidth = 80.0f;
|
||||
@@ -101,20 +91,16 @@ TEST(UIEditorTabStripTest, LayoutUsesCoreTabArrangementAndBuildsCloseRects) {
|
||||
|
||||
ASSERT_EQ(layout.tabHeaderRects.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].width, 90.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].x, 104.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].width, 80.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].x, 94.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].width, 80.0f);
|
||||
|
||||
ASSERT_EQ(layout.closeButtonRects.size(), 2u);
|
||||
EXPECT_TRUE(layout.showCloseButtons[0]);
|
||||
EXPECT_FALSE(layout.showCloseButtons[0]);
|
||||
EXPECT_FALSE(layout.showCloseButtons[1]);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRects[0].x, 76.0f);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRects[0].y, 29.0f);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRects[0].width, 12.0f);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRects[0].height, 12.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, HitTestPrioritizesCloseButtonThenTabThenContent) {
|
||||
TEST(UIEditorTabStripTest, HitTestPrioritizesTabThenHeaderThenContent) {
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", true, 48.0f },
|
||||
{ "doc-b", "Document B", false, 40.0f }
|
||||
@@ -125,14 +111,23 @@ TEST(UIEditorTabStripTest, HitTestPrioritizesCloseButtonThenTabThenContent) {
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state);
|
||||
|
||||
const auto closeHit = HitTestUIEditorTabStrip(layout, state, UIPoint(85.0f, 34.0f));
|
||||
EXPECT_EQ(closeHit.kind, UIEditorTabStripHitTargetKind::CloseButton);
|
||||
EXPECT_EQ(closeHit.index, 0u);
|
||||
const auto rightSideTabHit = HitTestUIEditorTabStrip(
|
||||
layout,
|
||||
state,
|
||||
UIPoint(
|
||||
layout.tabHeaderRects[0].x + layout.tabHeaderRects[0].width - 2.0f,
|
||||
layout.tabHeaderRects[0].y + layout.tabHeaderRects[0].height * 0.5f));
|
||||
EXPECT_EQ(rightSideTabHit.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||
EXPECT_EQ(rightSideTabHit.index, 0u);
|
||||
|
||||
const auto tabHit = HitTestUIEditorTabStrip(layout, state, UIPoint(40.0f, 34.0f));
|
||||
EXPECT_EQ(tabHit.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||
EXPECT_EQ(tabHit.index, 0u);
|
||||
|
||||
const auto headerHit = HitTestUIEditorTabStrip(layout, state, UIPoint(200.0f, 34.0f));
|
||||
EXPECT_EQ(headerHit.kind, UIEditorTabStripHitTargetKind::HeaderBackground);
|
||||
EXPECT_EQ(headerHit.index, UIEditorTabStripInvalidIndex);
|
||||
|
||||
const auto contentHit = HitTestUIEditorTabStrip(layout, state, UIPoint(40.0f, 70.0f));
|
||||
EXPECT_EQ(contentHit.kind, UIEditorTabStripHitTargetKind::Content);
|
||||
EXPECT_EQ(contentHit.index, UIEditorTabStripInvalidIndex);
|
||||
@@ -147,7 +142,6 @@ TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
|
||||
UIEditorTabStripState state = {};
|
||||
state.selectedIndex = 0u;
|
||||
state.hoveredIndex = 1u;
|
||||
state.closeHoveredIndex = 0u;
|
||||
state.focused = true;
|
||||
|
||||
const UIEditorTabStripLayout layout =
|
||||
@@ -155,24 +149,28 @@ TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
|
||||
|
||||
UIDrawList background("TabStripBackground");
|
||||
AppendUIEditorTabStripBackground(background, layout, state);
|
||||
ASSERT_EQ(background.GetCommandCount(), 8u);
|
||||
ASSERT_EQ(background.GetCommandCount(), 9u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[4].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[7].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[5].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[6].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[7].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[8].type, UIDrawCommandType::RectOutline);
|
||||
|
||||
UIDrawList foreground("TabStripForeground");
|
||||
AppendUIEditorTabStripForeground(foreground, layout, items, state);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 9u);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 8u);
|
||||
const auto& foregroundCommands = foreground.GetCommands();
|
||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(foregroundCommands[1].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[1].text, "Document A");
|
||||
EXPECT_EQ(foregroundCommands[5].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[5].text, "X");
|
||||
EXPECT_EQ(foregroundCommands[7].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[7].text, "Document B");
|
||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(foregroundCommands[1].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(foregroundCommands[2].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(foregroundCommands[3].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[3].text, "Document A");
|
||||
EXPECT_EQ(foregroundCommands[5].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(foregroundCommands[6].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[6].text, "Document B");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -15,6 +15,7 @@ using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorTabStripInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorTabStripInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
|
||||
|
||||
std::vector<UIEditorTabStripItem> BuildTabItems() {
|
||||
@@ -73,7 +74,7 @@ UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabAndCloseState) {
|
||||
TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabOnly) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
@@ -85,7 +86,7 @@ TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabAndCloseState)
|
||||
items,
|
||||
{});
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
@@ -93,21 +94,11 @@ TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabAndCloseState)
|
||||
{ MakePointerMove(
|
||||
initialFrame.layout.tabHeaderRects[1].x + 12.0f,
|
||||
initialFrame.layout.tabHeaderRects[1].y + 12.0f) });
|
||||
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||
EXPECT_EQ(state.tabStripState.hoveredIndex, 1u);
|
||||
EXPECT_EQ(state.tabStripState.closeHoveredIndex, XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex);
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakePointerMove(
|
||||
initialFrame.layout.closeButtonRects[0].x + 4.0f,
|
||||
initialFrame.layout.closeButtonRects[0].y + 4.0f) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorTabStripHitTargetKind::CloseButton);
|
||||
EXPECT_EQ(state.tabStripState.hoveredIndex, 0u);
|
||||
EXPECT_EQ(state.tabStripState.closeHoveredIndex, 0u);
|
||||
EXPECT_EQ(state.tabStripState.closeHoveredIndex, UIEditorTabStripInvalidIndex);
|
||||
EXPECT_FALSE(frame.result.closeRequested);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, LeftClickTabSelectsAndFocusesStrip) {
|
||||
@@ -141,38 +132,6 @@ TEST(UIEditorTabStripInteractionTest, LeftClickTabSelectsAndFocusesStrip) {
|
||||
EXPECT_TRUE(state.tabStripState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, LeftClickCloseButtonRequestsCloseWithoutChangingSelection) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint closeCenter = RectCenter(initialFrame.layout.closeButtonRects[1]);
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(closeCenter.x, closeCenter.y),
|
||||
MakePointerUp(closeCenter.x, closeCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.closeRequested);
|
||||
EXPECT_FALSE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(frame.result.closedTabId, "doc-b");
|
||||
EXPECT_EQ(frame.result.closedIndex, 1u);
|
||||
EXPECT_EQ(selectedTabId, "doc-a");
|
||||
EXPECT_TRUE(state.tabStripState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, KeyboardNavigationMovesSelectionWhenFocused) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-b";
|
||||
@@ -238,7 +197,377 @@ TEST(UIEditorTabStripInteractionTest, OutsideClickAndFocusLostClearFocusAndHover
|
||||
items,
|
||||
{ MakePointerLeave(), MakeFocusLost() });
|
||||
EXPECT_FALSE(state.tabStripState.focused);
|
||||
EXPECT_EQ(state.tabStripState.hoveredIndex, XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex);
|
||||
EXPECT_EQ(state.tabStripState.closeHoveredIndex, XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex);
|
||||
EXPECT_EQ(state.tabStripState.hoveredIndex, UIEditorTabStripInvalidIndex);
|
||||
EXPECT_EQ(state.tabStripState.closeHoveredIndex, UIEditorTabStripInvalidIndex);
|
||||
EXPECT_FALSE(state.hasPointerPosition);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, DraggingTabRequestsPointerCaptureAndShowsInsertionPreview) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[0]);
|
||||
const auto& lastRect = initialFrame.layout.tabHeaderRects.back();
|
||||
const UIPoint dragPoint(lastRect.x + lastRect.width - 2.0f, sourceCenter.y);
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(frame.result.dragStarted);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-a");
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 0u);
|
||||
EXPECT_EQ(frame.result.dropInsertionIndex, items.size());
|
||||
EXPECT_EQ(frame.result.reorderToIndex, 2u);
|
||||
EXPECT_TRUE(frame.result.reorderPreviewActive);
|
||||
EXPECT_EQ(frame.result.reorderPreviewIndex, 2u);
|
||||
EXPECT_TRUE(state.reorderCaptureActive);
|
||||
EXPECT_EQ(state.reorderSourceIndex, 0u);
|
||||
EXPECT_EQ(state.reorderPreviewIndex, 2u);
|
||||
EXPECT_TRUE(frame.layout.insertionPreview.visible);
|
||||
EXPECT_EQ(frame.layout.insertionPreview.insertionIndex, items.size());
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, DroppingDraggedTabEmitsSameStackReorderRequest) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[0]);
|
||||
const auto& lastRect = initialFrame.layout.tabHeaderRects.back();
|
||||
const UIPoint dragPoint(lastRect.x + lastRect.width - 2.0f, sourceCenter.y);
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y),
|
||||
MakePointerUp(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.dragEnded);
|
||||
EXPECT_TRUE(frame.result.reorderRequested);
|
||||
EXPECT_FALSE(frame.result.dragCanceled);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-a");
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 0u);
|
||||
EXPECT_EQ(frame.result.dropInsertionIndex, items.size());
|
||||
EXPECT_EQ(frame.result.reorderToIndex, 2u);
|
||||
EXPECT_FALSE(state.reorderCaptureActive);
|
||||
EXPECT_EQ(state.reorderSourceIndex, UIEditorTabStripInvalidIndex);
|
||||
EXPECT_EQ(state.reorderPreviewIndex, UIEditorTabStripInvalidIndex);
|
||||
EXPECT_FALSE(frame.layout.insertionPreview.visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, DroppingRightmostTabToFrontCommitsReorderRequest) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-c";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[2]);
|
||||
const UIPoint dropPoint(
|
||||
initialFrame.layout.tabHeaderRects[0].x + 4.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dropPoint.x, dropPoint.y),
|
||||
MakePointerUp(dropPoint.x, dropPoint.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.dragEnded);
|
||||
EXPECT_TRUE(frame.result.reorderRequested);
|
||||
EXPECT_FALSE(frame.result.dragCanceled);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-c");
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 2u);
|
||||
EXPECT_EQ(frame.result.dropInsertionIndex, 0u);
|
||||
EXPECT_EQ(frame.result.reorderToIndex, 0u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, DroppingRightmostTabIntoMiddleCommitsReorderRequest) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-c";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[2]);
|
||||
const UIPoint dropPoint(
|
||||
initialFrame.layout.tabHeaderRects[1].x + 4.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dropPoint.x, dropPoint.y),
|
||||
MakePointerUp(dropPoint.x, dropPoint.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.dragEnded);
|
||||
EXPECT_TRUE(frame.result.reorderRequested);
|
||||
EXPECT_FALSE(frame.result.dragCanceled);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-c");
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 2u);
|
||||
EXPECT_EQ(frame.result.dropInsertionIndex, 1u);
|
||||
EXPECT_EQ(frame.result.reorderToIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, SeparateFrameDropOfRightmostTabIntoMiddleStillCommitsReorderRequest) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-c";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[2]);
|
||||
const UIPoint dropPoint(
|
||||
initialFrame.layout.tabHeaderRects[1].x + 4.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dropPoint.x, dropPoint.y)
|
||||
});
|
||||
ASSERT_TRUE(frame.result.requestPointerCapture);
|
||||
ASSERT_TRUE(frame.result.reorderPreviewActive);
|
||||
ASSERT_TRUE(state.reorderDragState.active);
|
||||
ASSERT_FALSE(state.reorderDragState.targetId.empty());
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakePointerUp(dropPoint.x, dropPoint.y) });
|
||||
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.dragEnded);
|
||||
EXPECT_TRUE(frame.result.reorderRequested);
|
||||
EXPECT_FALSE(frame.result.dragCanceled);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-c");
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 2u);
|
||||
EXPECT_EQ(frame.result.dropInsertionIndex, 1u);
|
||||
EXPECT_EQ(frame.result.reorderToIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, ReleasingOutsideHeaderStillCommitsLastVisibleReorderPreview) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[0]);
|
||||
const auto& lastRect = initialFrame.layout.tabHeaderRects.back();
|
||||
const UIPoint dragPoint(lastRect.x + lastRect.width - 2.0f, sourceCenter.y);
|
||||
const UIPoint releasePoint(
|
||||
dragPoint.x,
|
||||
initialFrame.layout.headerRect.y + initialFrame.layout.headerRect.height + 8.0f);
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
ASSERT_TRUE(frame.result.reorderPreviewActive);
|
||||
ASSERT_EQ(state.tabStripState.reorder.previewInsertionIndex, items.size());
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakePointerUp(releasePoint.x, releasePoint.y) });
|
||||
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(frame.result.dragEnded);
|
||||
EXPECT_TRUE(frame.result.reorderRequested);
|
||||
EXPECT_FALSE(frame.result.dragCanceled);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-a");
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 0u);
|
||||
EXPECT_EQ(frame.result.dropInsertionIndex, items.size());
|
||||
EXPECT_EQ(frame.result.reorderToIndex, 2u);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, EscapeCancelsActiveTabDragAndReleasesCapture) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[1]);
|
||||
const UIPoint dragPoint(
|
||||
initialFrame.layout.tabHeaderRects[2].x + 8.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
ASSERT_TRUE(frame.result.dragStarted);
|
||||
ASSERT_TRUE(state.reorderCaptureActive);
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakeKeyDown(KeyCode::Escape) });
|
||||
|
||||
EXPECT_TRUE(frame.result.dragCanceled);
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(frame.result.reorderRequested);
|
||||
EXPECT_EQ(frame.result.draggedTabId, "doc-b");
|
||||
EXPECT_FALSE(state.reorderCaptureActive);
|
||||
EXPECT_EQ(state.reorderSourceIndex, UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, FocusLostCancelsActiveTabDragAndClearsPreview) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-a";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[0]);
|
||||
const UIPoint dragPoint(
|
||||
initialFrame.layout.tabHeaderRects[2].x + 8.0f,
|
||||
sourceCenter.y);
|
||||
|
||||
auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(dragPoint.x, dragPoint.y)
|
||||
});
|
||||
ASSERT_TRUE(frame.result.dragStarted);
|
||||
ASSERT_TRUE(frame.result.reorderPreviewActive);
|
||||
|
||||
frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{ MakeFocusLost() });
|
||||
|
||||
EXPECT_TRUE(frame.result.dragCanceled);
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(frame.result.reorderRequested);
|
||||
EXPECT_FALSE(state.tabStripState.focused);
|
||||
EXPECT_FALSE(state.reorderCaptureActive);
|
||||
EXPECT_EQ(state.reorderPreviewIndex, UIEditorTabStripInvalidIndex);
|
||||
EXPECT_FALSE(frame.layout.insertionPreview.visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripInteractionTest, VisibleInsertionIndexTracksHeaderGapsInsteadOfFinalTabIndices) {
|
||||
const auto items = BuildTabItems();
|
||||
std::string selectedTabId = "doc-b";
|
||||
UIEditorTabStripInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{});
|
||||
const UIPoint sourceCenter = RectCenter(initialFrame.layout.tabHeaderRects[1]);
|
||||
const float betweenFirstAndSecond =
|
||||
(initialFrame.layout.tabHeaderRects[0].x + initialFrame.layout.tabHeaderRects[0].width +
|
||||
initialFrame.layout.tabHeaderRects[1].x) * 0.5f;
|
||||
|
||||
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||
state,
|
||||
selectedTabId,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
items,
|
||||
{
|
||||
MakePointerDown(sourceCenter.x, sourceCenter.y),
|
||||
MakePointerMove(betweenFirstAndSecond, sourceCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_TRUE(frame.result.reorderPreviewActive);
|
||||
EXPECT_EQ(frame.result.dragSourceIndex, 1u);
|
||||
EXPECT_EQ(frame.result.dropInsertionIndex, 1u);
|
||||
EXPECT_EQ(frame.result.reorderToIndex, 1u);
|
||||
EXPECT_EQ(frame.result.reorderPreviewIndex, 1u);
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ TEST(UIEditorViewportShellTest, ResolveRequestUsesViewportInputRectSize) {
|
||||
spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(request.slotLayout.inputRect.width, 800.0f);
|
||||
EXPECT_FLOAT_EQ(request.slotLayout.inputRect.height, 532.0f);
|
||||
EXPECT_FLOAT_EQ(request.slotLayout.inputRect.height, 554.0f);
|
||||
EXPECT_FLOAT_EQ(request.requestedViewportSize.width, 800.0f);
|
||||
EXPECT_FLOAT_EQ(request.requestedViewportSize.height, 532.0f);
|
||||
EXPECT_FLOAT_EQ(request.requestedViewportSize.height, 554.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportShellTest, ResolveRequestTracksChromeBarVisibility) {
|
||||
|
||||
@@ -92,7 +92,7 @@ TEST(UIEditorViewportSlotTest, DesiredToolWidthUsesExplicitValueBeforeEstimatedL
|
||||
inferredWidth.label = "Scene";
|
||||
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(explicitWidth), 88.0f);
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(inferredWidth), 55.0f);
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(inferredWidth), 48.5f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportSlotTest, LayoutBuildsTopBarSurfaceBottomBarAndAspectFittedTexture) {
|
||||
@@ -142,12 +142,12 @@ TEST(UIEditorViewportSlotTest, ToolItemsAlignToEdgesAndTitleRectClampsBetweenToo
|
||||
BuildToolItems(),
|
||||
{});
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[0].x, 12.0f);
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[0].x, 8.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);
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[1].x, 768.0f);
|
||||
EXPECT_FLOAT_EQ(layout.toolItemRects[2].x, 820.0f);
|
||||
EXPECT_FLOAT_EQ(layout.titleRect.x, 110.0f);
|
||||
EXPECT_FLOAT_EQ(layout.titleRect.width, 652.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorViewportSlotTest, HitTestPrioritizesToolThenStatusThenSurface) {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||
@@ -9,6 +11,9 @@ using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandKindName;
|
||||
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
@@ -17,7 +22,11 @@ using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceControllerValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
@@ -25,6 +34,9 @@ UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "doc-c", "Document C", {}, true, true, true },
|
||||
{ "hidden-a", "Hidden A", {}, true, true, true },
|
||||
{ "hidden-b", "Hidden B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true },
|
||||
{ "root", "Root", {}, true, false, false }
|
||||
};
|
||||
@@ -49,6 +61,34 @@ UIEditorWorkspaceModel BuildWorkspace() {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildReorderWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("hidden-a-node", "hidden-a", "Hidden A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
|
||||
BuildUIEditorWorkspacePanel("hidden-b-node", "hidden-b", "Hidden B", true),
|
||||
BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true)
|
||||
},
|
||||
2u);
|
||||
workspace.activePanelId = "doc-b";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSession BuildReorderSession() {
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "hidden-a", true, false },
|
||||
{ "doc-b", true, true },
|
||||
{ "hidden-b", true, false },
|
||||
{ "doc-c", true, true }
|
||||
};
|
||||
return session;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, CommandNameHelpersExposeStableDebugNames) {
|
||||
@@ -146,3 +186,201 @@ TEST(UIEditorWorkspaceControllerTest, RejectsUnknownPanelAndNonCloseablePanelCom
|
||||
EXPECT_EQ(nonCloseable.status, UIEditorWorkspaceCommandStatus::Rejected);
|
||||
EXPECT_EQ(rootController.GetWorkspace().activePanelId, "root");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, ReorderTabUsesModelSurgeryAndPreservesSelectionActiveAndSession) {
|
||||
UIEditorWorkspaceController controller(
|
||||
BuildPanelRegistry(),
|
||||
BuildReorderWorkspace(),
|
||||
BuildReorderSession());
|
||||
|
||||
const auto result = controller.ReorderTab("document-tabs", "doc-c", 0u);
|
||||
EXPECT_EQ(result.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activePanelId, "doc-b");
|
||||
ASSERT_EQ(result.visiblePanelIds.size(), 1u);
|
||||
EXPECT_EQ(result.visiblePanelIds[0], "doc-b");
|
||||
|
||||
const UIEditorWorkspaceNode* tabStack =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "document-tabs");
|
||||
ASSERT_NE(tabStack, nullptr);
|
||||
ASSERT_EQ(tabStack->children.size(), 5u);
|
||||
EXPECT_EQ(tabStack->children[0].panel.panelId, "doc-c");
|
||||
EXPECT_EQ(tabStack->children[1].panel.panelId, "hidden-a");
|
||||
EXPECT_EQ(tabStack->children[2].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(tabStack->children[3].panel.panelId, "hidden-b");
|
||||
EXPECT_EQ(tabStack->children[4].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(tabStack->selectedTabIndex, 4u);
|
||||
|
||||
const auto* hiddenState =
|
||||
FindUIEditorPanelSessionState(controller.GetSession(), "hidden-a");
|
||||
ASSERT_NE(hiddenState, nullptr);
|
||||
EXPECT_TRUE(hiddenState->open);
|
||||
EXPECT_FALSE(hiddenState->visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, ReorderTabRejectsHiddenPanelsAndOutOfRangeTargetsAndReportsNoOp) {
|
||||
UIEditorWorkspaceController controller(
|
||||
BuildPanelRegistry(),
|
||||
BuildReorderWorkspace(),
|
||||
BuildReorderSession());
|
||||
|
||||
const auto hidden = controller.ReorderTab("document-tabs", "hidden-a", 0u);
|
||||
EXPECT_EQ(hidden.status, UIEditorWorkspaceLayoutOperationStatus::Rejected);
|
||||
|
||||
const auto outOfRange = controller.ReorderTab("document-tabs", "doc-a", 4u);
|
||||
EXPECT_EQ(outOfRange.status, UIEditorWorkspaceLayoutOperationStatus::Rejected);
|
||||
|
||||
const auto noOp = controller.ReorderTab("document-tabs", "doc-a", 0u);
|
||||
EXPECT_EQ(noOp.status, UIEditorWorkspaceLayoutOperationStatus::NoOp);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, MoveTabToStackMovesVisibleTabAcrossStacks) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.55f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-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";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
UIEditorWorkspaceController controller(BuildPanelRegistry(), workspace, session);
|
||||
|
||||
const auto result = controller.MoveTabToStack("left-tabs", "doc-b", "details-node", 1u);
|
||||
EXPECT_EQ(result.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activePanelId, "doc-b");
|
||||
|
||||
const UIEditorWorkspaceNode* leftTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "left-tabs");
|
||||
const UIEditorWorkspaceNode* detailsTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "details-node");
|
||||
ASSERT_NE(leftTabs, nullptr);
|
||||
ASSERT_NE(detailsTabs, nullptr);
|
||||
ASSERT_EQ(leftTabs->children.size(), 1u);
|
||||
ASSERT_EQ(detailsTabs->children.size(), 2u);
|
||||
EXPECT_EQ(detailsTabs->children[1].panel.panelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, DockTabRelativeCreatesSplitAroundTargetStack) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.55f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-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";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
UIEditorWorkspaceController controller(BuildPanelRegistry(), workspace, session);
|
||||
|
||||
const auto result = controller.DockTabRelative(
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"details-node",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f);
|
||||
EXPECT_EQ(result.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activePanelId, "doc-b");
|
||||
|
||||
const UIEditorWorkspaceNode* detailsTabs =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), "details-node");
|
||||
ASSERT_NE(detailsTabs, nullptr);
|
||||
EXPECT_EQ(detailsTabs->kind, XCEngine::UI::Editor::UIEditorWorkspaceNodeKind::TabStack);
|
||||
|
||||
bool foundDockedTab = false;
|
||||
for (const std::string candidate : { "details-node__dock_doc-b_stack", "details-node__dock_doc-b_stack-1" }) {
|
||||
const UIEditorWorkspaceNode* docked =
|
||||
FindUIEditorWorkspaceNode(controller.GetWorkspace(), candidate);
|
||||
if (docked != nullptr) {
|
||||
foundDockedTab = docked->children.size() == 1u &&
|
||||
docked->children[0].panel.panelId == "doc-b";
|
||||
if (foundDockedTab) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(foundDockedTab);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceControllerTest, DockTabRelativeReturnsNoOpWhenRedockingAlreadyDockedSingleTabStack) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.55f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-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";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
UIEditorWorkspaceController controller(BuildPanelRegistry(), workspace, session);
|
||||
|
||||
const auto first = controller.DockTabRelative(
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"details-node",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f);
|
||||
ASSERT_EQ(first.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
|
||||
|
||||
std::string movedStackId = {};
|
||||
for (const std::string candidate : { "details-node__dock_doc-b_stack", "details-node__dock_doc-b_stack-1" }) {
|
||||
if (FindUIEditorWorkspaceNode(controller.GetWorkspace(), candidate) != nullptr) {
|
||||
movedStackId = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(movedStackId.empty());
|
||||
|
||||
const UIEditorWorkspaceModel afterFirstDock = controller.GetWorkspace();
|
||||
|
||||
const auto second = controller.DockTabRelative(
|
||||
movedStackId,
|
||||
"doc-b",
|
||||
"details-node",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f);
|
||||
EXPECT_EQ(second.status, UIEditorWorkspaceLayoutOperationStatus::NoOp);
|
||||
EXPECT_EQ(second.activePanelId, "doc-b");
|
||||
EXPECT_TRUE(
|
||||
AreUIEditorWorkspaceModelsEquivalent(
|
||||
controller.GetWorkspace(),
|
||||
afterFirstDock));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
@@ -12,17 +13,25 @@ using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent;
|
||||
using XCEngine::UI::Editor::CanonicalizeUIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
|
||||
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceActivePanel;
|
||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::TryActivateUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::TryDockUIEditorWorkspaceTabRelative;
|
||||
using XCEngine::UI::Editor::TryMoveUIEditorWorkspaceTabToStack;
|
||||
using XCEngine::UI::Editor::TryReorderUIEditorWorkspaceTab;
|
||||
using XCEngine::UI::Editor::TrySetUIEditorWorkspaceSplitRatio;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceNode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceValidationCode;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceVisiblePanel;
|
||||
using XCEngine::UI::Editor::ValidateUIEditorWorkspace;
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(const UIEditorWorkspaceModel& workspace) {
|
||||
@@ -170,6 +179,78 @@ TEST(UIEditorWorkspaceModelTest, SplitRatioMutationTargetsSplitNodeAndRejectsInv
|
||||
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 1.0f));
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, ReorderTabMovesVisibleTabsAndPreservesHiddenOrderSelectionAndActivePanel) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("hidden-a-node", "hidden-a", "Hidden A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
|
||||
BuildUIEditorWorkspacePanel("hidden-b-node", "hidden-b", "Hidden B", true),
|
||||
BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true)
|
||||
},
|
||||
2u);
|
||||
workspace.activePanelId = "doc-b";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "hidden-a", true, false },
|
||||
{ "doc-b", true, true },
|
||||
{ "hidden-b", true, false },
|
||||
{ "doc-c", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryReorderUIEditorWorkspaceTab(
|
||||
workspace,
|
||||
session,
|
||||
"document-tabs",
|
||||
"doc-c",
|
||||
0u));
|
||||
|
||||
const UIEditorWorkspaceNode* tabStack =
|
||||
FindUIEditorWorkspaceNode(workspace, "document-tabs");
|
||||
ASSERT_NE(tabStack, nullptr);
|
||||
ASSERT_EQ(tabStack->children.size(), 5u);
|
||||
EXPECT_EQ(tabStack->children[0].panel.panelId, "doc-c");
|
||||
EXPECT_EQ(tabStack->children[1].panel.panelId, "hidden-a");
|
||||
EXPECT_EQ(tabStack->children[2].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(tabStack->children[3].panel.panelId, "hidden-b");
|
||||
EXPECT_EQ(tabStack->children[4].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(tabStack->selectedTabIndex, 4u);
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
|
||||
EXPECT_FALSE(TryReorderUIEditorWorkspaceTab(
|
||||
workspace,
|
||||
session,
|
||||
"document-tabs",
|
||||
"hidden-a",
|
||||
0u));
|
||||
|
||||
ASSERT_TRUE(TryReorderUIEditorWorkspaceTab(
|
||||
workspace,
|
||||
session,
|
||||
"document-tabs",
|
||||
"doc-a",
|
||||
3u));
|
||||
tabStack = FindUIEditorWorkspaceNode(workspace, "document-tabs");
|
||||
ASSERT_NE(tabStack, nullptr);
|
||||
EXPECT_EQ(tabStack->children[0].panel.panelId, "doc-c");
|
||||
EXPECT_EQ(tabStack->children[1].panel.panelId, "hidden-a");
|
||||
EXPECT_EQ(tabStack->children[2].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(tabStack->children[3].panel.panelId, "hidden-b");
|
||||
EXPECT_EQ(tabStack->children[4].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(tabStack->selectedTabIndex, 2u);
|
||||
|
||||
EXPECT_FALSE(TryReorderUIEditorWorkspaceTab(
|
||||
workspace,
|
||||
session,
|
||||
"document-tabs",
|
||||
"doc-a",
|
||||
4u));
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, CanonicalizeWrapsStandalonePanelsIntoSingleTabStacks) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
@@ -195,3 +276,254 @@ TEST(UIEditorWorkspaceModelTest, CanonicalizeWrapsStandalonePanelsIntoSingleTabS
|
||||
"left");
|
||||
EXPECT_EQ(canonicalWorkspace.root.children[1].kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, MoveTabToStackMergesIntoTargetStackAndActivatesMovedPanel) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.58f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"right-tabs",
|
||||
"details",
|
||||
"Details",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryMoveUIEditorWorkspaceTabToStack(
|
||||
workspace,
|
||||
session,
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"right-tabs",
|
||||
1u));
|
||||
|
||||
const UIEditorWorkspaceNode* leftTabs =
|
||||
FindUIEditorWorkspaceNode(workspace, "left-tabs");
|
||||
const UIEditorWorkspaceNode* rightTabs =
|
||||
FindUIEditorWorkspaceNode(workspace, "right-tabs");
|
||||
ASSERT_NE(leftTabs, nullptr);
|
||||
ASSERT_NE(rightTabs, nullptr);
|
||||
ASSERT_EQ(leftTabs->children.size(), 1u);
|
||||
ASSERT_EQ(rightTabs->children.size(), 2u);
|
||||
EXPECT_EQ(leftTabs->children[0].panel.panelId, "doc-a");
|
||||
EXPECT_EQ(rightTabs->children[0].panel.panelId, "details");
|
||||
EXPECT_EQ(rightTabs->children[1].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(rightTabs->selectedTabIndex, 1u);
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, DockTabRelativeSplitsTargetStackAndCreatesNewLeaf) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.58f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"right-tabs",
|
||||
"details",
|
||||
"Details",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"right-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f));
|
||||
|
||||
const UIEditorWorkspaceNode* rightTabs =
|
||||
FindUIEditorWorkspaceNode(workspace, "right-tabs");
|
||||
ASSERT_NE(rightTabs, nullptr);
|
||||
ASSERT_EQ(rightTabs->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(rightTabs->children.size(), 1u);
|
||||
EXPECT_EQ(rightTabs->children[0].panel.panelId, "details");
|
||||
|
||||
const UIEditorWorkspaceNode* movedTabStack = nullptr;
|
||||
if (const UIEditorWorkspaceNode* rootSplit =
|
||||
FindUIEditorWorkspaceNode(workspace, "root-split");
|
||||
rootSplit != nullptr) {
|
||||
const auto visibleIds = CollectVisiblePanelIds(workspace);
|
||||
ASSERT_EQ(visibleIds.size(), 3u);
|
||||
EXPECT_EQ(visibleIds[0], "doc-a");
|
||||
EXPECT_EQ(visibleIds[1], "details");
|
||||
EXPECT_EQ(visibleIds[2], "doc-b");
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace);
|
||||
ASSERT_EQ(visiblePanels.size(), 3u);
|
||||
EXPECT_EQ(visiblePanels[2].panelId, "doc-b");
|
||||
|
||||
for (const std::string candidate : { "right-tabs__dock_doc-b_stack", "right-tabs__dock_doc-b_stack-1" }) {
|
||||
movedTabStack = FindUIEditorWorkspaceNode(workspace, candidate);
|
||||
if (movedTabStack != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(movedTabStack, nullptr);
|
||||
ASSERT_EQ(movedTabStack->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(movedTabStack->children.size(), 1u);
|
||||
EXPECT_EQ(movedTabStack->children[0].panel.panelId, "doc-b");
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, DockTabRelativeCanRedockSingleTabBranchBackOntoSiblingStack) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceTabStack(
|
||||
"center-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("scene-node", "scene", "Scene", true),
|
||||
BuildUIEditorWorkspacePanel("game-node", "game", "Game", true)
|
||||
},
|
||||
0u);
|
||||
workspace.activePanelId = "scene";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "scene", true, true },
|
||||
{ "game", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
"center-tabs",
|
||||
"scene",
|
||||
"center-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Top,
|
||||
0.5f));
|
||||
|
||||
const UIEditorWorkspaceNode* firstDockStack =
|
||||
FindUIEditorWorkspaceNode(workspace, "center-tabs__dock_scene_stack");
|
||||
ASSERT_NE(firstDockStack, nullptr);
|
||||
ASSERT_EQ(firstDockStack->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(firstDockStack->children.size(), 1u);
|
||||
EXPECT_EQ(firstDockStack->children[0].panel.panelId, "scene");
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
"center-tabs__dock_scene_stack",
|
||||
"scene",
|
||||
"center-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Top,
|
||||
0.5f));
|
||||
|
||||
const auto validation = ValidateUIEditorWorkspace(workspace);
|
||||
ASSERT_TRUE(validation.IsValid()) << validation.message;
|
||||
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace);
|
||||
ASSERT_EQ(visiblePanels.size(), 2u);
|
||||
EXPECT_EQ(visiblePanels[0].panelId, "scene");
|
||||
EXPECT_EQ(visiblePanels[1].panelId, "game");
|
||||
EXPECT_EQ(workspace.activePanelId, "scene");
|
||||
}
|
||||
|
||||
TEST(UIEditorWorkspaceModelTest, DockTabRelativeCanRedockGeneratedSingleTabStackWithoutChangingLayout) {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.58f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"left-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"right-tabs",
|
||||
"details",
|
||||
"Details",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates = {
|
||||
{ "doc-a", true, true },
|
||||
{ "doc-b", true, true },
|
||||
{ "details", true, true }
|
||||
};
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
"left-tabs",
|
||||
"doc-b",
|
||||
"right-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f));
|
||||
|
||||
std::string movedStackId = {};
|
||||
for (const std::string candidate : { "right-tabs__dock_doc-b_stack", "right-tabs__dock_doc-b_stack-1" }) {
|
||||
if (FindUIEditorWorkspaceNode(workspace, candidate) != nullptr) {
|
||||
movedStackId = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(movedStackId.empty());
|
||||
|
||||
const UIEditorWorkspaceModel afterFirstDock = workspace;
|
||||
|
||||
ASSERT_TRUE(TryDockUIEditorWorkspaceTabRelative(
|
||||
workspace,
|
||||
session,
|
||||
movedStackId,
|
||||
"doc-b",
|
||||
"right-tabs",
|
||||
UIEditorWorkspaceDockPlacement::Bottom,
|
||||
0.4f));
|
||||
|
||||
const auto validation = ValidateUIEditorWorkspace(workspace);
|
||||
ASSERT_TRUE(validation.IsValid()) << validation.message;
|
||||
EXPECT_TRUE(AreUIEditorWorkspaceModelsEquivalent(workspace, afterFirstDock));
|
||||
EXPECT_EQ(workspace.activePanelId, "doc-b");
|
||||
|
||||
const UIEditorWorkspaceNode* rightTabs =
|
||||
FindUIEditorWorkspaceNode(workspace, "right-tabs");
|
||||
ASSERT_NE(rightTabs, nullptr);
|
||||
EXPECT_EQ(rightTabs->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(rightTabs->children.size(), 1u);
|
||||
EXPECT_EQ(rightTabs->children[0].panel.panelId, "details");
|
||||
|
||||
const UIEditorWorkspaceNode* movedTabStack =
|
||||
FindUIEditorWorkspaceNode(workspace, movedStackId);
|
||||
ASSERT_NE(movedTabStack, nullptr);
|
||||
ASSERT_EQ(movedTabStack->kind, UIEditorWorkspaceNodeKind::TabStack);
|
||||
ASSERT_EQ(movedTabStack->children.size(), 1u);
|
||||
EXPECT_EQ(movedTabStack->children[0].panel.panelId, "doc-b");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user