Refine XCEditor docking and DPI rendering
This commit is contained in:
@@ -22,6 +22,7 @@ endfunction()
|
|||||||
set(XCUI_EDITOR_FOUNDATION_SOURCES
|
set(XCUI_EDITOR_FOUNDATION_SOURCES
|
||||||
src/Foundation/UIEditorCommandDispatcher.cpp
|
src/Foundation/UIEditorCommandDispatcher.cpp
|
||||||
src/Foundation/UIEditorCommandRegistry.cpp
|
src/Foundation/UIEditorCommandRegistry.cpp
|
||||||
|
src/Foundation/UIEditorRuntimeTrace.cpp
|
||||||
src/Foundation/UIEditorShortcutManager.cpp
|
src/Foundation/UIEditorShortcutManager.cpp
|
||||||
src/Foundation/UIEditorTheme.cpp
|
src/Foundation/UIEditorTheme.cpp
|
||||||
)
|
)
|
||||||
@@ -129,6 +130,7 @@ add_library(XCUIEditorHost STATIC
|
|||||||
target_include_directories(XCUIEditorHost
|
target_include_directories(XCUIEditorHost
|
||||||
PUBLIC
|
PUBLIC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
${CMAKE_SOURCE_DIR}/engine/include
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ namespace XCEngine::UI::Editor::Host {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect) {
|
constexpr float kBaseDpi = 96.0f;
|
||||||
return D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
|
constexpr float kDefaultFontSize = 16.0f;
|
||||||
}
|
|
||||||
|
|
||||||
std::string HrToString(const char* operation, HRESULT hr) {
|
std::string HrToString(const char* operation, HRESULT hr) {
|
||||||
char buffer[128] = {};
|
char buffer[128] = {};
|
||||||
@@ -18,6 +17,27 @@ std::string HrToString(const char* operation, HRESULT hr) {
|
|||||||
return buffer;
|
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
|
} // namespace
|
||||||
|
|
||||||
bool NativeRenderer::Initialize(HWND hwnd) {
|
bool NativeRenderer::Initialize(HWND hwnd) {
|
||||||
@@ -64,6 +84,17 @@ void NativeRenderer::Shutdown() {
|
|||||||
m_hwnd = nullptr;
|
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) {
|
void NativeRenderer::Resize(UINT width, UINT height) {
|
||||||
if (!m_renderTarget || width == 0 || height == 0) {
|
if (!m_renderTarget || width == 0 || height == 0) {
|
||||||
return;
|
return;
|
||||||
@@ -104,6 +135,52 @@ const std::string& NativeRenderer::GetLastRenderError() const {
|
|||||||
return m_lastRenderError;
|
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(
|
bool NativeRenderer::CaptureToPng(
|
||||||
const ::XCEngine::UI::UIDrawData& drawData,
|
const ::XCEngine::UI::UIDrawData& drawData,
|
||||||
UINT width,
|
UINT width,
|
||||||
@@ -146,7 +223,9 @@ bool NativeRenderer::CaptureToPng(
|
|||||||
|
|
||||||
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties(
|
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties(
|
||||||
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
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;
|
Microsoft::WRL::ComPtr<ID2D1RenderTarget> offscreenRenderTarget;
|
||||||
hr = m_d2dFactory->CreateWicBitmapRenderTarget(
|
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 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 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(
|
const D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties(
|
||||||
m_hwnd,
|
m_hwnd,
|
||||||
D2D1::SizeU(width, height));
|
D2D1::SizeU(width, height));
|
||||||
@@ -324,6 +407,7 @@ bool NativeRenderer::CreateDeviceResources() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_renderTarget->SetDpi(kBaseDpi, kBaseDpi);
|
||||||
m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
||||||
m_lastRenderError.clear();
|
m_lastRenderError.clear();
|
||||||
return true;
|
return true;
|
||||||
@@ -333,6 +417,7 @@ bool NativeRenderer::RenderToTarget(
|
|||||||
ID2D1RenderTarget& renderTarget,
|
ID2D1RenderTarget& renderTarget,
|
||||||
ID2D1SolidColorBrush& solidBrush,
|
ID2D1SolidColorBrush& solidBrush,
|
||||||
const ::XCEngine::UI::UIDrawData& drawData) {
|
const ::XCEngine::UI::UIDrawData& drawData) {
|
||||||
|
renderTarget.SetDpi(kBaseDpi, kBaseDpi);
|
||||||
renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
||||||
renderTarget.BeginDraw();
|
renderTarget.BeginDraw();
|
||||||
renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f));
|
renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f));
|
||||||
@@ -358,13 +443,15 @@ void NativeRenderer::RenderCommand(
|
|||||||
const ::XCEngine::UI::UIDrawCommand& command,
|
const ::XCEngine::UI::UIDrawCommand& command,
|
||||||
std::vector<D2D1_RECT_F>& clipStack) {
|
std::vector<D2D1_RECT_F>& clipStack) {
|
||||||
solidBrush.SetColor(ToD2DColor(command.color));
|
solidBrush.SetColor(ToD2DColor(command.color));
|
||||||
|
const float dpiScale = ClampDpiScale(m_dpiScale);
|
||||||
|
|
||||||
switch (command.type) {
|
switch (command.type) {
|
||||||
case ::XCEngine::UI::UIDrawCommandType::FilledRect: {
|
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) {
|
if (command.rounding > 0.0f) {
|
||||||
renderTarget.FillRoundedRectangle(
|
renderTarget.FillRoundedRectangle(
|
||||||
D2D1::RoundedRect(rect, command.rounding, command.rounding),
|
D2D1::RoundedRect(rect, rounding, rounding),
|
||||||
&solidBrush);
|
&solidBrush);
|
||||||
} else {
|
} else {
|
||||||
renderTarget.FillRectangle(rect, &solidBrush);
|
renderTarget.FillRectangle(rect, &solidBrush);
|
||||||
@@ -372,11 +459,12 @@ void NativeRenderer::RenderCommand(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ::XCEngine::UI::UIDrawCommandType::RectOutline: {
|
case ::XCEngine::UI::UIDrawCommandType::RectOutline: {
|
||||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||||
const float thickness = command.thickness > 0.0f ? command.thickness : 1.0f;
|
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) {
|
if (command.rounding > 0.0f) {
|
||||||
renderTarget.DrawRoundedRectangle(
|
renderTarget.DrawRoundedRectangle(
|
||||||
D2D1::RoundedRect(rect, command.rounding, command.rounding),
|
D2D1::RoundedRect(rect, rounding, rounding),
|
||||||
&solidBrush,
|
&solidBrush,
|
||||||
thickness);
|
thickness);
|
||||||
} else {
|
} else {
|
||||||
@@ -389,8 +477,9 @@ void NativeRenderer::RenderCommand(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float fontSize = command.fontSize > 0.0f ? command.fontSize : 16.0f;
|
const float fontSize = ResolveFontSize(command.fontSize);
|
||||||
IDWriteTextFormat* textFormat = GetTextFormat(fontSize);
|
const float scaledFontSize = fontSize * dpiScale;
|
||||||
|
IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize);
|
||||||
if (textFormat == nullptr) {
|
if (textFormat == nullptr) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -401,11 +490,14 @@ void NativeRenderer::RenderCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const D2D1_SIZE_F targetSize = renderTarget.GetSize();
|
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(
|
const D2D1_RECT_F layoutRect = D2D1::RectF(
|
||||||
command.position.x,
|
originX,
|
||||||
command.position.y,
|
originY,
|
||||||
targetSize.width,
|
targetSize.width,
|
||||||
command.position.y + fontSize * 1.8f);
|
originY + lineHeight);
|
||||||
renderTarget.DrawTextW(
|
renderTarget.DrawTextW(
|
||||||
text.c_str(),
|
text.c_str(),
|
||||||
static_cast<UINT32>(text.size()),
|
static_cast<UINT32>(text.size()),
|
||||||
@@ -413,7 +505,7 @@ void NativeRenderer::RenderCommand(
|
|||||||
layoutRect,
|
layoutRect,
|
||||||
&solidBrush,
|
&solidBrush,
|
||||||
D2D1_DRAW_TEXT_OPTIONS_CLIP,
|
D2D1_DRAW_TEXT_OPTIONS_CLIP,
|
||||||
DWRITE_MEASURING_MODE_NATURAL);
|
DWRITE_MEASURING_MODE_GDI_NATURAL);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ::XCEngine::UI::UIDrawCommandType::Image: {
|
case ::XCEngine::UI::UIDrawCommandType::Image: {
|
||||||
@@ -421,12 +513,12 @@ void NativeRenderer::RenderCommand(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||||
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
|
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ::XCEngine::UI::UIDrawCommandType::PushClipRect: {
|
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);
|
renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||||
clipStack.push_back(rect);
|
clipStack.push_back(rect);
|
||||||
break;
|
break;
|
||||||
@@ -443,12 +535,13 @@ void NativeRenderer::RenderCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
|
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const {
|
||||||
if (!m_dwriteFactory) {
|
if (!m_dwriteFactory) {
|
||||||
return nullptr;
|
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);
|
const auto found = m_textFormats.find(key);
|
||||||
if (found != m_textFormats.end()) {
|
if (found != m_textFormats.end()) {
|
||||||
return found->second.Get();
|
return found->second.Get();
|
||||||
@@ -461,7 +554,7 @@ IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
|
|||||||
DWRITE_FONT_WEIGHT_REGULAR,
|
DWRITE_FONT_WEIGHT_REGULAR,
|
||||||
DWRITE_FONT_STYLE_NORMAL,
|
DWRITE_FONT_STYLE_NORMAL,
|
||||||
DWRITE_FONT_STRETCH_NORMAL,
|
DWRITE_FONT_STRETCH_NORMAL,
|
||||||
fontSize,
|
resolvedFontSize,
|
||||||
L"",
|
L"",
|
||||||
textFormat.ReleaseAndGetAddressOf());
|
textFormat.ReleaseAndGetAddressOf());
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#define NOMINMAX
|
#define NOMINMAX
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
||||||
|
|
||||||
#include <XCEngine/UI/DrawData.h>
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
#include <d2d1.h>
|
#include <d2d1.h>
|
||||||
@@ -20,13 +22,17 @@
|
|||||||
|
|
||||||
namespace XCEngine::UI::Editor::Host {
|
namespace XCEngine::UI::Editor::Host {
|
||||||
|
|
||||||
class NativeRenderer {
|
class NativeRenderer : public ::XCEngine::UI::Editor::UIEditorTextMeasurer {
|
||||||
public:
|
public:
|
||||||
bool Initialize(HWND hwnd);
|
bool Initialize(HWND hwnd);
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
void SetDpiScale(float dpiScale);
|
||||||
|
float GetDpiScale() const;
|
||||||
void Resize(UINT width, UINT height);
|
void Resize(UINT width, UINT height);
|
||||||
bool Render(const ::XCEngine::UI::UIDrawData& drawData);
|
bool Render(const ::XCEngine::UI::UIDrawData& drawData);
|
||||||
const std::string& GetLastRenderError() const;
|
const std::string& GetLastRenderError() const;
|
||||||
|
float MeasureTextWidth(
|
||||||
|
const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const override;
|
||||||
bool CaptureToPng(
|
bool CaptureToPng(
|
||||||
const ::XCEngine::UI::UIDrawData& drawData,
|
const ::XCEngine::UI::UIDrawData& drawData,
|
||||||
UINT width,
|
UINT width,
|
||||||
@@ -49,7 +55,7 @@ private:
|
|||||||
const ::XCEngine::UI::UIDrawCommand& command,
|
const ::XCEngine::UI::UIDrawCommand& command,
|
||||||
std::vector<D2D1_RECT_F>& clipStack);
|
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 D2D1_COLOR_F ToD2DColor(const ::XCEngine::UI::UIColor& color);
|
||||||
static std::wstring Utf8ToWide(std::string_view text);
|
static std::wstring Utf8ToWide(std::string_view text);
|
||||||
|
|
||||||
@@ -59,9 +65,10 @@ private:
|
|||||||
Microsoft::WRL::ComPtr<IWICImagingFactory> m_wicFactory;
|
Microsoft::WRL::ComPtr<IWICImagingFactory> m_wicFactory;
|
||||||
Microsoft::WRL::ComPtr<ID2D1HwndRenderTarget> m_renderTarget;
|
Microsoft::WRL::ComPtr<ID2D1HwndRenderTarget> m_renderTarget;
|
||||||
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> m_solidBrush;
|
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 = {};
|
std::string m_lastRenderError = {};
|
||||||
bool m_wicComInitialized = false;
|
bool m_wicComInitialized = false;
|
||||||
|
float m_dpiScale = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace XCEngine::UI::Editor::Host
|
} // namespace XCEngine::UI::Editor::Host
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "Shell/ProductShellAsset.h"
|
#include "Shell/ProductShellAsset.h"
|
||||||
|
|
||||||
|
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||||
|
|
||||||
#include <XCEngine/Input/InputTypes.h>
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
@@ -10,8 +11,11 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <shellscalingapi.h>
|
||||||
|
|
||||||
#ifndef XCUIEDITOR_REPO_ROOT
|
#ifndef XCUIEDITOR_REPO_ROOT
|
||||||
#define XCUIEDITOR_REPO_ROOT "."
|
#define XCUIEDITOR_REPO_ROOT "."
|
||||||
#endif
|
#endif
|
||||||
@@ -34,11 +38,112 @@ using App::BuildProductShellInteractionDefinition;
|
|||||||
|
|
||||||
constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost";
|
constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost";
|
||||||
constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor";
|
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) {
|
Application* GetApplicationFromWindow(HWND hwnd) {
|
||||||
return reinterpret_cast<Application*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
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) {
|
std::string TruncateText(const std::string& text, std::size_t maxLength) {
|
||||||
if (text.size() <= maxLength) {
|
if (text.size() <= maxLength) {
|
||||||
return text;
|
return text;
|
||||||
@@ -146,6 +251,44 @@ bool IsRepeatKeyMessage(LPARAM lParam) {
|
|||||||
return (static_cast<unsigned long>(lParam) & (1ul << 30)) != 0ul;
|
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
|
} // namespace
|
||||||
|
|
||||||
int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
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) {
|
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||||
m_hInstance = hInstance;
|
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_shellAsset = BuildProductShellAsset(ResolveRepoRootPath());
|
||||||
m_shellValidation = ValidateEditorShellAsset(m_shellAsset);
|
m_shellValidation = ValidateEditorShellAsset(m_shellAsset);
|
||||||
m_validationMessage = m_shellValidation.message;
|
m_validationMessage = m_shellValidation.message;
|
||||||
if (!m_shellValidation.IsValid()) {
|
if (!m_shellValidation.IsValid()) {
|
||||||
|
LogRuntimeTrace("app", "shell asset validation failed: " + m_validationMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +338,10 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
|||||||
m_shellServices = {};
|
m_shellServices = {};
|
||||||
m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher();
|
m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher();
|
||||||
m_shellServices.shortcutManager = &m_shortcutManager;
|
m_shellServices.shortcutManager = &m_shortcutManager;
|
||||||
|
m_shellServices.textMeasurer = &m_renderer;
|
||||||
m_lastStatus = "Ready";
|
m_lastStatus = "Ready";
|
||||||
m_lastMessage = "Old editor shell baseline loaded.";
|
m_lastMessage = "Old editor shell baseline loaded.";
|
||||||
|
LogRuntimeTrace("app", "workspace initialized: " + DescribeWorkspaceState());
|
||||||
|
|
||||||
WNDCLASSEXW windowClass = {};
|
WNDCLASSEXW windowClass = {};
|
||||||
windowClass.cbSize = sizeof(windowClass);
|
windowClass.cbSize = sizeof(windowClass);
|
||||||
@@ -200,6 +352,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
|||||||
windowClass.lpszClassName = kWindowClassName;
|
windowClass.lpszClassName = kWindowClassName;
|
||||||
m_windowClassAtom = RegisterClassExW(&windowClass);
|
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||||
if (m_windowClassAtom == 0) {
|
if (m_windowClassAtom == 0) {
|
||||||
|
LogRuntimeTrace("app", "window class registration failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +360,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
|||||||
0,
|
0,
|
||||||
kWindowClassName,
|
kWindowClassName,
|
||||||
kWindowTitle,
|
kWindowTitle,
|
||||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
WS_OVERLAPPEDWINDOW,
|
||||||
CW_USEDEFAULT,
|
CW_USEDEFAULT,
|
||||||
CW_USEDEFAULT,
|
CW_USEDEFAULT,
|
||||||
1540,
|
1540,
|
||||||
@@ -217,26 +370,37 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
|||||||
hInstance,
|
hInstance,
|
||||||
this);
|
this);
|
||||||
if (m_hwnd == nullptr) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowWindow(m_hwnd, nCmdShow);
|
ShowWindow(m_hwnd, nCmdShow);
|
||||||
UpdateWindow(m_hwnd);
|
UpdateWindow(m_hwnd);
|
||||||
|
|
||||||
if (!m_renderer.Initialize(m_hwnd)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_autoScreenshot.Initialize(m_shellAsset.captureRootPath);
|
m_autoScreenshot.Initialize(m_shellAsset.captureRootPath);
|
||||||
if (IsAutoCaptureOnStartupEnabled()) {
|
if (IsAutoCaptureOnStartupEnabled()) {
|
||||||
m_autoScreenshot.RequestCapture("startup");
|
m_autoScreenshot.RequestCapture("startup");
|
||||||
m_lastStatus = "Capture";
|
m_lastStatus = "Capture";
|
||||||
m_lastMessage = "Startup capture requested.";
|
m_lastMessage = "Startup capture requested.";
|
||||||
}
|
}
|
||||||
|
LogRuntimeTrace("app", "initialize completed");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::Shutdown() {
|
void Application::Shutdown() {
|
||||||
|
LogRuntimeTrace("app", "shutdown begin");
|
||||||
if (GetCapture() == m_hwnd) {
|
if (GetCapture() == m_hwnd) {
|
||||||
ReleaseCapture();
|
ReleaseCapture();
|
||||||
}
|
}
|
||||||
@@ -253,6 +417,9 @@ void Application::Shutdown() {
|
|||||||
UnregisterClassW(kWindowClassName, m_hInstance);
|
UnregisterClassW(kWindowClassName, m_hInstance);
|
||||||
m_windowClassAtom = 0;
|
m_windowClassAtom = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogRuntimeTrace("app", "shutdown end");
|
||||||
|
ShutdownUIEditorRuntimeTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::RenderFrame() {
|
void Application::RenderFrame() {
|
||||||
@@ -262,8 +429,12 @@ void Application::RenderFrame() {
|
|||||||
|
|
||||||
RECT clientRect = {};
|
RECT clientRect = {};
|
||||||
GetClientRect(m_hwnd, &clientRect);
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
|
const unsigned int pixelWidth =
|
||||||
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
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 = {};
|
UIDrawData drawData = {};
|
||||||
UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell");
|
UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell");
|
||||||
@@ -277,6 +448,11 @@ void Application::RenderFrame() {
|
|||||||
const UIEditorShellInteractionDefinition definition = BuildShellDefinition();
|
const UIEditorShellInteractionDefinition definition = BuildShellDefinition();
|
||||||
std::vector<UIInputEvent> frameEvents = std::move(m_pendingInputEvents);
|
std::vector<UIInputEvent> frameEvents = std::move(m_pendingInputEvents);
|
||||||
m_pendingInputEvents.clear();
|
m_pendingInputEvents.clear();
|
||||||
|
if (!frameEvents.empty()) {
|
||||||
|
LogRuntimeTrace(
|
||||||
|
"input",
|
||||||
|
DescribeInputEvents(frameEvents) + " | " + DescribeWorkspaceState());
|
||||||
|
}
|
||||||
|
|
||||||
m_shellFrame = UpdateUIEditorShellInteraction(
|
m_shellFrame = UpdateUIEditorShellInteraction(
|
||||||
m_shellInteractionState,
|
m_shellInteractionState,
|
||||||
@@ -286,6 +462,24 @@ void Application::RenderFrame() {
|
|||||||
frameEvents,
|
frameEvents,
|
||||||
m_shellServices,
|
m_shellServices,
|
||||||
metrics);
|
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);
|
ApplyHostCaptureRequests(m_shellFrame.result);
|
||||||
UpdateLastStatus(m_shellFrame.result);
|
UpdateLastStatus(m_shellFrame.result);
|
||||||
AppendUIEditorShellInteraction(
|
AppendUIEditorShellInteraction(
|
||||||
@@ -311,11 +505,78 @@ void Application::RenderFrame() {
|
|||||||
m_autoScreenshot.CaptureIfRequested(
|
m_autoScreenshot.CaptureIfRequested(
|
||||||
m_renderer,
|
m_renderer,
|
||||||
drawData,
|
drawData,
|
||||||
static_cast<unsigned int>(width),
|
pixelWidth,
|
||||||
static_cast<unsigned int>(height),
|
pixelHeight,
|
||||||
framePresented);
|
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) {
|
void Application::OnResize(UINT width, UINT height) {
|
||||||
if (width == 0 || height == 0) {
|
if (width == 0 || height == 0) {
|
||||||
return;
|
return;
|
||||||
@@ -324,6 +585,27 @@ void Application::OnResize(UINT width, UINT height) {
|
|||||||
m_renderer.Resize(width, 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) {
|
void Application::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) {
|
||||||
if (result.requestPointerCapture && GetCapture() != m_hwnd) {
|
if (result.requestPointerCapture && GetCapture() != m_hwnd) {
|
||||||
SetCapture(m_hwnd);
|
SetCapture(m_hwnd);
|
||||||
@@ -338,6 +620,10 @@ bool Application::HasInteractiveCaptureState() const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!m_shellInteractionState.workspaceInteractionState.dockHostInteractionState.activeTabDragNodeId.empty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& panelState : m_shellInteractionState.workspaceInteractionState.composeState.panelStates) {
|
for (const auto& panelState : m_shellInteractionState.workspaceInteractionState.composeState.panelStates) {
|
||||||
if (panelState.viewportShellState.inputBridgeState.captured) {
|
if (panelState.viewportShellState.inputBridgeState.captured) {
|
||||||
return true;
|
return true;
|
||||||
@@ -434,9 +720,9 @@ void Application::QueuePointerEvent(
|
|||||||
UIInputEvent event = {};
|
UIInputEvent event = {};
|
||||||
event.type = type;
|
event.type = type;
|
||||||
event.pointerButton = button;
|
event.pointerButton = button;
|
||||||
event.position = UIPoint(
|
event.position = ConvertClientPixelsToDips(
|
||||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
GET_X_LPARAM(lParam),
|
||||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
GET_Y_LPARAM(lParam));
|
||||||
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
||||||
m_pendingInputEvents.push_back(event);
|
m_pendingInputEvents.push_back(event);
|
||||||
}
|
}
|
||||||
@@ -448,9 +734,7 @@ void Application::QueuePointerLeaveEvent() {
|
|||||||
POINT clientPoint = {};
|
POINT clientPoint = {};
|
||||||
GetCursorPos(&clientPoint);
|
GetCursorPos(&clientPoint);
|
||||||
ScreenToClient(m_hwnd, &clientPoint);
|
ScreenToClient(m_hwnd, &clientPoint);
|
||||||
event.position = UIPoint(
|
event.position = ConvertClientPixelsToDips(clientPoint.x, clientPoint.y);
|
||||||
static_cast<float>(clientPoint.x),
|
|
||||||
static_cast<float>(clientPoint.y));
|
|
||||||
}
|
}
|
||||||
m_pendingInputEvents.push_back(event);
|
m_pendingInputEvents.push_back(event);
|
||||||
}
|
}
|
||||||
@@ -468,9 +752,7 @@ void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM
|
|||||||
|
|
||||||
UIInputEvent event = {};
|
UIInputEvent event = {};
|
||||||
event.type = UIInputEventType::PointerWheel;
|
event.type = UIInputEventType::PointerWheel;
|
||||||
event.position = UIPoint(
|
event.position = ConvertClientPixelsToDips(screenPoint.x, screenPoint.y);
|
||||||
static_cast<float>(screenPoint.x),
|
|
||||||
static_cast<float>(screenPoint.y));
|
|
||||||
event.wheelDelta = static_cast<float>(wheelDelta);
|
event.wheelDelta = static_cast<float>(wheelDelta);
|
||||||
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
||||||
m_pendingInputEvents.push_back(event);
|
m_pendingInputEvents.push_back(event);
|
||||||
@@ -507,6 +789,19 @@ std::filesystem::path Application::ResolveRepoRootPath() {
|
|||||||
return std::filesystem::path(root).lexically_normal();
|
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(
|
UIEditorHostCommandEvaluationResult Application::EvaluateHostCommand(
|
||||||
std::string_view commandId) const {
|
std::string_view commandId) const {
|
||||||
UIEditorHostCommandEvaluationResult result = {};
|
UIEditorHostCommandEvaluationResult result = {};
|
||||||
@@ -573,6 +868,7 @@ UIEditorHostCommandDispatchResult Application::DispatchHostCommand(
|
|||||||
|
|
||||||
LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||||
if (message == WM_NCCREATE) {
|
if (message == WM_NCCREATE) {
|
||||||
|
TryEnableNonClientDpiScaling(hwnd);
|
||||||
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||||
auto* application = reinterpret_cast<Application*>(createStruct->lpCreateParams);
|
auto* application = reinterpret_cast<Application*>(createStruct->lpCreateParams);
|
||||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(application));
|
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);
|
Application* application = GetApplicationFromWindow(hwnd);
|
||||||
switch (message) {
|
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:
|
case WM_SIZE:
|
||||||
if (application != nullptr && wParam != SIZE_MINIMIZED) {
|
if (application != nullptr && wParam != SIZE_MINIMIZED) {
|
||||||
application->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
application->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||||
|
|||||||
@@ -42,10 +42,18 @@ private:
|
|||||||
void Shutdown();
|
void Shutdown();
|
||||||
void RenderFrame();
|
void RenderFrame();
|
||||||
void OnResize(UINT width, UINT height);
|
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);
|
void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result);
|
||||||
bool HasInteractiveCaptureState() const;
|
bool HasInteractiveCaptureState() const;
|
||||||
UIEditorShellInteractionDefinition BuildShellDefinition() const;
|
UIEditorShellInteractionDefinition BuildShellDefinition() const;
|
||||||
void UpdateLastStatus(const UIEditorShellInteractionResult& result);
|
void UpdateLastStatus(const UIEditorShellInteractionResult& result);
|
||||||
|
std::string DescribeWorkspaceState() const;
|
||||||
|
std::string DescribeInputEvents(
|
||||||
|
const std::vector<::XCEngine::UI::UIInputEvent>& events) const;
|
||||||
void QueuePointerEvent(
|
void QueuePointerEvent(
|
||||||
::XCEngine::UI::UIInputEventType type,
|
::XCEngine::UI::UIInputEventType type,
|
||||||
::XCEngine::UI::UIPointerButton button,
|
::XCEngine::UI::UIPointerButton button,
|
||||||
@@ -57,6 +65,7 @@ private:
|
|||||||
void QueueCharacterEvent(WPARAM wParam, LPARAM lParam);
|
void QueueCharacterEvent(WPARAM wParam, LPARAM lParam);
|
||||||
void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type);
|
void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type);
|
||||||
static std::filesystem::path ResolveRepoRootPath();
|
static std::filesystem::path ResolveRepoRootPath();
|
||||||
|
static LONG WINAPI HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo);
|
||||||
|
|
||||||
HWND m_hwnd = nullptr;
|
HWND m_hwnd = nullptr;
|
||||||
HINSTANCE m_hInstance = nullptr;
|
HINSTANCE m_hInstance = nullptr;
|
||||||
@@ -76,6 +85,8 @@ private:
|
|||||||
std::string m_validationMessage = {};
|
std::string m_validationMessage = {};
|
||||||
std::string m_lastStatus = {};
|
std::string m_lastStatus = {};
|
||||||
std::string m_lastMessage = {};
|
std::string m_lastMessage = {};
|
||||||
|
UINT m_windowDpi = 96u;
|
||||||
|
float m_dpiScale = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow);
|
int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow);
|
||||||
|
|||||||
@@ -22,11 +22,21 @@ struct UIEditorTabStripItem {
|
|||||||
float desiredHeaderLabelWidth = 0.0f;
|
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 {
|
struct UIEditorTabStripState {
|
||||||
std::size_t selectedIndex = UIEditorTabStripInvalidIndex;
|
std::size_t selectedIndex = UIEditorTabStripInvalidIndex;
|
||||||
std::size_t hoveredIndex = UIEditorTabStripInvalidIndex;
|
std::size_t hoveredIndex = UIEditorTabStripInvalidIndex;
|
||||||
std::size_t closeHoveredIndex = UIEditorTabStripInvalidIndex;
|
std::size_t closeHoveredIndex = UIEditorTabStripInvalidIndex;
|
||||||
bool focused = false;
|
bool focused = false;
|
||||||
|
UIEditorTabStripReorderState reorder = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UIEditorTabStripMetrics {
|
struct UIEditorTabStripMetrics {
|
||||||
@@ -38,21 +48,26 @@ struct UIEditorTabStripMetrics {
|
|||||||
float closeInsetRight = 6.0f;
|
float closeInsetRight = 6.0f;
|
||||||
float closeInsetY = 0.0f;
|
float closeInsetY = 0.0f;
|
||||||
float labelInsetX = 8.0f;
|
float labelInsetX = 8.0f;
|
||||||
float labelInsetY = -2.5f;
|
float labelInsetY = -0.5f;
|
||||||
float baseBorderThickness = 1.0f;
|
float baseBorderThickness = 1.0f;
|
||||||
float selectedBorderThickness = 1.0f;
|
float selectedBorderThickness = 1.0f;
|
||||||
float focusedBorderThickness = 1.0f;
|
float focusedBorderThickness = 1.0f;
|
||||||
|
float reorderDragThreshold = 6.0f;
|
||||||
|
float reorderPreviewThickness = 2.0f;
|
||||||
|
float reorderPreviewInsetY = 3.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UIEditorTabStripPalette {
|
struct UIEditorTabStripPalette {
|
||||||
::XCEngine::UI::UIColor stripBackgroundColor =
|
::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 headerBackgroundColor =
|
||||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||||
::XCEngine::UI::UIColor contentBackgroundColor =
|
::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 stripBorderColor =
|
||||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
::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 focusedBorderColor =
|
||||||
::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f);
|
::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f);
|
||||||
::XCEngine::UI::UIColor tabColor =
|
::XCEngine::UI::UIColor tabColor =
|
||||||
@@ -60,7 +75,7 @@ struct UIEditorTabStripPalette {
|
|||||||
::XCEngine::UI::UIColor tabHoveredColor =
|
::XCEngine::UI::UIColor tabHoveredColor =
|
||||||
::XCEngine::UI::UIColor(0.23f, 0.23f, 0.23f, 1.0f);
|
::XCEngine::UI::UIColor(0.23f, 0.23f, 0.23f, 1.0f);
|
||||||
::XCEngine::UI::UIColor tabSelectedColor =
|
::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 tabBorderColor =
|
||||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
||||||
::XCEngine::UI::UIColor tabHoveredBorderColor =
|
::XCEngine::UI::UIColor tabHoveredBorderColor =
|
||||||
@@ -81,6 +96,14 @@ struct UIEditorTabStripPalette {
|
|||||||
::XCEngine::UI::UIColor(0.30f, 0.30f, 0.30f, 1.0f);
|
::XCEngine::UI::UIColor(0.30f, 0.30f, 0.30f, 1.0f);
|
||||||
::XCEngine::UI::UIColor closeGlyphColor =
|
::XCEngine::UI::UIColor closeGlyphColor =
|
||||||
::XCEngine::UI::UIColor(0.83f, 0.83f, 0.83f, 1.0f);
|
::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 {
|
struct UIEditorTabStripLayout {
|
||||||
@@ -91,6 +114,7 @@ struct UIEditorTabStripLayout {
|
|||||||
std::vector<::XCEngine::UI::UIRect> closeButtonRects = {};
|
std::vector<::XCEngine::UI::UIRect> closeButtonRects = {};
|
||||||
std::vector<bool> showCloseButtons = {};
|
std::vector<bool> showCloseButtons = {};
|
||||||
std::size_t selectedIndex = UIEditorTabStripInvalidIndex;
|
std::size_t selectedIndex = UIEditorTabStripInvalidIndex;
|
||||||
|
UIEditorTabStripInsertionPreview insertionPreview = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class UIEditorTabStripHitTargetKind : std::uint8_t {
|
enum class UIEditorTabStripHitTargetKind : std::uint8_t {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <XCEditor/Collections/UIEditorTabStrip.h>
|
#include <XCEditor/Collections/UIEditorTabStrip.h>
|
||||||
|
|
||||||
#include <XCEngine/UI/DrawData.h>
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
#include <XCEngine/UI/Widgets/UIDragDropInteraction.h>
|
||||||
#include <XCEngine/UI/Widgets/UITabStripModel.h>
|
#include <XCEngine/UI/Widgets/UITabStripModel.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -13,9 +14,13 @@ namespace XCEngine::UI::Editor {
|
|||||||
struct UIEditorTabStripInteractionState {
|
struct UIEditorTabStripInteractionState {
|
||||||
Widgets::UIEditorTabStripState tabStripState = {};
|
Widgets::UIEditorTabStripState tabStripState = {};
|
||||||
::XCEngine::UI::Widgets::UITabStripModel navigationModel = {};
|
::XCEngine::UI::Widgets::UITabStripModel navigationModel = {};
|
||||||
|
::XCEngine::UI::Widgets::UIDragDropState reorderDragState = {};
|
||||||
Widgets::UIEditorTabStripHitTarget pressedTarget = {};
|
Widgets::UIEditorTabStripHitTarget pressedTarget = {};
|
||||||
::XCEngine::UI::UIPoint pointerPosition = {};
|
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||||
|
std::size_t reorderSourceIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||||
|
std::size_t reorderPreviewIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||||
bool hasPointerPosition = false;
|
bool hasPointerPosition = false;
|
||||||
|
bool reorderCaptureActive = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UIEditorTabStripInteractionResult {
|
struct UIEditorTabStripInteractionResult {
|
||||||
@@ -23,11 +28,23 @@ struct UIEditorTabStripInteractionResult {
|
|||||||
bool selectionChanged = false;
|
bool selectionChanged = false;
|
||||||
bool closeRequested = false;
|
bool closeRequested = false;
|
||||||
bool keyboardNavigated = 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 = {};
|
Widgets::UIEditorTabStripHitTarget hitTarget = {};
|
||||||
std::string selectedTabId = {};
|
std::string selectedTabId = {};
|
||||||
std::size_t selectedIndex = Widgets::UIEditorTabStripInvalidIndex;
|
std::size_t selectedIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||||
std::string closedTabId = {};
|
std::string closedTabId = {};
|
||||||
std::size_t closedIndex = Widgets::UIEditorTabStripInvalidIndex;
|
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 {
|
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 = {};
|
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 {
|
struct UIEditorDockHostState {
|
||||||
bool focused = false;
|
bool focused = false;
|
||||||
UIEditorDockHostHitTarget hoveredTarget = {};
|
UIEditorDockHostHitTarget hoveredTarget = {};
|
||||||
std::string activeSplitterNodeId = {};
|
std::string activeSplitterNodeId = {};
|
||||||
std::vector<UIEditorDockHostTabStripVisualState> tabStripStates = {};
|
std::vector<UIEditorDockHostTabStripVisualState> tabStripStates = {};
|
||||||
|
UIEditorDockHostDropPreviewState dropPreview = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UIEditorDockHostMetrics {
|
struct UIEditorDockHostMetrics {
|
||||||
::XCEngine::UI::Layout::UISplitterMetrics splitterMetrics =
|
::XCEngine::UI::Layout::UISplitterMetrics splitterMetrics =
|
||||||
::XCEngine::UI::Layout::UISplitterMetrics{ 4.0f, 12.0f };
|
::XCEngine::UI::Layout::UISplitterMetrics{ 1.0f, 10.0f };
|
||||||
UIEditorTabStripMetrics tabStripMetrics = {};
|
UIEditorTabStripMetrics tabStripMetrics = {};
|
||||||
UIEditorPanelFrameMetrics panelFrameMetrics = {};
|
UIEditorPanelFrameMetrics panelFrameMetrics = {};
|
||||||
::XCEngine::UI::UISize minimumStandalonePanelBodySize =
|
::XCEngine::UI::UISize minimumStandalonePanelBodySize =
|
||||||
@@ -75,6 +86,10 @@ struct UIEditorDockHostPalette {
|
|||||||
::XCEngine::UI::UIColor(0.70f, 0.72f, 0.74f, 1.0f);
|
::XCEngine::UI::UIColor(0.70f, 0.72f, 0.74f, 1.0f);
|
||||||
::XCEngine::UI::UIColor placeholderMutedColor =
|
::XCEngine::UI::UIColor placeholderMutedColor =
|
||||||
::XCEngine::UI::UIColor(0.58f, 0.59f, 0.62f, 1.0f);
|
::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 {
|
struct UIEditorDockHostTabItemLayout {
|
||||||
@@ -107,10 +122,20 @@ struct UIEditorDockHostTabStackLayout {
|
|||||||
UIEditorPanelFrameLayout contentFrameLayout = {};
|
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 {
|
struct UIEditorDockHostLayout {
|
||||||
::XCEngine::UI::UIRect bounds = {};
|
::XCEngine::UI::UIRect bounds = {};
|
||||||
std::vector<UIEditorDockHostSplitterLayout> splitters = {};
|
std::vector<UIEditorDockHostSplitterLayout> splitters = {};
|
||||||
std::vector<UIEditorDockHostTabStackLayout> tabStacks = {};
|
std::vector<UIEditorDockHostTabStackLayout> tabStacks = {};
|
||||||
|
UIEditorDockHostDropPreviewLayout dropPreview = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Allows higher-level compose to own panel body presentation while DockHost
|
// Allows higher-level compose to own panel body presentation while DockHost
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ struct UIEditorDockHostInteractionState {
|
|||||||
Widgets::UIEditorDockHostState dockHostState = {};
|
Widgets::UIEditorDockHostState dockHostState = {};
|
||||||
::XCEngine::UI::Widgets::UISplitterDragState splitterDragState = {};
|
::XCEngine::UI::Widgets::UISplitterDragState splitterDragState = {};
|
||||||
std::vector<UIEditorDockHostTabStripInteractionEntry> tabStripInteractions = {};
|
std::vector<UIEditorDockHostTabStripInteractionEntry> tabStripInteractions = {};
|
||||||
|
std::string activeTabDragNodeId = {};
|
||||||
|
std::string activeTabDragPanelId = {};
|
||||||
::XCEngine::UI::UIPoint pointerPosition = {};
|
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||||
bool hasPointerPosition = false;
|
bool hasPointerPosition = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ struct UIEditorMenuBarMetrics {
|
|||||||
float barHeight = 24.0f;
|
float barHeight = 24.0f;
|
||||||
float horizontalInset = 0.0f;
|
float horizontalInset = 0.0f;
|
||||||
float verticalInset = 2.0f;
|
float verticalInset = 2.0f;
|
||||||
float buttonGap = 2.0f;
|
float buttonGap = 1.0f;
|
||||||
float buttonPaddingX = 10.0f;
|
float buttonPaddingX = 4.0f;
|
||||||
|
float labelFontSize = 13.0f;
|
||||||
float estimatedGlyphWidth = 6.5f;
|
float estimatedGlyphWidth = 6.5f;
|
||||||
float labelInsetY = -1.5f;
|
float labelInsetY = -1.5f;
|
||||||
float barCornerRounding = 0.0f;
|
float barCornerRounding = 0.0f;
|
||||||
float buttonCornerRounding = 0.0f;
|
float buttonCornerRounding = 0.75f;
|
||||||
float baseBorderThickness = 1.0f;
|
float baseBorderThickness = 1.0f;
|
||||||
float focusedBorderThickness = 1.0f;
|
float focusedBorderThickness = 1.0f;
|
||||||
float openBorderThickness = 1.0f;
|
float openBorderThickness = 1.0f;
|
||||||
@@ -42,23 +43,23 @@ struct UIEditorMenuBarMetrics {
|
|||||||
|
|
||||||
struct UIEditorMenuBarPalette {
|
struct UIEditorMenuBarPalette {
|
||||||
::XCEngine::UI::UIColor barColor =
|
::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 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 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 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 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 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 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 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 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 textDisabled =
|
||||||
::XCEngine::UI::UIColor(0.50f, 0.50f, 0.50f, 1.0f);
|
::XCEngine::UI::UIColor(0.50f, 0.50f, 0.50f, 1.0f);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,19 +34,19 @@ struct UIEditorMenuPopupState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct UIEditorMenuPopupMetrics {
|
struct UIEditorMenuPopupMetrics {
|
||||||
float contentPaddingX = 6.0f;
|
float contentPaddingX = 2.0f;
|
||||||
float contentPaddingY = 6.0f;
|
float contentPaddingY = 6.0f;
|
||||||
float itemHeight = 28.0f;
|
float itemHeight = 28.0f;
|
||||||
float separatorHeight = 9.0f;
|
float separatorHeight = 9.0f;
|
||||||
float checkColumnWidth = 18.0f;
|
float checkColumnWidth = 12.0f;
|
||||||
float shortcutGap = 20.0f;
|
float shortcutGap = 14.0f;
|
||||||
float submenuIndicatorWidth = 14.0f;
|
float submenuIndicatorWidth = 10.0f;
|
||||||
float rowCornerRounding = 5.0f;
|
float rowCornerRounding = 2.5f;
|
||||||
float popupCornerRounding = 8.0f;
|
float popupCornerRounding = 4.0f;
|
||||||
float labelInsetX = 14.0f;
|
float labelInsetX = 4.0f;
|
||||||
float labelInsetY = -1.0f;
|
float labelInsetY = -1.0f;
|
||||||
float labelFontSize = 13.0f;
|
float labelFontSize = 13.0f;
|
||||||
float shortcutInsetRight = 24.0f;
|
float shortcutInsetRight = 8.0f;
|
||||||
float estimatedGlyphWidth = 7.0f;
|
float estimatedGlyphWidth = 7.0f;
|
||||||
float glyphFontSize = 12.0f;
|
float glyphFontSize = 12.0f;
|
||||||
float separatorThickness = 1.0f;
|
float separatorThickness = 1.0f;
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ struct UIEditorShellComposeFrame {
|
|||||||
UIEditorWorkspaceComposeFrame workspaceFrame = {};
|
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(
|
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||||
const ::XCEngine::UI::UIRect& bounds,
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
||||||
#include <XCEditor/Shell/UIEditorMenuModel.h>
|
#include <XCEditor/Shell/UIEditorMenuModel.h>
|
||||||
#include <XCEditor/Shell/UIEditorMenuSession.h>
|
#include <XCEditor/Shell/UIEditorMenuSession.h>
|
||||||
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
||||||
@@ -50,6 +51,7 @@ struct UIEditorShellInteractionPalette {
|
|||||||
struct UIEditorShellInteractionServices {
|
struct UIEditorShellInteractionServices {
|
||||||
const UIEditorCommandDispatcher* commandDispatcher = nullptr;
|
const UIEditorCommandDispatcher* commandDispatcher = nullptr;
|
||||||
const UIEditorShortcutManager* shortcutManager = nullptr;
|
const UIEditorShortcutManager* shortcutManager = nullptr;
|
||||||
|
const UIEditorTextMeasurer* textMeasurer = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UIEditorShellInteractionMenuButtonRequest {
|
struct UIEditorShellInteractionMenuButtonRequest {
|
||||||
|
|||||||
@@ -105,6 +105,21 @@ public:
|
|||||||
UIEditorWorkspaceLayoutOperationResult SetSplitRatio(
|
UIEditorWorkspaceLayoutOperationResult SetSplitRatio(
|
||||||
std::string_view nodeId,
|
std::string_view nodeId,
|
||||||
float splitRatio);
|
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);
|
UIEditorWorkspaceCommandResult Dispatch(const UIEditorWorkspaceCommand& command);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
namespace XCEngine::UI::Editor {
|
namespace XCEngine::UI::Editor {
|
||||||
|
|
||||||
|
struct UIEditorWorkspaceSession;
|
||||||
|
|
||||||
enum class UIEditorWorkspaceNodeKind : std::uint8_t {
|
enum class UIEditorWorkspaceNodeKind : std::uint8_t {
|
||||||
Panel = 0,
|
Panel = 0,
|
||||||
TabStack,
|
TabStack,
|
||||||
@@ -19,6 +21,14 @@ enum class UIEditorWorkspaceSplitAxis : std::uint8_t {
|
|||||||
Vertical
|
Vertical
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class UIEditorWorkspaceDockPlacement : std::uint8_t {
|
||||||
|
Center = 0,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Top,
|
||||||
|
Bottom
|
||||||
|
};
|
||||||
|
|
||||||
struct UIEditorWorkspacePanelState {
|
struct UIEditorWorkspacePanelState {
|
||||||
std::string panelId = {};
|
std::string panelId = {};
|
||||||
std::string title = {};
|
std::string title = {};
|
||||||
@@ -133,4 +143,28 @@ bool TrySetUIEditorWorkspaceSplitRatio(
|
|||||||
std::string_view nodeId,
|
std::string_view nodeId,
|
||||||
float splitRatio);
|
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
|
} // namespace XCEngine::UI::Editor
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ using ::XCEngine::UI::Layout::MeasureUITabStripHeaderWidth;
|
|||||||
|
|
||||||
constexpr float kTabRounding = 0.0f;
|
constexpr float kTabRounding = 0.0f;
|
||||||
constexpr float kStripRounding = 0.0f;
|
constexpr float kStripRounding = 0.0f;
|
||||||
constexpr float kHeaderFontSize = 11.0f;
|
constexpr float kHeaderFontSize = 13.0f;
|
||||||
constexpr float kCloseFontSize = 10.0f;
|
|
||||||
|
|
||||||
float ClampNonNegative(float value) {
|
float ClampNonNegative(float value) {
|
||||||
return (std::max)(value, 0.0f);
|
return (std::max)(value, 0.0f);
|
||||||
@@ -44,10 +43,6 @@ float ResolveTabRounding(const UIEditorTabStripMetrics& metrics) {
|
|||||||
return kTabRounding;
|
return kTabRounding;
|
||||||
}
|
}
|
||||||
|
|
||||||
float ResolveCloseButtonRounding(const UIEditorTabStripMetrics& metrics) {
|
|
||||||
return (std::min)(ClampNonNegative(metrics.closeButtonExtent) * 0.2f, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t ResolveSelectedIndex(
|
std::size_t ResolveSelectedIndex(
|
||||||
std::size_t itemCount,
|
std::size_t itemCount,
|
||||||
std::size_t selectedIndex) {
|
std::size_t selectedIndex) {
|
||||||
@@ -78,20 +73,18 @@ float ResolveTabTextTop(
|
|||||||
return rect.y + (std::max)(0.0f, (rect.height - kHeaderFontSize) * 0.5f) + metrics.labelInsetY;
|
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(
|
UIColor ResolveStripBorderColor(
|
||||||
const UIEditorTabStripState& state,
|
const UIEditorTabStripState& state,
|
||||||
const UIEditorTabStripPalette& palette) {
|
const UIEditorTabStripPalette& palette) {
|
||||||
return state.focused ? palette.focusedBorderColor : palette.stripBorderColor;
|
(void)state;
|
||||||
|
return palette.stripBorderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
float ResolveStripBorderThickness(
|
float ResolveStripBorderThickness(
|
||||||
const UIEditorTabStripState& state,
|
const UIEditorTabStripState& state,
|
||||||
const UIEditorTabStripMetrics& metrics) {
|
const UIEditorTabStripMetrics& metrics) {
|
||||||
return state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness;
|
(void)state;
|
||||||
|
return metrics.baseBorderThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
UIColor ResolveTabFillColor(
|
UIColor ResolveTabFillColor(
|
||||||
@@ -115,7 +108,8 @@ UIColor ResolveTabBorderColor(
|
|||||||
bool focused,
|
bool focused,
|
||||||
const UIEditorTabStripPalette& palette) {
|
const UIEditorTabStripPalette& palette) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
return focused ? palette.focusedBorderColor : palette.tabSelectedBorderColor;
|
(void)focused;
|
||||||
|
return palette.tabSelectedBorderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
@@ -125,33 +119,121 @@ UIColor ResolveTabBorderColor(
|
|||||||
return palette.tabBorderColor;
|
return palette.tabBorderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
float ResolveTabBorderThickness(
|
float ResolveTabBorderThickness(bool selected, bool focused, const UIEditorTabStripMetrics& metrics) {
|
||||||
bool selected,
|
|
||||||
bool focused,
|
|
||||||
const UIEditorTabStripMetrics& metrics) {
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
return focused ? metrics.focusedBorderThickness : metrics.selectedBorderThickness;
|
(void)focused;
|
||||||
|
return metrics.selectedBorderThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
return metrics.baseBorderThickness;
|
return metrics.baseBorderThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
UIRect BuildCloseButtonRect(
|
void AppendHeaderContentSeparator(
|
||||||
const UIRect& headerRect,
|
UIDrawList& drawList,
|
||||||
|
const UIEditorTabStripLayout& layout,
|
||||||
|
const UIEditorTabStripPalette& palette,
|
||||||
const UIEditorTabStripMetrics& metrics) {
|
const UIEditorTabStripMetrics& metrics) {
|
||||||
const float insetY = ClampNonNegative(metrics.closeInsetY);
|
if (layout.headerRect.width <= 0.0f ||
|
||||||
const float extent = (std::min)(
|
layout.headerRect.height <= 0.0f ||
|
||||||
ClampNonNegative(metrics.closeButtonExtent),
|
layout.contentRect.height <= 0.0f) {
|
||||||
(std::max)(headerRect.height - insetY * 2.0f, 0.0f));
|
return;
|
||||||
if (extent <= 0.0f) {
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return UIRect(
|
const float thickness = (std::max)(ClampNonNegative(metrics.baseBorderThickness), 1.0f);
|
||||||
headerRect.x + headerRect.width - ClampNonNegative(metrics.closeInsetRight) - extent,
|
const float separatorY = layout.contentRect.y;
|
||||||
headerRect.y + insetY + (std::max)(0.0f, headerRect.height - insetY * 2.0f - extent) * 0.5f,
|
const float separatorLeft = layout.headerRect.x;
|
||||||
extent,
|
const float separatorRight = layout.headerRect.x + layout.headerRect.width;
|
||||||
extent);
|
|
||||||
|
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
|
} // namespace
|
||||||
@@ -162,13 +244,8 @@ float ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
|||||||
const float labelWidth = ResolveEstimatedLabelWidth(item, metrics);
|
const float labelWidth = ResolveEstimatedLabelWidth(item, metrics);
|
||||||
const float horizontalPadding = ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
const float horizontalPadding = ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||||
const float extraLeftInset = (std::max)(ClampNonNegative(metrics.labelInsetX) - horizontalPadding, 0.0f);
|
const float extraLeftInset = (std::max)(ClampNonNegative(metrics.labelInsetX) - horizontalPadding, 0.0f);
|
||||||
const float extraRightInset = (std::max)(ClampNonNegative(metrics.closeInsetRight) - horizontalPadding, 0.0f);
|
(void)item;
|
||||||
const float closeBudget = item.closable
|
return labelWidth + extraLeftInset;
|
||||||
? ClampNonNegative(metrics.closeButtonExtent) +
|
|
||||||
ClampNonNegative(metrics.closeButtonGap) +
|
|
||||||
extraRightInset
|
|
||||||
: 0.0f;
|
|
||||||
return labelWidth + extraLeftInset + closeBudget;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t ResolveUIEditorTabStripSelectedIndex(
|
std::size_t ResolveUIEditorTabStripSelectedIndex(
|
||||||
@@ -188,34 +265,6 @@ std::size_t ResolveUIEditorTabStripSelectedIndex(
|
|||||||
return ResolveSelectedIndex(items.size(), fallbackIndex);
|
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(
|
UIEditorTabStripLayout BuildUIEditorTabStripLayout(
|
||||||
const UIRect& bounds,
|
const UIRect& bounds,
|
||||||
const std::vector<UIEditorTabStripItem>& items,
|
const std::vector<UIEditorTabStripItem>& items,
|
||||||
@@ -245,17 +294,7 @@ UIEditorTabStripLayout BuildUIEditorTabStripLayout(
|
|||||||
layout.tabHeaderRects = arranged.tabHeaderRects;
|
layout.tabHeaderRects = arranged.tabHeaderRects;
|
||||||
layout.closeButtonRects.resize(items.size());
|
layout.closeButtonRects.resize(items.size());
|
||||||
layout.showCloseButtons.resize(items.size(), false);
|
layout.showCloseButtons.resize(items.size(), false);
|
||||||
|
layout.insertionPreview = BuildInsertionPreview(layout, state, metrics);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,15 +308,6 @@ UIEditorTabStripHitTarget HitTestUIEditorTabStrip(
|
|||||||
return target;
|
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) {
|
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
||||||
if (IsPointInsideRect(layout.tabHeaderRects[index], point)) {
|
if (IsPointInsideRect(layout.tabHeaderRects[index], point)) {
|
||||||
target.kind = UIEditorTabStripHitTargetKind::Tab;
|
target.kind = UIEditorTabStripHitTargetKind::Tab;
|
||||||
@@ -322,7 +352,7 @@ void AppendUIEditorTabStripBackground(
|
|||||||
|
|
||||||
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
||||||
const bool selected = layout.selectedIndex == index;
|
const bool selected = layout.selectedIndex == index;
|
||||||
const bool hovered = state.hoveredIndex == index || state.closeHoveredIndex == index;
|
const bool hovered = state.hoveredIndex == index;
|
||||||
drawList.AddFilledRect(
|
drawList.AddFilledRect(
|
||||||
layout.tabHeaderRects[index],
|
layout.tabHeaderRects[index],
|
||||||
ResolveTabFillColor(selected, hovered, palette),
|
ResolveTabFillColor(selected, hovered, palette),
|
||||||
@@ -332,6 +362,20 @@ void AppendUIEditorTabStripBackground(
|
|||||||
ResolveTabBorderColor(selected, hovered, state.focused, palette),
|
ResolveTabBorderColor(selected, hovered, state.focused, palette),
|
||||||
ResolveTabBorderThickness(selected, state.focused, metrics),
|
ResolveTabBorderThickness(selected, state.focused, metrics),
|
||||||
tabRounding);
|
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 UIEditorTabStripState& state,
|
||||||
const UIEditorTabStripPalette& palette,
|
const UIEditorTabStripPalette& palette,
|
||||||
const UIEditorTabStripMetrics& metrics) {
|
const UIEditorTabStripMetrics& metrics) {
|
||||||
|
AppendHeaderContentSeparator(drawList, layout, palette, metrics);
|
||||||
|
|
||||||
const float leftInset = (std::max)(
|
const float leftInset = (std::max)(
|
||||||
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
|
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
|
||||||
ClampNonNegative(metrics.labelInsetX));
|
ClampNonNegative(metrics.labelInsetX));
|
||||||
@@ -349,12 +395,9 @@ void AppendUIEditorTabStripForeground(
|
|||||||
for (std::size_t index = 0; index < items.size() && index < layout.tabHeaderRects.size(); ++index) {
|
for (std::size_t index = 0; index < items.size() && index < layout.tabHeaderRects.size(); ++index) {
|
||||||
const UIRect& tabRect = layout.tabHeaderRects[index];
|
const UIRect& tabRect = layout.tabHeaderRects[index];
|
||||||
const bool selected = layout.selectedIndex == 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;
|
const float textLeft = tabRect.x + leftInset;
|
||||||
float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
const float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||||
if (layout.showCloseButtons[index]) {
|
|
||||||
textRight = layout.closeButtonRects[index].x - ClampNonNegative(metrics.closeButtonGap);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textRight > textLeft) {
|
if (textRight > textLeft) {
|
||||||
const UIRect clipRect(
|
const UIRect clipRect(
|
||||||
@@ -370,30 +413,6 @@ void AppendUIEditorTabStripForeground(
|
|||||||
kHeaderFontSize);
|
kHeaderFontSize);
|
||||||
drawList.PopClipRect();
|
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 <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace XCEngine::UI::Editor {
|
namespace XCEngine::UI::Editor {
|
||||||
@@ -12,6 +13,16 @@ using ::XCEngine::Input::KeyCode;
|
|||||||
using ::XCEngine::UI::UIInputEvent;
|
using ::XCEngine::UI::UIInputEvent;
|
||||||
using ::XCEngine::UI::UIInputEventType;
|
using ::XCEngine::UI::UIInputEventType;
|
||||||
using ::XCEngine::UI::UIPointerButton;
|
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::BuildUIEditorTabStripLayout;
|
||||||
using Widgets::HitTestUIEditorTabStrip;
|
using Widgets::HitTestUIEditorTabStrip;
|
||||||
using Widgets::ResolveUIEditorTabStripSelectedIndex;
|
using Widgets::ResolveUIEditorTabStripSelectedIndex;
|
||||||
@@ -19,6 +30,9 @@ using Widgets::UIEditorTabStripHitTarget;
|
|||||||
using Widgets::UIEditorTabStripHitTargetKind;
|
using Widgets::UIEditorTabStripHitTargetKind;
|
||||||
using Widgets::UIEditorTabStripInvalidIndex;
|
using Widgets::UIEditorTabStripInvalidIndex;
|
||||||
|
|
||||||
|
constexpr ::XCEngine::UI::UIElementId kTabStripDragOwnerId = 0x58435441u;
|
||||||
|
constexpr std::string_view kTabStripDragPayloadType = "xc.editor.tab";
|
||||||
|
|
||||||
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case UIInputEventType::PointerMove:
|
case UIInputEventType::PointerMove:
|
||||||
@@ -53,6 +67,14 @@ void ClearHoverState(UIEditorTabStripInteractionState& state) {
|
|||||||
state.tabStripState.closeHoveredIndex = UIEditorTabStripInvalidIndex;
|
state.tabStripState.closeHoveredIndex = UIEditorTabStripInvalidIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearReorderState(UIEditorTabStripInteractionState& state) {
|
||||||
|
state.tabStripState.reorder = {};
|
||||||
|
state.reorderDragState = {};
|
||||||
|
state.reorderSourceIndex = UIEditorTabStripInvalidIndex;
|
||||||
|
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||||
|
state.reorderCaptureActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
void SyncHoverTarget(
|
void SyncHoverTarget(
|
||||||
UIEditorTabStripInteractionState& state,
|
UIEditorTabStripInteractionState& state,
|
||||||
const Widgets::UIEditorTabStripLayout& layout) {
|
const Widgets::UIEditorTabStripLayout& layout) {
|
||||||
@@ -68,11 +90,6 @@ void SyncHoverTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (hitTarget.kind) {
|
switch (hitTarget.kind) {
|
||||||
case UIEditorTabStripHitTargetKind::CloseButton:
|
|
||||||
state.tabStripState.hoveredIndex = hitTarget.index;
|
|
||||||
state.tabStripState.closeHoveredIndex = hitTarget.index;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UIEditorTabStripHitTargetKind::Tab:
|
case UIEditorTabStripHitTargetKind::Tab:
|
||||||
state.tabStripState.hoveredIndex = hitTarget.index;
|
state.tabStripState.hoveredIndex = hitTarget.index;
|
||||||
break;
|
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
|
} // namespace
|
||||||
|
|
||||||
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||||
@@ -184,11 +400,33 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
|||||||
state.hasPointerPosition = false;
|
state.hasPointerPosition = false;
|
||||||
state.pressedTarget = {};
|
state.pressedTarget = {};
|
||||||
ClearHoverState(state);
|
ClearHoverState(state);
|
||||||
|
CancelTabReorder(state, eventResult, items);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case UIInputEventType::PointerMove:
|
case UIInputEventType::PointerMove:
|
||||||
case UIInputEventType::PointerEnter:
|
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:
|
case UIInputEventType::PointerLeave:
|
||||||
|
if (state.reorderDragState.active) {
|
||||||
|
SyncReorderPreview(state, layout, items, eventResult);
|
||||||
|
eventResult.consumed = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case UIInputEventType::PointerButtonDown: {
|
case UIInputEventType::PointerButtonDown: {
|
||||||
@@ -201,6 +439,9 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
|||||||
(state.hasPointerPosition && IsPointInside(layout.bounds, state.pointerPosition))) {
|
(state.hasPointerPosition && IsPointInside(layout.bounds, state.pointerPosition))) {
|
||||||
state.tabStripState.focused = true;
|
state.tabStripState.focused = true;
|
||||||
eventResult.consumed = true;
|
eventResult.consumed = true;
|
||||||
|
if (eventResult.hitTarget.kind == UIEditorTabStripHitTargetKind::Tab) {
|
||||||
|
BeginTabReorder(state, items, eventResult.hitTarget.index);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state.tabStripState.focused = false;
|
state.tabStripState.focused = false;
|
||||||
state.pressedTarget = {};
|
state.pressedTarget = {};
|
||||||
@@ -218,22 +459,49 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
|||||||
const bool matchedPressedTarget =
|
const bool matchedPressedTarget =
|
||||||
AreEquivalentTargets(state.pressedTarget, eventResult.hitTarget);
|
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) {
|
if (matchedPressedTarget) {
|
||||||
switch (eventResult.hitTarget.kind) {
|
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:
|
case UIEditorTabStripHitTargetKind::Tab:
|
||||||
SelectTab(
|
SelectTab(
|
||||||
state,
|
state,
|
||||||
@@ -271,6 +539,12 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case UIInputEventType::KeyDown:
|
case UIInputEventType::KeyDown:
|
||||||
|
if (HasReorderInteraction(state) &&
|
||||||
|
static_cast<KeyCode>(event.keyCode) == KeyCode::Escape) {
|
||||||
|
CancelTabReorder(state, eventResult, items);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.tabStripState.focused &&
|
if (state.tabStripState.focused &&
|
||||||
!HasNavigationModifiers(event.modifiers) &&
|
!HasNavigationModifiers(event.modifiers) &&
|
||||||
ApplyKeyboardNavigation(state, event.keyCode) &&
|
ApplyKeyboardNavigation(state, event.keyCode) &&
|
||||||
@@ -299,13 +573,25 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
|||||||
HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition);
|
HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.reorderDragState.active) {
|
||||||
|
SyncReorderPreview(state, layout, items, eventResult);
|
||||||
|
}
|
||||||
|
|
||||||
if (eventResult.consumed ||
|
if (eventResult.consumed ||
|
||||||
eventResult.selectionChanged ||
|
eventResult.selectionChanged ||
|
||||||
eventResult.closeRequested ||
|
eventResult.closeRequested ||
|
||||||
eventResult.keyboardNavigated ||
|
eventResult.keyboardNavigated ||
|
||||||
|
eventResult.requestPointerCapture ||
|
||||||
|
eventResult.releasePointerCapture ||
|
||||||
|
eventResult.dragStarted ||
|
||||||
|
eventResult.dragEnded ||
|
||||||
|
eventResult.dragCanceled ||
|
||||||
|
eventResult.reorderRequested ||
|
||||||
|
eventResult.reorderPreviewActive ||
|
||||||
eventResult.hitTarget.kind != UIEditorTabStripHitTargetKind::None ||
|
eventResult.hitTarget.kind != UIEditorTabStripHitTargetKind::None ||
|
||||||
!eventResult.selectedTabId.empty() ||
|
!eventResult.selectedTabId.empty() ||
|
||||||
!eventResult.closedTabId.empty()) {
|
!eventResult.closedTabId.empty() ||
|
||||||
|
!eventResult.draggedTabId.empty()) {
|
||||||
interactionResult = std::move(eventResult);
|
interactionResult = std::move(eventResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,6 +604,9 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
|||||||
interactionResult.hitTarget =
|
interactionResult.hitTarget =
|
||||||
HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition);
|
HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition);
|
||||||
}
|
}
|
||||||
|
if (state.reorderDragState.active) {
|
||||||
|
SyncReorderPreview(state, layout, items, interactionResult);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
std::move(layout),
|
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;
|
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
|
} // namespace
|
||||||
|
|
||||||
const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
|
const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
|
||||||
@@ -539,6 +624,7 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
|
|||||||
state,
|
state,
|
||||||
metrics,
|
metrics,
|
||||||
layout);
|
layout);
|
||||||
|
layout.dropPreview = ResolveDropPreviewLayout(layout, state);
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,8 +693,6 @@ void AppendUIEditorDockHostBackground(
|
|||||||
const UIEditorDockHostLayout& layout,
|
const UIEditorDockHostLayout& layout,
|
||||||
const UIEditorDockHostPalette& palette,
|
const UIEditorDockHostPalette& palette,
|
||||||
const UIEditorDockHostMetrics& metrics) {
|
const UIEditorDockHostMetrics& metrics) {
|
||||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
|
||||||
BuildTabContentFrameMetrics(metrics);
|
|
||||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||||
tabItems.reserve(tabStack.items.size());
|
tabItems.reserve(tabStack.items.size());
|
||||||
@@ -626,12 +710,6 @@ void AppendUIEditorDockHostBackground(
|
|||||||
tabStack.tabStripState,
|
tabStack.tabStripState,
|
||||||
palette.tabStripPalette,
|
palette.tabStripPalette,
|
||||||
metrics.tabStripMetrics);
|
metrics.tabStripMetrics);
|
||||||
AppendUIEditorPanelFrameBackground(
|
|
||||||
drawList,
|
|
||||||
tabStack.contentFrameLayout,
|
|
||||||
tabStack.contentFrameState,
|
|
||||||
palette.panelFramePalette,
|
|
||||||
tabContentFrameMetrics);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
|
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
|
||||||
@@ -648,8 +726,6 @@ void AppendUIEditorDockHostForeground(
|
|||||||
const UIEditorDockHostForegroundOptions& options,
|
const UIEditorDockHostForegroundOptions& options,
|
||||||
const UIEditorDockHostPalette& palette,
|
const UIEditorDockHostPalette& palette,
|
||||||
const UIEditorDockHostMetrics& metrics) {
|
const UIEditorDockHostMetrics& metrics) {
|
||||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
|
||||||
BuildTabContentFrameMetrics(metrics);
|
|
||||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||||
tabItems.reserve(tabStack.items.size());
|
tabItems.reserve(tabStack.items.size());
|
||||||
@@ -668,18 +744,21 @@ void AppendUIEditorDockHostForeground(
|
|||||||
tabStack.tabStripState,
|
tabStack.tabStripState,
|
||||||
palette.tabStripPalette,
|
palette.tabStripPalette,
|
||||||
metrics.tabStripMetrics);
|
metrics.tabStripMetrics);
|
||||||
AppendUIEditorPanelFrameForeground(
|
|
||||||
drawList,
|
|
||||||
tabStack.contentFrameLayout,
|
|
||||||
tabStack.contentFrameState,
|
|
||||||
{},
|
|
||||||
palette.panelFramePalette,
|
|
||||||
tabContentFrameMetrics);
|
|
||||||
|
|
||||||
if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) {
|
if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (layout.dropPreview.visible) {
|
||||||
|
drawList.AddFilledRect(
|
||||||
|
layout.dropPreview.previewRect,
|
||||||
|
palette.dropPreviewFillColor);
|
||||||
|
drawList.AddRectOutline(
|
||||||
|
layout.dropPreview.previewRect,
|
||||||
|
palette.dropPreviewBorderColor,
|
||||||
|
1.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppendUIEditorDockHostForeground(
|
void AppendUIEditorDockHostForeground(
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
|
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
|
||||||
|
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||||
|
|
||||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -13,6 +15,7 @@ namespace {
|
|||||||
|
|
||||||
using ::XCEngine::UI::UIInputEvent;
|
using ::XCEngine::UI::UIInputEvent;
|
||||||
using ::XCEngine::UI::UIInputEventType;
|
using ::XCEngine::UI::UIInputEventType;
|
||||||
|
using ::XCEngine::UI::UIPoint;
|
||||||
using ::XCEngine::UI::UIRect;
|
using ::XCEngine::UI::UIRect;
|
||||||
using ::XCEngine::UI::Widgets::BeginUISplitterDrag;
|
using ::XCEngine::UI::Widgets::BeginUISplitterDrag;
|
||||||
using ::XCEngine::UI::Widgets::EndUISplitterDrag;
|
using ::XCEngine::UI::Widgets::EndUISplitterDrag;
|
||||||
@@ -30,8 +33,17 @@ using Widgets::UIEditorTabStripItem;
|
|||||||
struct DockHostTabStripEventResult {
|
struct DockHostTabStripEventResult {
|
||||||
bool consumed = false;
|
bool consumed = false;
|
||||||
bool commandRequested = 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;
|
UIEditorWorkspaceCommandKind commandKind = UIEditorWorkspaceCommandKind::ActivatePanel;
|
||||||
|
std::size_t dropInsertionIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||||
std::string panelId = {};
|
std::string panelId = {};
|
||||||
|
std::string nodeId = {};
|
||||||
|
std::string draggedTabId = {};
|
||||||
UIEditorDockHostHitTarget hitTarget = {};
|
UIEditorDockHostHitTarget hitTarget = {};
|
||||||
int priority = 0;
|
int priority = 0;
|
||||||
};
|
};
|
||||||
@@ -130,6 +142,13 @@ void PruneTabStripInteractionEntries(
|
|||||||
return !isVisibleNodeId(entry.nodeId);
|
return !isVisibleNodeId(entry.nodeId);
|
||||||
}),
|
}),
|
||||||
state.dockHostState.tabStripStates.end());
|
state.dockHostState.tabStripStates.end());
|
||||||
|
|
||||||
|
if (!state.activeTabDragNodeId.empty() &&
|
||||||
|
!isVisibleNodeId(state.activeTabDragNodeId)) {
|
||||||
|
state.activeTabDragNodeId.clear();
|
||||||
|
state.activeTabDragPanelId.clear();
|
||||||
|
state.dockHostState.dropPreview = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state) {
|
void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state) {
|
||||||
@@ -152,6 +171,33 @@ bool HasFocusedTabStrip(const UIEditorDockHostInteractionState& state) {
|
|||||||
}) != state.tabStripInteractions.end();
|
}) != 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(
|
std::vector<UIEditorTabStripItem> BuildTabStripItems(
|
||||||
const UIEditorDockHostTabStackLayout& tabStack) {
|
const UIEditorDockHostTabStackLayout& tabStack) {
|
||||||
std::vector<UIEditorTabStripItem> items = {};
|
std::vector<UIEditorTabStripItem> items = {};
|
||||||
@@ -198,6 +244,13 @@ UIEditorDockHostHitTarget MapTabStripHitTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ResolveTabStripPriority(const UIEditorTabStripInteractionResult& result) {
|
int ResolveTabStripPriority(const UIEditorTabStripInteractionResult& result) {
|
||||||
|
if (result.reorderRequested ||
|
||||||
|
result.dragStarted ||
|
||||||
|
result.dragEnded ||
|
||||||
|
result.dragCanceled) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.closeRequested) {
|
if (result.closeRequested) {
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
@@ -221,6 +274,11 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
|||||||
DockHostTabStripEventResult resolved = {};
|
DockHostTabStripEventResult resolved = {};
|
||||||
|
|
||||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||||
|
if (!state.activeTabDragNodeId.empty() &&
|
||||||
|
tabStack.nodeId != state.activeTabDragNodeId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
UIEditorDockHostTabStripInteractionEntry& entry =
|
UIEditorDockHostTabStripInteractionEntry& entry =
|
||||||
FindOrCreateTabStripInteractionEntry(state, tabStack.nodeId);
|
FindOrCreateTabStripInteractionEntry(state, tabStack.nodeId);
|
||||||
std::string selectedTabId = tabStack.selectedPanelId;
|
std::string selectedTabId = tabStack.selectedPanelId;
|
||||||
@@ -238,7 +296,15 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolved.nodeId = tabStack.nodeId;
|
||||||
resolved.hitTarget = MapTabStripHitTarget(tabStack, frame.result);
|
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()) ||
|
if ((frame.result.closeRequested && !frame.result.closedTabId.empty()) ||
|
||||||
(event.type == UIInputEventType::PointerButtonUp &&
|
(event.type == UIInputEventType::PointerButtonUp &&
|
||||||
frame.result.consumed &&
|
frame.result.consumed &&
|
||||||
@@ -250,6 +316,12 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
|||||||
!frame.result.closedTabId.empty()
|
!frame.result.closedTabId.empty()
|
||||||
? frame.result.closedTabId
|
? frame.result.closedTabId
|
||||||
: resolved.hitTarget.panelId;
|
: 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 ||
|
} else if ((frame.result.selectionChanged ||
|
||||||
frame.result.keyboardNavigated ||
|
frame.result.keyboardNavigated ||
|
||||||
(event.type == UIInputEventType::PointerButtonUp &&
|
(event.type == UIInputEventType::PointerButtonUp &&
|
||||||
@@ -266,6 +338,7 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
|||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
resolved.commandRequested = false;
|
resolved.commandRequested = false;
|
||||||
|
resolved.reorderRequested = false;
|
||||||
resolved.panelId.clear();
|
resolved.panelId.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +350,103 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
|||||||
return resolved;
|
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(
|
void SyncHoverTarget(
|
||||||
UIEditorDockHostInteractionState& state,
|
UIEditorDockHostInteractionState& state,
|
||||||
const Widgets::UIEditorDockHostLayout& layout) {
|
const Widgets::UIEditorDockHostLayout& layout) {
|
||||||
@@ -290,6 +460,11 @@ void SyncHoverTarget(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.activeTabDragNodeId.empty()) {
|
||||||
|
state.dockHostState.hoveredTarget = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.hasPointerPosition) {
|
if (!state.hasPointerPosition) {
|
||||||
state.dockHostState.hoveredTarget = {};
|
state.dockHostState.hoveredTarget = {};
|
||||||
return;
|
return;
|
||||||
@@ -340,6 +515,20 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
||||||
? ProcessTabStripEvent(state, layout, event, metrics)
|
? ProcessTabStripEvent(state, layout, event, metrics)
|
||||||
: DockHostTabStripEventResult {};
|
: 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) {
|
switch (event.type) {
|
||||||
case UIInputEventType::FocusGained:
|
case UIInputEventType::FocusGained:
|
||||||
@@ -355,6 +544,12 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
eventResult.consumed = true;
|
eventResult.consumed = true;
|
||||||
eventResult.releasePointerCapture = true;
|
eventResult.releasePointerCapture = true;
|
||||||
}
|
}
|
||||||
|
if (!state.activeTabDragNodeId.empty() || tabStripResult.dragCanceled) {
|
||||||
|
ClearTabDockDragState(state);
|
||||||
|
eventResult.consumed = true;
|
||||||
|
eventResult.releasePointerCapture =
|
||||||
|
eventResult.releasePointerCapture || tabStripResult.releasePointerCapture;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case UIInputEventType::PointerMove:
|
case UIInputEventType::PointerMove:
|
||||||
@@ -381,6 +576,21 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
eventResult.hitTarget.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
|
eventResult.hitTarget.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
|
||||||
eventResult.hitTarget.nodeId = state.dockHostState.activeSplitterNodeId;
|
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;
|
break;
|
||||||
|
|
||||||
@@ -388,6 +598,7 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
if (!state.splitterDragState.active) {
|
if (!state.splitterDragState.active) {
|
||||||
state.dockHostState.hoveredTarget = {};
|
state.dockHostState.hoveredTarget = {};
|
||||||
}
|
}
|
||||||
|
state.dockHostState.dropPreview = {};
|
||||||
if (!HasFocusedTabStrip(state)) {
|
if (!HasFocusedTabStrip(state)) {
|
||||||
state.dockHostState.focused = false;
|
state.dockHostState.focused = false;
|
||||||
}
|
}
|
||||||
@@ -464,6 +675,96 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
break;
|
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()) {
|
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||||
eventResult.commandResult = DispatchPanelCommand(
|
eventResult.commandResult = DispatchPanelCommand(
|
||||||
controller,
|
controller,
|
||||||
@@ -534,6 +835,14 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case UIInputEventType::KeyDown:
|
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()) {
|
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||||
eventResult.commandResult = DispatchPanelCommand(
|
eventResult.commandResult = DispatchPanelCommand(
|
||||||
controller,
|
controller,
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ using ::XCEngine::UI::UIDrawList;
|
|||||||
using ::XCEngine::UI::UIPoint;
|
using ::XCEngine::UI::UIPoint;
|
||||||
using ::XCEngine::UI::UIRect;
|
using ::XCEngine::UI::UIRect;
|
||||||
|
|
||||||
constexpr float kMenuBarFontSize = 13.0f;
|
|
||||||
|
|
||||||
float ClampNonNegative(float value) {
|
float ClampNonNegative(float value) {
|
||||||
return (std::max)(value, 0.0f);
|
return (std::max)(value, 0.0f);
|
||||||
}
|
}
|
||||||
@@ -24,6 +22,10 @@ bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) {
|
|||||||
point.y <= rect.y + rect.height;
|
point.y <= rect.y + rect.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsVisibleColor(const UIColor& color) {
|
||||||
|
return color.a > 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
float ResolveEstimatedLabelWidth(
|
float ResolveEstimatedLabelWidth(
|
||||||
const UIEditorMenuBarItem& item,
|
const UIEditorMenuBarItem& item,
|
||||||
const UIEditorMenuBarMetrics& metrics) {
|
const UIEditorMenuBarMetrics& metrics) {
|
||||||
@@ -35,7 +37,9 @@ float ResolveEstimatedLabelWidth(
|
|||||||
}
|
}
|
||||||
|
|
||||||
float ResolveLabelTop(const UIRect& rect, const UIEditorMenuBarMetrics& metrics) {
|
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(
|
bool IsButtonFocused(
|
||||||
@@ -164,27 +168,40 @@ void AppendUIEditorMenuBarBackground(
|
|||||||
const UIEditorMenuBarPalette& palette,
|
const UIEditorMenuBarPalette& palette,
|
||||||
const UIEditorMenuBarMetrics& metrics) {
|
const UIEditorMenuBarMetrics& metrics) {
|
||||||
drawList.AddFilledRect(layout.bounds, palette.barColor, metrics.barCornerRounding);
|
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(
|
drawList.AddRectOutline(
|
||||||
layout.bounds,
|
layout.bounds,
|
||||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
barBorderColor,
|
||||||
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness,
|
barBorderThickness,
|
||||||
metrics.barCornerRounding);
|
metrics.barCornerRounding);
|
||||||
|
}
|
||||||
|
|
||||||
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
|
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
|
||||||
const bool open = state.openIndex == index;
|
const bool open = state.openIndex == index;
|
||||||
const bool hovered = state.hoveredIndex == index;
|
const bool hovered = state.hoveredIndex == index;
|
||||||
const bool focused = IsButtonFocused(state, 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(
|
drawList.AddFilledRect(
|
||||||
layout.buttonRects[index],
|
layout.buttonRects[index],
|
||||||
ResolveButtonFillColor(open, hovered, palette),
|
buttonFillColor,
|
||||||
metrics.buttonCornerRounding);
|
metrics.buttonCornerRounding);
|
||||||
|
}
|
||||||
|
if (IsVisibleColor(buttonBorderColor) && buttonBorderThickness > 0.0f) {
|
||||||
drawList.AddRectOutline(
|
drawList.AddRectOutline(
|
||||||
layout.buttonRects[index],
|
layout.buttonRects[index],
|
||||||
ResolveButtonBorderColor(open, focused, palette),
|
buttonBorderColor,
|
||||||
ResolveButtonBorderThickness(open, focused, metrics),
|
buttonBorderThickness,
|
||||||
metrics.buttonCornerRounding);
|
metrics.buttonCornerRounding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AppendUIEditorMenuBarForeground(
|
void AppendUIEditorMenuBarForeground(
|
||||||
UIDrawList& drawList,
|
UIDrawList& drawList,
|
||||||
@@ -206,7 +223,7 @@ void AppendUIEditorMenuBarForeground(
|
|||||||
UIPoint(textLeft, ResolveLabelTop(rect, metrics)),
|
UIPoint(textLeft, ResolveLabelTop(rect, metrics)),
|
||||||
items[index].label,
|
items[index].label,
|
||||||
items[index].enabled ? palette.textPrimary : palette.textDisabled,
|
items[index].enabled ? palette.textPrimary : palette.textDisabled,
|
||||||
kMenuBarFontSize);
|
ClampNonNegative(metrics.labelFontSize));
|
||||||
drawList.PopClipRect();
|
drawList.PopClipRect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,8 +171,14 @@ void AppendUIEditorMenuPopupBackground(
|
|||||||
const UIRect& rect = layout.itemRects[index];
|
const UIRect& rect = layout.itemRects[index];
|
||||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||||
const float lineY = rect.y + rect.height * 0.5f;
|
const float lineY = rect.y + rect.height * 0.5f;
|
||||||
|
const float separatorInset =
|
||||||
|
ClampNonNegative(metrics.contentPaddingX) + 3.0f;
|
||||||
drawList.AddFilledRect(
|
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);
|
palette.separatorColor);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,19 @@ void AppendUIEditorShellToolbar(
|
|||||||
|
|
||||||
} // namespace
|
} // 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(
|
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||||
const UIRect& bounds,
|
const UIRect& bounds,
|
||||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||||
|
|||||||
@@ -148,7 +148,9 @@ const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
||||||
const UIEditorResolvedMenuModel& model) {
|
const UIEditorResolvedMenuModel& model,
|
||||||
|
const UIEditorShellInteractionServices& services,
|
||||||
|
const Widgets::UIEditorMenuBarMetrics& metrics) {
|
||||||
std::vector<Widgets::UIEditorMenuBarItem> items = {};
|
std::vector<Widgets::UIEditorMenuBarItem> items = {};
|
||||||
items.reserve(model.menus.size());
|
items.reserve(model.menus.size());
|
||||||
|
|
||||||
@@ -157,6 +159,10 @@ std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
|||||||
item.menuId = menu.menuId;
|
item.menuId = menu.menuId;
|
||||||
item.label = menu.label;
|
item.label = menu.label;
|
||||||
item.enabled = !menu.items.empty();
|
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));
|
items.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +181,9 @@ UIEditorShellComposeModel BuildShellComposeModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
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 = {};
|
std::vector<Widgets::UIEditorMenuPopupItem> widgetItems = {};
|
||||||
widgetItems.reserve(items.size());
|
widgetItems.reserve(items.size());
|
||||||
|
|
||||||
@@ -188,6 +196,16 @@ std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
|||||||
widgetItem.enabled = item.enabled;
|
widgetItem.enabled = item.enabled;
|
||||||
widgetItem.checked = item.checked;
|
widgetItem.checked = item.checked;
|
||||||
widgetItem.hasSubmenu = item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty();
|
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));
|
widgetItems.push_back(std::move(widgetItem));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,10 +262,14 @@ BuildRequestOutput BuildRequest(
|
|||||||
const UIEditorWorkspaceController& controller,
|
const UIEditorWorkspaceController& controller,
|
||||||
const UIEditorShellInteractionModel& model,
|
const UIEditorShellInteractionModel& model,
|
||||||
const UIEditorShellInteractionState& state,
|
const UIEditorShellInteractionState& state,
|
||||||
const UIEditorShellInteractionMetrics& metrics) {
|
const UIEditorShellInteractionMetrics& metrics,
|
||||||
|
const UIEditorShellInteractionServices& services) {
|
||||||
BuildRequestOutput output = {};
|
BuildRequestOutput output = {};
|
||||||
UIEditorShellInteractionRequest& request = output.request;
|
UIEditorShellInteractionRequest& request = output.request;
|
||||||
request.menuBarItems = BuildMenuBarItems(model.resolvedMenuModel);
|
request.menuBarItems = BuildMenuBarItems(
|
||||||
|
model.resolvedMenuModel,
|
||||||
|
services,
|
||||||
|
metrics.shellMetrics.menuBarMetrics);
|
||||||
|
|
||||||
const UIEditorShellComposeModel shellModel =
|
const UIEditorShellComposeModel shellModel =
|
||||||
BuildShellComposeModel(model, request.menuBarItems);
|
BuildShellComposeModel(model, request.menuBarItems);
|
||||||
@@ -293,7 +315,10 @@ BuildRequestOutput BuildRequest(
|
|||||||
popupRequest.sourceItemId = popupState.itemId;
|
popupRequest.sourceItemId = popupState.itemId;
|
||||||
popupRequest.overlayEntry = *overlayEntry;
|
popupRequest.overlayEntry = *overlayEntry;
|
||||||
popupRequest.resolvedItems = *resolvedItems;
|
popupRequest.resolvedItems = *resolvedItems;
|
||||||
popupRequest.widgetItems = BuildPopupWidgetItems(popupRequest.resolvedItems);
|
popupRequest.widgetItems = BuildPopupWidgetItems(
|
||||||
|
popupRequest.resolvedItems,
|
||||||
|
services,
|
||||||
|
metrics.popupMetrics);
|
||||||
|
|
||||||
const float popupWidth =
|
const float popupWidth =
|
||||||
ResolveUIEditorMenuPopupDesiredWidth(popupRequest.widgetItems, metrics.popupMetrics);
|
ResolveUIEditorMenuPopupDesiredWidth(popupRequest.widgetItems, metrics.popupMetrics);
|
||||||
@@ -506,7 +531,8 @@ UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest(
|
|||||||
controller,
|
controller,
|
||||||
model,
|
model,
|
||||||
state,
|
state,
|
||||||
metrics).request;
|
metrics,
|
||||||
|
services).request;
|
||||||
}
|
}
|
||||||
|
|
||||||
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||||
@@ -525,7 +551,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
|||||||
controller,
|
controller,
|
||||||
model,
|
model,
|
||||||
state,
|
state,
|
||||||
metrics);
|
metrics,
|
||||||
|
services);
|
||||||
UIEditorShellInteractionRequest request = std::move(requestBuild.request);
|
UIEditorShellInteractionRequest request = std::move(requestBuild.request);
|
||||||
|
|
||||||
if (requestBuild.hadInvalidPopupState && state.menuSession.HasOpenMenu()) {
|
if (requestBuild.hadInvalidPopupState && state.menuSession.HasOpenMenu()) {
|
||||||
@@ -539,7 +566,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
|||||||
controller,
|
controller,
|
||||||
model,
|
model,
|
||||||
state,
|
state,
|
||||||
metrics);
|
metrics,
|
||||||
|
services);
|
||||||
request = std::move(requestBuild.request);
|
request = std::move(requestBuild.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,7 +719,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
|||||||
controller,
|
controller,
|
||||||
model,
|
model,
|
||||||
state,
|
state,
|
||||||
metrics).request;
|
metrics,
|
||||||
|
services).request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,7 +743,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
|||||||
controller,
|
controller,
|
||||||
model,
|
model,
|
||||||
state,
|
state,
|
||||||
metrics).request;
|
metrics,
|
||||||
|
services).request;
|
||||||
|
|
||||||
const RequestHit finalHit =
|
const RequestHit finalHit =
|
||||||
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
|
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
|
||||||
|
|||||||
@@ -110,21 +110,13 @@ UIColor ResolveSurfaceBorderColor(
|
|||||||
return palette.surfaceCapturedBorderColor;
|
return palette.surfaceCapturedBorderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.surfaceActive) {
|
|
||||||
return palette.surfaceActiveBorderColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.surfaceHovered) {
|
|
||||||
return palette.surfaceHoveredBorderColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return palette.surfaceBorderColor;
|
return palette.surfaceBorderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
float ResolveSurfaceBorderThickness(
|
float ResolveSurfaceBorderThickness(
|
||||||
const UIEditorViewportSlotState& state,
|
const UIEditorViewportSlotState& state,
|
||||||
const UIEditorViewportSlotMetrics& metrics) {
|
const UIEditorViewportSlotMetrics& metrics) {
|
||||||
if (state.inputCaptured || state.focused) {
|
if (state.inputCaptured) {
|
||||||
return metrics.focusedSurfaceBorderThickness;
|
return metrics.focusedSurfaceBorderThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,8 +348,8 @@ void AppendUIEditorViewportSlotBackground(
|
|||||||
drawList.AddFilledRect(layout.bounds, palette.frameColor, metrics.cornerRounding);
|
drawList.AddFilledRect(layout.bounds, palette.frameColor, metrics.cornerRounding);
|
||||||
drawList.AddRectOutline(
|
drawList.AddRectOutline(
|
||||||
layout.bounds,
|
layout.bounds,
|
||||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
palette.borderColor,
|
||||||
state.focused ? metrics.focusedBorderThickness : metrics.outerBorderThickness,
|
metrics.outerBorderThickness,
|
||||||
metrics.cornerRounding);
|
metrics.cornerRounding);
|
||||||
|
|
||||||
if (layout.hasTopBar) {
|
if (layout.hasTopBar) {
|
||||||
@@ -366,12 +358,6 @@ void AppendUIEditorViewportSlotBackground(
|
|||||||
|
|
||||||
drawList.AddFilledRect(layout.surfaceRect, palette.surfaceColor);
|
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) {
|
if (state.inputCaptured) {
|
||||||
drawList.AddFilledRect(layout.inputRect, palette.captureOverlayColor);
|
drawList.AddFilledRect(layout.inputRect, palette.captureOverlayColor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||||
|
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace XCEngine::UI::Editor {
|
namespace XCEngine::UI::Editor {
|
||||||
|
|
||||||
namespace {
|
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(
|
std::vector<std::string> CollectVisiblePanelIds(
|
||||||
const UIEditorWorkspaceModel& workspace,
|
const UIEditorWorkspaceModel& workspace,
|
||||||
const UIEditorWorkspaceSession& session) {
|
const UIEditorWorkspaceSession& session) {
|
||||||
@@ -22,6 +32,58 @@ std::vector<std::string> CollectVisiblePanelIds(
|
|||||||
return ids;
|
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
|
} // namespace
|
||||||
|
|
||||||
std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind) {
|
std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind) {
|
||||||
@@ -297,6 +359,290 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRati
|
|||||||
"Split ratio updated.");
|
"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(
|
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
|
||||||
const UIEditorWorkspaceCommand& command) {
|
const UIEditorWorkspaceCommand& command) {
|
||||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
||||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||||
|
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
@@ -43,6 +44,19 @@ UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelN
|
|||||||
return tabStack;
|
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(
|
void CanonicalizeNodeRecursive(
|
||||||
UIEditorWorkspaceNode& node,
|
UIEditorWorkspaceNode& node,
|
||||||
bool allowStandalonePanelLeaf) {
|
bool allowStandalonePanelLeaf) {
|
||||||
@@ -67,7 +81,7 @@ void CanonicalizeNodeRecursive(
|
|||||||
|
|
||||||
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
||||||
node.children.size() == 1u) {
|
node.children.size() == 1u) {
|
||||||
node = std::move(node.children.front());
|
CollapseSplitNodeToOnlyChild(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +145,256 @@ UIEditorWorkspaceNode* FindMutableNodeRecursive(
|
|||||||
return nullptr;
|
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(
|
bool TryActivateRecursive(
|
||||||
UIEditorWorkspaceNode& node,
|
UIEditorWorkspaceNode& node,
|
||||||
std::string_view panelId) {
|
std::string_view panelId) {
|
||||||
@@ -484,4 +748,266 @@ bool TrySetUIEditorWorkspaceSplitRatio(
|
|||||||
return true;
|
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
|
} // namespace XCEngine::UI::Editor
|
||||||
|
|||||||
@@ -148,6 +148,11 @@ if(TARGET editor_ui_scroll_view_basic_validation)
|
|||||||
editor_ui_scroll_view_basic_validation)
|
editor_ui_scroll_view_basic_validation)
|
||||||
endif()
|
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
|
add_custom_target(editor_ui_integration_tests
|
||||||
DEPENDS
|
DEPENDS
|
||||||
${EDITOR_UI_INTEGRATION_TARGETS}
|
${EDITOR_UI_INTEGRATION_TARGETS}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ endif()
|
|||||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/dock_host_basic/CMakeLists.txt")
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/dock_host_basic/CMakeLists.txt")
|
||||||
add_subdirectory(dock_host_basic)
|
add_subdirectory(dock_host_basic)
|
||||||
endif()
|
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")
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/panel_content_host_basic/CMakeLists.txt")
|
||||||
add_subdirectory(panel_content_host_basic)
|
add_subdirectory(panel_content_host_basic)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ private:
|
|||||||
if (action == ActionId::Reset) {
|
if (action == ActionId::Reset) {
|
||||||
ResetScenario();
|
ResetScenario();
|
||||||
m_lastStatus = "Ready";
|
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;
|
m_lastColor = kWarning;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -586,11 +586,11 @@ private:
|
|||||||
|
|
||||||
DrawCard(drawList, m_introRect, "这个测试验证什么功能?", "只验证 DockHost 基础交互 contract,不做 editor 业务面板。");
|
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 + 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 + 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 + 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 + 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, "操作", "这里只保留当前场景必要按钮。");
|
DrawCard(drawList, m_controlsRect, "操作", "这里只保留当前场景必要按钮。");
|
||||||
for (const ButtonState& button : m_buttons) {
|
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: " + items[target.index].title;
|
||||||
}
|
}
|
||||||
return "Tab";
|
return "Tab";
|
||||||
case UIEditorTabStripHitTargetKind::CloseButton:
|
|
||||||
if (target.index < items.size()) {
|
|
||||||
return "CloseButton: " + items[target.index].title;
|
|
||||||
}
|
|
||||||
return "CloseButton";
|
|
||||||
case UIEditorTabStripHitTargetKind::None:
|
case UIEditorTabStripHitTargetKind::None:
|
||||||
default:
|
default:
|
||||||
return "None";
|
return "None";
|
||||||
@@ -218,9 +213,6 @@ std::string JoinTabTitles(const std::vector<UIEditorTabStripItem>& items) {
|
|||||||
stream << " | ";
|
stream << " | ";
|
||||||
}
|
}
|
||||||
stream << items[index].title;
|
stream << items[index].title;
|
||||||
if (!items[index].closable) {
|
|
||||||
stream << " (locked)";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
@@ -502,15 +494,6 @@ private:
|
|||||||
void ApplyInteractionResult(
|
void ApplyInteractionResult(
|
||||||
const UIEditorTabStripInteractionResult& result,
|
const UIEditorTabStripInteractionResult& result,
|
||||||
std::string_view source) {
|
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) &&
|
if ((result.selectionChanged || result.keyboardNavigated) &&
|
||||||
!result.selectedTabId.empty()) {
|
!result.selectedTabId.empty()) {
|
||||||
DispatchCommand(
|
DispatchCommand(
|
||||||
@@ -609,7 +592,7 @@ private:
|
|||||||
drawList,
|
drawList,
|
||||||
introRect,
|
introRect,
|
||||||
"这个测试验证什么功能?",
|
"这个测试验证什么功能?",
|
||||||
"验证 TabStrip 的 header 命中、选中切换、关闭请求和键盘导航,不接业务面板。");
|
"验证 TabStrip 的 header 命中、选中切换和键盘导航,不接业务面板。");
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
UIPoint(introRect.x + 16.0f, introRect.y + 68.0f),
|
UIPoint(introRect.x + 16.0f, introRect.y + 68.0f),
|
||||||
"1. 点击 tab,检查 selected / active panel 是否同步。",
|
"1. 点击 tab,检查 selected / active panel 是否同步。",
|
||||||
@@ -617,7 +600,7 @@ private:
|
|||||||
12.0f);
|
12.0f);
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
UIPoint(introRect.x + 16.0f, introRect.y + 92.0f),
|
UIPoint(introRect.x + 16.0f, introRect.y + 92.0f),
|
||||||
"2. 点击 X,只验证关闭请求;Document C 没有关闭按钮。",
|
"2. 所有 tab 都没有关闭按钮;这里只验证命中、focus 和选中同步。",
|
||||||
kTextMuted,
|
kTextMuted,
|
||||||
12.0f);
|
12.0f);
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
@@ -719,7 +702,7 @@ private:
|
|||||||
12.0f);
|
12.0f);
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
UIPoint(m_layout.contentRect.x + 20.0f, m_layout.contentRect.y + 100.0f),
|
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,
|
kTextWeak,
|
||||||
12.0f);
|
12.0f);
|
||||||
|
|
||||||
|
|||||||
@@ -606,7 +606,7 @@ private:
|
|||||||
12.0f);
|
12.0f);
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 92.0f),
|
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 92.0f),
|
||||||
"2. tab 激活/关闭、panel 激活/关闭都必须统一回到 controller。",
|
"2. tab 激活、panel 激活/关闭都必须统一回到 controller。",
|
||||||
kTextPrimary,
|
kTextPrimary,
|
||||||
12.0f);
|
12.0f);
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ TEST(UIEditorDockHostTest, LayoutComposesOnlyUnifiedTabStacksFromWorkspaceTree)
|
|||||||
|
|
||||||
const auto* rootSplitter = FindUIEditorDockHostSplitterLayout(layout, "root-split");
|
const auto* rootSplitter = FindUIEditorDockHostSplitterLayout(layout, "root-split");
|
||||||
ASSERT_NE(rootSplitter, nullptr);
|
ASSERT_NE(rootSplitter, nullptr);
|
||||||
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.x, 395.0f);
|
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.x, 399.5f);
|
||||||
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.width, 10.0f);
|
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.width, 1.0f);
|
||||||
|
|
||||||
const auto& tabStack = layout.tabStacks.front();
|
const auto& tabStack = layout.tabStacks.front();
|
||||||
EXPECT_EQ(tabStack.nodeId, "document-tabs");
|
EXPECT_EQ(tabStack.nodeId, "document-tabs");
|
||||||
@@ -138,7 +138,7 @@ TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds)
|
|||||||
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.height, 480.0f);
|
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.height, 480.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabCloseThenPanelBody) {
|
TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabThenPanelBody) {
|
||||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||||
const UIEditorWorkspaceSession session =
|
const UIEditorWorkspaceSession session =
|
||||||
@@ -160,14 +160,14 @@ TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabCloseThenPanelBody)
|
|||||||
EXPECT_EQ(splitterHit.nodeId, "root-split");
|
EXPECT_EQ(splitterHit.nodeId, "root-split");
|
||||||
|
|
||||||
ASSERT_EQ(layout.tabStacks.size(), 3u);
|
ASSERT_EQ(layout.tabStacks.size(), 3u);
|
||||||
const auto& closeRect = layout.tabStacks.front().tabStripLayout.closeButtonRects[1];
|
const auto& tabRect = layout.tabStacks.front().tabStripLayout.tabHeaderRects[1];
|
||||||
const auto tabCloseHit = HitTestUIEditorDockHost(
|
const auto tabHit = HitTestUIEditorDockHost(
|
||||||
layout,
|
layout,
|
||||||
UIPoint(closeRect.x + closeRect.width * 0.5f, closeRect.y + closeRect.height * 0.5f));
|
UIPoint(tabRect.x + tabRect.width - 6.0f, tabRect.y + tabRect.height * 0.5f));
|
||||||
EXPECT_EQ(tabCloseHit.kind, UIEditorDockHostHitTargetKind::TabCloseButton);
|
EXPECT_EQ(tabHit.kind, UIEditorDockHostHitTargetKind::Tab);
|
||||||
EXPECT_EQ(tabCloseHit.nodeId, "document-tabs");
|
EXPECT_EQ(tabHit.nodeId, "document-tabs");
|
||||||
EXPECT_EQ(tabCloseHit.panelId, "doc-b");
|
EXPECT_EQ(tabHit.panelId, "doc-b");
|
||||||
EXPECT_EQ(tabCloseHit.index, 1u);
|
EXPECT_EQ(tabHit.index, 1u);
|
||||||
|
|
||||||
const auto panelBodyHit = HitTestUIEditorDockHost(
|
const auto panelBodyHit = HitTestUIEditorDockHost(
|
||||||
layout,
|
layout,
|
||||||
@@ -186,7 +186,7 @@ TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) {
|
|||||||
UIEditorDockHostState state = {};
|
UIEditorDockHostState state = {};
|
||||||
state.focused = true;
|
state.focused = true;
|
||||||
state.hoveredTarget = UIEditorDockHostHitTarget{
|
state.hoveredTarget = UIEditorDockHostHitTarget{
|
||||||
UIEditorDockHostHitTargetKind::TabCloseButton,
|
UIEditorDockHostHitTargetKind::Tab,
|
||||||
"document-tabs",
|
"document-tabs",
|
||||||
"doc-b",
|
"doc-b",
|
||||||
1u
|
1u
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using XCEngine::UI::UIInputEvent;
|
using XCEngine::UI::UIInputEvent;
|
||||||
@@ -19,6 +22,7 @@ using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
|||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||||
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
||||||
|
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||||
using XCEngine::UI::Editor::UIEditorDockHostInteractionState;
|
using XCEngine::UI::Editor::UIEditorDockHostInteractionState;
|
||||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||||
@@ -33,6 +37,7 @@ UIEditorPanelRegistry BuildPanelRegistry() {
|
|||||||
registry.panels = {
|
registry.panels = {
|
||||||
{ "doc-a", "Document A", {}, true, true, true },
|
{ "doc-a", "Document A", {}, true, true, true },
|
||||||
{ "doc-b", "Document B", {}, true, true, true },
|
{ "doc-b", "Document B", {}, true, true, true },
|
||||||
|
{ "doc-c", "Document C", {}, true, true, true },
|
||||||
{ "details", "Details", {}, true, true, true },
|
{ "details", "Details", {}, true, true, true },
|
||||||
{ "console", "Console", {}, true, true, true }
|
{ "console", "Console", {}, true, true, true }
|
||||||
};
|
};
|
||||||
@@ -62,6 +67,30 @@ UIEditorWorkspaceModel BuildWorkspace() {
|
|||||||
return workspace;
|
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 MakePointerMove(float x, float y) {
|
||||||
UIInputEvent event = {};
|
UIInputEvent event = {};
|
||||||
event.type = UIInputEventType::PointerMove;
|
event.type = UIInputEventType::PointerMove;
|
||||||
@@ -230,60 +259,6 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
|
|||||||
EXPECT_EQ(documentStack->selectedPanelId, "doc-a");
|
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) {
|
TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationThroughTabStripInteraction) {
|
||||||
auto controller =
|
auto controller =
|
||||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||||
@@ -399,7 +374,7 @@ TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackBodyActivatesTargetP
|
|||||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackTabCloseClosesPanelThroughController) {
|
TEST(UIEditorDockHostInteractionTest, DraggingTabWithinSameStackRequestsCaptureAndCommitsReorder) {
|
||||||
auto controller =
|
auto controller =
|
||||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||||
UIEditorDockHostInteractionState state = {};
|
UIEditorDockHostInteractionState state = {};
|
||||||
@@ -409,29 +384,411 @@ TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackTabCloseClosesPanelT
|
|||||||
controller,
|
controller,
|
||||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||||
{});
|
{});
|
||||||
const auto* consoleStack = FindTabStackByNodeId(frame.layout, "console-node");
|
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||||
ASSERT_NE(consoleStack, nullptr);
|
ASSERT_NE(documentStack, nullptr);
|
||||||
const UIRect closeRect = consoleStack->tabStripLayout.closeButtonRects[0];
|
const UIPoint sourceCenter = RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
|
||||||
const UIPoint closeCenter = RectCenter(closeRect);
|
const UIPoint dropPoint(
|
||||||
|
documentStack->tabStripLayout.headerRect.x +
|
||||||
|
documentStack->tabStripLayout.headerRect.width - 2.0f,
|
||||||
|
sourceCenter.y);
|
||||||
|
|
||||||
frame = UpdateUIEditorDockHostInteraction(
|
frame = UpdateUIEditorDockHostInteraction(
|
||||||
state,
|
state,
|
||||||
controller,
|
controller,
|
||||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||||
{ MakePointerMove(closeCenter.x, closeCenter.y) });
|
{ MakePointerDown(sourceCenter.x, sourceCenter.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) });
|
|
||||||
EXPECT_TRUE(frame.result.consumed);
|
EXPECT_TRUE(frame.result.consumed);
|
||||||
EXPECT_TRUE(frame.result.commandExecuted);
|
|
||||||
|
|
||||||
const auto* panelState = FindUIEditorPanelSessionState(controller.GetSession(), "console");
|
frame = UpdateUIEditorDockHostInteraction(
|
||||||
ASSERT_NE(panelState, nullptr);
|
state,
|
||||||
EXPECT_FALSE(panelState->open);
|
controller,
|
||||||
EXPECT_FALSE(panelState->visible);
|
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");
|
UIDrawList background("MenuBarBackground");
|
||||||
AppendUIEditorMenuBarBackground(background, layout, items, state);
|
AppendUIEditorMenuBarBackground(background, layout, items, state);
|
||||||
ASSERT_EQ(background.GetCommandCount(), 6u);
|
ASSERT_EQ(background.GetCommandCount(), 3u);
|
||||||
const auto& backgroundCommands = background.GetCommands();
|
const auto& backgroundCommands = background.GetCommands();
|
||||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
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[2].type, UIDrawCommandType::FilledRect);
|
||||||
EXPECT_EQ(backgroundCommands[5].type, UIDrawCommandType::RectOutline);
|
|
||||||
|
|
||||||
UIDrawList foreground("MenuBarForeground");
|
UIDrawList foreground("MenuBarForeground");
|
||||||
AppendUIEditorMenuBarForeground(foreground, layout, items, state);
|
AppendUIEditorMenuBarForeground(foreground, layout, items, state);
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ using XCEngine::UI::Editor::UIEditorShellInteractionModel;
|
|||||||
using XCEngine::UI::Editor::UIEditorShellInteractionPopupItemRequest;
|
using XCEngine::UI::Editor::UIEditorShellInteractionPopupItemRequest;
|
||||||
using XCEngine::UI::Editor::UIEditorShellInteractionServices;
|
using XCEngine::UI::Editor::UIEditorShellInteractionServices;
|
||||||
using XCEngine::UI::Editor::UIEditorShellInteractionState;
|
using XCEngine::UI::Editor::UIEditorShellInteractionState;
|
||||||
|
using XCEngine::UI::Editor::UIEditorTextMeasureRequest;
|
||||||
|
using XCEngine::UI::Editor::UIEditorTextMeasurer;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||||
@@ -52,6 +54,13 @@ using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupInvalidIndex;
|
|||||||
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
|
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
|
||||||
using XCEngine::UI::Widgets::UIPopupDismissReason;
|
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 BuildPanelRegistry() {
|
||||||
UIEditorPanelRegistry registry = {};
|
UIEditorPanelRegistry registry = {};
|
||||||
registry.panels = {
|
registry.panels = {
|
||||||
@@ -219,6 +228,76 @@ UIEditorWorkspaceController BuildController() {
|
|||||||
return BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
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 UIEditorShellInteractionMenuButtonRequest* FindMenuButton(
|
||||||
const UIEditorShellInteractionFrame& frame,
|
const UIEditorShellInteractionFrame& frame,
|
||||||
std::string_view menuId) {
|
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::HitTestUIEditorTabStrip;
|
||||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripDesiredHeaderLabelWidth;
|
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripDesiredHeaderLabelWidth;
|
||||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndex;
|
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndex;
|
||||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndexAfterClose;
|
|
||||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
||||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
|
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::UIEditorTabStripMetrics;
|
||||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripState;
|
using XCEngine::UI::Editor::Widgets::UIEditorTabStripState;
|
||||||
|
|
||||||
TEST(UIEditorTabStripTest, DesiredHeaderWidthReservesCloseButtonBudget) {
|
TEST(UIEditorTabStripTest, DesiredHeaderWidthUsesLabelWidthAndLeftInsetOnly) {
|
||||||
UIEditorTabStripMetrics metrics = {};
|
UIEditorTabStripMetrics metrics = {};
|
||||||
metrics.layoutMetrics.tabHorizontalPadding = 10.0f;
|
metrics.layoutMetrics.tabHorizontalPadding = 10.0f;
|
||||||
metrics.estimatedGlyphWidth = 8.0f;
|
metrics.estimatedGlyphWidth = 8.0f;
|
||||||
@@ -32,14 +31,14 @@ TEST(UIEditorTabStripTest, DesiredHeaderWidthReservesCloseButtonBudget) {
|
|||||||
metrics.closeInsetRight = 14.0f;
|
metrics.closeInsetRight = 14.0f;
|
||||||
metrics.labelInsetX = 12.0f;
|
metrics.labelInsetX = 12.0f;
|
||||||
|
|
||||||
const float closableWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
const float measuredWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||||
UIEditorTabStripItem{ "doc-a", "ABCD", true, 0.0f },
|
UIEditorTabStripItem{ "doc-a", "ABCD", true, 0.0f },
|
||||||
metrics);
|
metrics);
|
||||||
const float fixedWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
const float fixedWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||||
UIEditorTabStripItem{ "doc-b", "Ignored", false, 42.0f },
|
UIEditorTabStripItem{ "doc-b", "Ignored", false, 42.0f },
|
||||||
metrics);
|
metrics);
|
||||||
|
|
||||||
EXPECT_FLOAT_EQ(closableWidth, 58.0f);
|
EXPECT_FLOAT_EQ(measuredWidth, 34.0f);
|
||||||
EXPECT_FLOAT_EQ(fixedWidth, 44.0f);
|
EXPECT_FLOAT_EQ(fixedWidth, 44.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,16 +57,7 @@ TEST(UIEditorTabStripTest, SelectedIndexResolvesByTabIdAndFallsBackToValidRange)
|
|||||||
UIEditorTabStripInvalidIndex);
|
UIEditorTabStripInvalidIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(UIEditorTabStripTest, ClosingTabsResolvesSelectionFallbackFromClosedIndex) {
|
TEST(UIEditorTabStripTest, LayoutUsesCoreTabArrangementWithoutCloseButtons) {
|
||||||
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) {
|
|
||||||
UIEditorTabStripMetrics metrics = {};
|
UIEditorTabStripMetrics metrics = {};
|
||||||
metrics.layoutMetrics.headerHeight = 30.0f;
|
metrics.layoutMetrics.headerHeight = 30.0f;
|
||||||
metrics.layoutMetrics.tabMinWidth = 80.0f;
|
metrics.layoutMetrics.tabMinWidth = 80.0f;
|
||||||
@@ -101,20 +91,16 @@ TEST(UIEditorTabStripTest, LayoutUsesCoreTabArrangementAndBuildsCloseRects) {
|
|||||||
|
|
||||||
ASSERT_EQ(layout.tabHeaderRects.size(), 2u);
|
ASSERT_EQ(layout.tabHeaderRects.size(), 2u);
|
||||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].x, 10.0f);
|
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].x, 10.0f);
|
||||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].width, 90.0f);
|
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].width, 80.0f);
|
||||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].x, 104.0f);
|
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].x, 94.0f);
|
||||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].width, 80.0f);
|
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].width, 80.0f);
|
||||||
|
|
||||||
ASSERT_EQ(layout.closeButtonRects.size(), 2u);
|
ASSERT_EQ(layout.closeButtonRects.size(), 2u);
|
||||||
EXPECT_TRUE(layout.showCloseButtons[0]);
|
EXPECT_FALSE(layout.showCloseButtons[0]);
|
||||||
EXPECT_FALSE(layout.showCloseButtons[1]);
|
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 = {
|
const std::vector<UIEditorTabStripItem> items = {
|
||||||
{ "doc-a", "Document A", true, 48.0f },
|
{ "doc-a", "Document A", true, 48.0f },
|
||||||
{ "doc-b", "Document B", false, 40.0f }
|
{ "doc-b", "Document B", false, 40.0f }
|
||||||
@@ -125,14 +111,23 @@ TEST(UIEditorTabStripTest, HitTestPrioritizesCloseButtonThenTabThenContent) {
|
|||||||
const UIEditorTabStripLayout layout =
|
const UIEditorTabStripLayout layout =
|
||||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state);
|
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state);
|
||||||
|
|
||||||
const auto closeHit = HitTestUIEditorTabStrip(layout, state, UIPoint(85.0f, 34.0f));
|
const auto rightSideTabHit = HitTestUIEditorTabStrip(
|
||||||
EXPECT_EQ(closeHit.kind, UIEditorTabStripHitTargetKind::CloseButton);
|
layout,
|
||||||
EXPECT_EQ(closeHit.index, 0u);
|
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));
|
const auto tabHit = HitTestUIEditorTabStrip(layout, state, UIPoint(40.0f, 34.0f));
|
||||||
EXPECT_EQ(tabHit.kind, UIEditorTabStripHitTargetKind::Tab);
|
EXPECT_EQ(tabHit.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||||
EXPECT_EQ(tabHit.index, 0u);
|
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));
|
const auto contentHit = HitTestUIEditorTabStrip(layout, state, UIPoint(40.0f, 70.0f));
|
||||||
EXPECT_EQ(contentHit.kind, UIEditorTabStripHitTargetKind::Content);
|
EXPECT_EQ(contentHit.kind, UIEditorTabStripHitTargetKind::Content);
|
||||||
EXPECT_EQ(contentHit.index, UIEditorTabStripInvalidIndex);
|
EXPECT_EQ(contentHit.index, UIEditorTabStripInvalidIndex);
|
||||||
@@ -147,7 +142,6 @@ TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
|
|||||||
UIEditorTabStripState state = {};
|
UIEditorTabStripState state = {};
|
||||||
state.selectedIndex = 0u;
|
state.selectedIndex = 0u;
|
||||||
state.hoveredIndex = 1u;
|
state.hoveredIndex = 1u;
|
||||||
state.closeHoveredIndex = 0u;
|
|
||||||
state.focused = true;
|
state.focused = true;
|
||||||
|
|
||||||
const UIEditorTabStripLayout layout =
|
const UIEditorTabStripLayout layout =
|
||||||
@@ -155,24 +149,28 @@ TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
|
|||||||
|
|
||||||
UIDrawList background("TabStripBackground");
|
UIDrawList background("TabStripBackground");
|
||||||
AppendUIEditorTabStripBackground(background, layout, state);
|
AppendUIEditorTabStripBackground(background, layout, state);
|
||||||
ASSERT_EQ(background.GetCommandCount(), 8u);
|
ASSERT_EQ(background.GetCommandCount(), 9u);
|
||||||
const auto& backgroundCommands = background.GetCommands();
|
const auto& backgroundCommands = background.GetCommands();
|
||||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||||
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::RectOutline);
|
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::RectOutline);
|
||||||
EXPECT_EQ(backgroundCommands[4].type, UIDrawCommandType::FilledRect);
|
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");
|
UIDrawList foreground("TabStripForeground");
|
||||||
AppendUIEditorTabStripForeground(foreground, layout, items, state);
|
AppendUIEditorTabStripForeground(foreground, layout, items, state);
|
||||||
ASSERT_EQ(foreground.GetCommandCount(), 9u);
|
ASSERT_EQ(foreground.GetCommandCount(), 8u);
|
||||||
const auto& foregroundCommands = foreground.GetCommands();
|
const auto& foregroundCommands = foreground.GetCommands();
|
||||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::PushClipRect);
|
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||||
EXPECT_EQ(foregroundCommands[1].type, UIDrawCommandType::Text);
|
EXPECT_EQ(foregroundCommands[1].type, UIDrawCommandType::FilledRect);
|
||||||
EXPECT_EQ(foregroundCommands[1].text, "Document A");
|
EXPECT_EQ(foregroundCommands[2].type, UIDrawCommandType::PushClipRect);
|
||||||
EXPECT_EQ(foregroundCommands[5].type, UIDrawCommandType::Text);
|
EXPECT_EQ(foregroundCommands[3].type, UIDrawCommandType::Text);
|
||||||
EXPECT_EQ(foregroundCommands[5].text, "X");
|
EXPECT_EQ(foregroundCommands[3].text, "Document A");
|
||||||
EXPECT_EQ(foregroundCommands[7].type, UIDrawCommandType::Text);
|
EXPECT_EQ(foregroundCommands[5].type, UIDrawCommandType::PushClipRect);
|
||||||
EXPECT_EQ(foregroundCommands[7].text, "Document B");
|
EXPECT_EQ(foregroundCommands[6].type, UIDrawCommandType::Text);
|
||||||
|
EXPECT_EQ(foregroundCommands[6].text, "Document B");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using XCEngine::UI::UIRect;
|
|||||||
using XCEngine::UI::Editor::UIEditorTabStripInteractionState;
|
using XCEngine::UI::Editor::UIEditorTabStripInteractionState;
|
||||||
using XCEngine::UI::Editor::UpdateUIEditorTabStripInteraction;
|
using XCEngine::UI::Editor::UpdateUIEditorTabStripInteraction;
|
||||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
|
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
|
||||||
|
|
||||||
std::vector<UIEditorTabStripItem> BuildTabItems() {
|
std::vector<UIEditorTabStripItem> BuildTabItems() {
|
||||||
@@ -73,7 +74,7 @@ UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabAndCloseState) {
|
TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabOnly) {
|
||||||
const auto items = BuildTabItems();
|
const auto items = BuildTabItems();
|
||||||
std::string selectedTabId = "doc-a";
|
std::string selectedTabId = "doc-a";
|
||||||
UIEditorTabStripInteractionState state = {};
|
UIEditorTabStripInteractionState state = {};
|
||||||
@@ -85,7 +86,7 @@ TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabAndCloseState)
|
|||||||
items,
|
items,
|
||||||
{});
|
{});
|
||||||
|
|
||||||
auto frame = UpdateUIEditorTabStripInteraction(
|
const auto frame = UpdateUIEditorTabStripInteraction(
|
||||||
state,
|
state,
|
||||||
selectedTabId,
|
selectedTabId,
|
||||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||||
@@ -93,21 +94,11 @@ TEST(UIEditorTabStripInteractionTest, PointerMoveUpdatesHoveredTabAndCloseState)
|
|||||||
{ MakePointerMove(
|
{ MakePointerMove(
|
||||||
initialFrame.layout.tabHeaderRects[1].x + 12.0f,
|
initialFrame.layout.tabHeaderRects[1].x + 12.0f,
|
||||||
initialFrame.layout.tabHeaderRects[1].y + 12.0f) });
|
initialFrame.layout.tabHeaderRects[1].y + 12.0f) });
|
||||||
|
|
||||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorTabStripHitTargetKind::Tab);
|
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||||
EXPECT_EQ(state.tabStripState.hoveredIndex, 1u);
|
EXPECT_EQ(state.tabStripState.hoveredIndex, 1u);
|
||||||
EXPECT_EQ(state.tabStripState.closeHoveredIndex, XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex);
|
EXPECT_EQ(state.tabStripState.closeHoveredIndex, UIEditorTabStripInvalidIndex);
|
||||||
|
EXPECT_FALSE(frame.result.closeRequested);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(UIEditorTabStripInteractionTest, LeftClickTabSelectsAndFocusesStrip) {
|
TEST(UIEditorTabStripInteractionTest, LeftClickTabSelectsAndFocusesStrip) {
|
||||||
@@ -141,38 +132,6 @@ TEST(UIEditorTabStripInteractionTest, LeftClickTabSelectsAndFocusesStrip) {
|
|||||||
EXPECT_TRUE(state.tabStripState.focused);
|
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) {
|
TEST(UIEditorTabStripInteractionTest, KeyboardNavigationMovesSelectionWhenFocused) {
|
||||||
const auto items = BuildTabItems();
|
const auto items = BuildTabItems();
|
||||||
std::string selectedTabId = "doc-b";
|
std::string selectedTabId = "doc-b";
|
||||||
@@ -238,7 +197,377 @@ TEST(UIEditorTabStripInteractionTest, OutsideClickAndFocusLostClearFocusAndHover
|
|||||||
items,
|
items,
|
||||||
{ MakePointerLeave(), MakeFocusLost() });
|
{ MakePointerLeave(), MakeFocusLost() });
|
||||||
EXPECT_FALSE(state.tabStripState.focused);
|
EXPECT_FALSE(state.tabStripState.focused);
|
||||||
EXPECT_EQ(state.tabStripState.hoveredIndex, XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex);
|
EXPECT_EQ(state.tabStripState.hoveredIndex, UIEditorTabStripInvalidIndex);
|
||||||
EXPECT_EQ(state.tabStripState.closeHoveredIndex, XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex);
|
EXPECT_EQ(state.tabStripState.closeHoveredIndex, UIEditorTabStripInvalidIndex);
|
||||||
EXPECT_FALSE(state.hasPointerPosition);
|
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);
|
spec);
|
||||||
|
|
||||||
EXPECT_FLOAT_EQ(request.slotLayout.inputRect.width, 800.0f);
|
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.width, 800.0f);
|
||||||
EXPECT_FLOAT_EQ(request.requestedViewportSize.height, 532.0f);
|
EXPECT_FLOAT_EQ(request.requestedViewportSize.height, 554.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(UIEditorViewportShellTest, ResolveRequestTracksChromeBarVisibility) {
|
TEST(UIEditorViewportShellTest, ResolveRequestTracksChromeBarVisibility) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ TEST(UIEditorViewportSlotTest, DesiredToolWidthUsesExplicitValueBeforeEstimatedL
|
|||||||
inferredWidth.label = "Scene";
|
inferredWidth.label = "Scene";
|
||||||
|
|
||||||
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(explicitWidth), 88.0f);
|
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(explicitWidth), 88.0f);
|
||||||
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(inferredWidth), 55.0f);
|
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(inferredWidth), 48.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(UIEditorViewportSlotTest, LayoutBuildsTopBarSurfaceBottomBarAndAspectFittedTexture) {
|
TEST(UIEditorViewportSlotTest, LayoutBuildsTopBarSurfaceBottomBarAndAspectFittedTexture) {
|
||||||
@@ -142,12 +142,12 @@ TEST(UIEditorViewportSlotTest, ToolItemsAlignToEdgesAndTitleRectClampsBetweenToo
|
|||||||
BuildToolItems(),
|
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[0].width, 96.0f);
|
||||||
EXPECT_FLOAT_EQ(layout.toolItemRects[1].x, 762.0f);
|
EXPECT_FLOAT_EQ(layout.toolItemRects[1].x, 768.0f);
|
||||||
EXPECT_FLOAT_EQ(layout.toolItemRects[2].x, 816.0f);
|
EXPECT_FLOAT_EQ(layout.toolItemRects[2].x, 820.0f);
|
||||||
EXPECT_FLOAT_EQ(layout.titleRect.x, 118.0f);
|
EXPECT_FLOAT_EQ(layout.titleRect.x, 110.0f);
|
||||||
EXPECT_FLOAT_EQ(layout.titleRect.width, 634.0f);
|
EXPECT_FLOAT_EQ(layout.titleRect.width, 652.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(UIEditorViewportSlotTest, HitTestPrioritizesToolThenStatusThenSurface) {
|
TEST(UIEditorViewportSlotTest, HitTestPrioritizesToolThenStatusThenSurface) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
||||||
@@ -9,6 +11,9 @@ using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
|
|||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
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::GetUIEditorWorkspaceCommandKindName;
|
||||||
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName;
|
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName;
|
||||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||||
@@ -17,7 +22,11 @@ using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
|
|||||||
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceControllerValidationCode;
|
using XCEngine::UI::Editor::UIEditorWorkspaceControllerValidationCode;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
using XCEngine::UI::Editor::UIEditorWorkspaceController;
|
||||||
|
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||||
|
using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||||
|
using XCEngine::UI::Editor::UIEditorWorkspaceNode;
|
||||||
|
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||||
|
|
||||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||||
@@ -25,6 +34,9 @@ UIEditorPanelRegistry BuildPanelRegistry() {
|
|||||||
registry.panels = {
|
registry.panels = {
|
||||||
{ "doc-a", "Document A", {}, true, true, true },
|
{ "doc-a", "Document A", {}, true, true, true },
|
||||||
{ "doc-b", "Document B", {}, 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 },
|
{ "details", "Details", {}, true, true, true },
|
||||||
{ "root", "Root", {}, true, false, false }
|
{ "root", "Root", {}, true, false, false }
|
||||||
};
|
};
|
||||||
@@ -49,6 +61,34 @@ UIEditorWorkspaceModel BuildWorkspace() {
|
|||||||
return workspace;
|
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
|
} // namespace
|
||||||
|
|
||||||
TEST(UIEditorWorkspaceControllerTest, CommandNameHelpersExposeStableDebugNames) {
|
TEST(UIEditorWorkspaceControllerTest, CommandNameHelpersExposeStableDebugNames) {
|
||||||
@@ -146,3 +186,201 @@ TEST(UIEditorWorkspaceControllerTest, RejectsUnknownPanelAndNonCloseablePanelCom
|
|||||||
EXPECT_EQ(nonCloseable.status, UIEditorWorkspaceCommandStatus::Rejected);
|
EXPECT_EQ(nonCloseable.status, UIEditorWorkspaceCommandStatus::Rejected);
|
||||||
EXPECT_EQ(rootController.GetWorkspace().activePanelId, "root");
|
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 <gtest/gtest.h>
|
||||||
|
|
||||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||||
|
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -12,17 +13,25 @@ using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
|||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||||
|
using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent;
|
||||||
using XCEngine::UI::Editor::CanonicalizeUIEditorWorkspaceModel;
|
using XCEngine::UI::Editor::CanonicalizeUIEditorWorkspaceModel;
|
||||||
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
|
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
|
||||||
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
|
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
|
||||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceActivePanel;
|
using XCEngine::UI::Editor::FindUIEditorWorkspaceActivePanel;
|
||||||
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
|
||||||
using XCEngine::UI::Editor::TryActivateUIEditorWorkspacePanel;
|
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::TrySetUIEditorWorkspaceSplitRatio;
|
||||||
|
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||||
|
using XCEngine::UI::Editor::UIEditorWorkspaceNode;
|
||||||
|
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
|
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||||
using XCEngine::UI::Editor::UIEditorWorkspaceValidationCode;
|
using XCEngine::UI::Editor::UIEditorWorkspaceValidationCode;
|
||||||
|
using XCEngine::UI::Editor::UIEditorWorkspaceVisiblePanel;
|
||||||
using XCEngine::UI::Editor::ValidateUIEditorWorkspace;
|
using XCEngine::UI::Editor::ValidateUIEditorWorkspace;
|
||||||
|
|
||||||
std::vector<std::string> CollectVisiblePanelIds(const UIEditorWorkspaceModel& workspace) {
|
std::vector<std::string> CollectVisiblePanelIds(const UIEditorWorkspaceModel& workspace) {
|
||||||
@@ -170,6 +179,78 @@ TEST(UIEditorWorkspaceModelTest, SplitRatioMutationTargetsSplitNodeAndRejectsInv
|
|||||||
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 1.0f));
|
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) {
|
TEST(UIEditorWorkspaceModelTest, CanonicalizeWrapsStandalonePanelsIntoSingleTabStacks) {
|
||||||
UIEditorWorkspaceModel workspace = {};
|
UIEditorWorkspaceModel workspace = {};
|
||||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||||
@@ -195,3 +276,254 @@ TEST(UIEditorWorkspaceModelTest, CanonicalizeWrapsStandalonePanelsIntoSingleTabS
|
|||||||
"left");
|
"left");
|
||||||
EXPECT_EQ(canonicalWorkspace.root.children[1].kind, UIEditorWorkspaceNodeKind::TabStack);
|
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