Refine XCEditor docking and DPI rendering
This commit is contained in:
@@ -22,6 +22,7 @@ endfunction()
|
||||
set(XCUI_EDITOR_FOUNDATION_SOURCES
|
||||
src/Foundation/UIEditorCommandDispatcher.cpp
|
||||
src/Foundation/UIEditorCommandRegistry.cpp
|
||||
src/Foundation/UIEditorRuntimeTrace.cpp
|
||||
src/Foundation/UIEditorShortcutManager.cpp
|
||||
src/Foundation/UIEditorTheme.cpp
|
||||
)
|
||||
@@ -129,6 +130,7 @@ add_library(XCUIEditorHost STATIC
|
||||
target_include_directories(XCUIEditorHost
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ namespace XCEngine::UI::Editor::Host {
|
||||
|
||||
namespace {
|
||||
|
||||
D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect) {
|
||||
return D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
|
||||
}
|
||||
constexpr float kBaseDpi = 96.0f;
|
||||
constexpr float kDefaultFontSize = 16.0f;
|
||||
|
||||
std::string HrToString(const char* operation, HRESULT hr) {
|
||||
char buffer[128] = {};
|
||||
@@ -18,6 +17,27 @@ std::string HrToString(const char* operation, HRESULT hr) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
float ClampDpiScale(float dpiScale) {
|
||||
return dpiScale > 0.0f ? dpiScale : 1.0f;
|
||||
}
|
||||
|
||||
float ResolveFontSize(float fontSize) {
|
||||
return fontSize > 0.0f ? fontSize : kDefaultFontSize;
|
||||
}
|
||||
|
||||
float SnapToPixel(float value, float dpiScale) {
|
||||
const float scale = ClampDpiScale(dpiScale);
|
||||
return std::round(value * scale);
|
||||
}
|
||||
|
||||
D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect, float dpiScale) {
|
||||
const float left = SnapToPixel(rect.x, dpiScale);
|
||||
const float top = SnapToPixel(rect.y, dpiScale);
|
||||
const float right = SnapToPixel(rect.x + rect.width, dpiScale);
|
||||
const float bottom = SnapToPixel(rect.y + rect.height, dpiScale);
|
||||
return D2D1::RectF(left, top, right, bottom);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool NativeRenderer::Initialize(HWND hwnd) {
|
||||
@@ -64,6 +84,17 @@ void NativeRenderer::Shutdown() {
|
||||
m_hwnd = nullptr;
|
||||
}
|
||||
|
||||
void NativeRenderer::SetDpiScale(float dpiScale) {
|
||||
m_dpiScale = ClampDpiScale(dpiScale);
|
||||
if (m_renderTarget) {
|
||||
m_renderTarget->SetDpi(kBaseDpi, kBaseDpi);
|
||||
}
|
||||
}
|
||||
|
||||
float NativeRenderer::GetDpiScale() const {
|
||||
return m_dpiScale;
|
||||
}
|
||||
|
||||
void NativeRenderer::Resize(UINT width, UINT height) {
|
||||
if (!m_renderTarget || width == 0 || height == 0) {
|
||||
return;
|
||||
@@ -104,6 +135,52 @@ const std::string& NativeRenderer::GetLastRenderError() const {
|
||||
return m_lastRenderError;
|
||||
}
|
||||
|
||||
float NativeRenderer::MeasureTextWidth(
|
||||
const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const {
|
||||
if (!m_dwriteFactory || request.text.empty()) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const std::wstring text = Utf8ToWide(request.text);
|
||||
if (text.empty()) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float dpiScale = ClampDpiScale(m_dpiScale);
|
||||
const float scaledFontSize = ResolveFontSize(request.fontSize) * dpiScale;
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize);
|
||||
if (textFormat == nullptr) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteTextLayout> textLayout;
|
||||
HRESULT hr = m_dwriteFactory->CreateTextLayout(
|
||||
text.c_str(),
|
||||
static_cast<UINT32>(text.size()),
|
||||
textFormat,
|
||||
4096.0f,
|
||||
scaledFontSize * 2.0f,
|
||||
textLayout.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr) || !textLayout) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
DWRITE_TEXT_METRICS textMetrics = {};
|
||||
hr = textLayout->GetMetrics(&textMetrics);
|
||||
if (FAILED(hr)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
DWRITE_OVERHANG_METRICS overhangMetrics = {};
|
||||
float width = textMetrics.widthIncludingTrailingWhitespace;
|
||||
if (SUCCEEDED(textLayout->GetOverhangMetrics(&overhangMetrics))) {
|
||||
width += (std::max)(overhangMetrics.left, 0.0f);
|
||||
width += (std::max)(overhangMetrics.right, 0.0f);
|
||||
}
|
||||
|
||||
return std::ceil(width) / dpiScale;
|
||||
}
|
||||
|
||||
bool NativeRenderer::CaptureToPng(
|
||||
const ::XCEngine::UI::UIDrawData& drawData,
|
||||
UINT width,
|
||||
@@ -146,7 +223,9 @@ bool NativeRenderer::CaptureToPng(
|
||||
|
||||
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties(
|
||||
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED));
|
||||
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
|
||||
kBaseDpi,
|
||||
kBaseDpi);
|
||||
|
||||
Microsoft::WRL::ComPtr<ID2D1RenderTarget> offscreenRenderTarget;
|
||||
hr = m_d2dFactory->CreateWicBitmapRenderTarget(
|
||||
@@ -301,7 +380,11 @@ bool NativeRenderer::CreateDeviceResources() {
|
||||
const UINT width = static_cast<UINT>((std::max)(clientRect.right - clientRect.left, 1L));
|
||||
const UINT height = static_cast<UINT>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||
|
||||
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties();
|
||||
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties(
|
||||
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
|
||||
kBaseDpi,
|
||||
kBaseDpi);
|
||||
const D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties(
|
||||
m_hwnd,
|
||||
D2D1::SizeU(width, height));
|
||||
@@ -324,6 +407,7 @@ bool NativeRenderer::CreateDeviceResources() {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_renderTarget->SetDpi(kBaseDpi, kBaseDpi);
|
||||
m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
||||
m_lastRenderError.clear();
|
||||
return true;
|
||||
@@ -333,6 +417,7 @@ bool NativeRenderer::RenderToTarget(
|
||||
ID2D1RenderTarget& renderTarget,
|
||||
ID2D1SolidColorBrush& solidBrush,
|
||||
const ::XCEngine::UI::UIDrawData& drawData) {
|
||||
renderTarget.SetDpi(kBaseDpi, kBaseDpi);
|
||||
renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
||||
renderTarget.BeginDraw();
|
||||
renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f));
|
||||
@@ -358,13 +443,15 @@ void NativeRenderer::RenderCommand(
|
||||
const ::XCEngine::UI::UIDrawCommand& command,
|
||||
std::vector<D2D1_RECT_F>& clipStack) {
|
||||
solidBrush.SetColor(ToD2DColor(command.color));
|
||||
const float dpiScale = ClampDpiScale(m_dpiScale);
|
||||
|
||||
switch (command.type) {
|
||||
case ::XCEngine::UI::UIDrawCommandType::FilledRect: {
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f;
|
||||
if (command.rounding > 0.0f) {
|
||||
renderTarget.FillRoundedRectangle(
|
||||
D2D1::RoundedRect(rect, command.rounding, command.rounding),
|
||||
D2D1::RoundedRect(rect, rounding, rounding),
|
||||
&solidBrush);
|
||||
} else {
|
||||
renderTarget.FillRectangle(rect, &solidBrush);
|
||||
@@ -372,11 +459,12 @@ void NativeRenderer::RenderCommand(
|
||||
break;
|
||||
}
|
||||
case ::XCEngine::UI::UIDrawCommandType::RectOutline: {
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
||||
const float thickness = command.thickness > 0.0f ? command.thickness : 1.0f;
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale;
|
||||
const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f;
|
||||
if (command.rounding > 0.0f) {
|
||||
renderTarget.DrawRoundedRectangle(
|
||||
D2D1::RoundedRect(rect, command.rounding, command.rounding),
|
||||
D2D1::RoundedRect(rect, rounding, rounding),
|
||||
&solidBrush,
|
||||
thickness);
|
||||
} else {
|
||||
@@ -389,8 +477,9 @@ void NativeRenderer::RenderCommand(
|
||||
break;
|
||||
}
|
||||
|
||||
const float fontSize = command.fontSize > 0.0f ? command.fontSize : 16.0f;
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(fontSize);
|
||||
const float fontSize = ResolveFontSize(command.fontSize);
|
||||
const float scaledFontSize = fontSize * dpiScale;
|
||||
IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize);
|
||||
if (textFormat == nullptr) {
|
||||
break;
|
||||
}
|
||||
@@ -401,11 +490,14 @@ void NativeRenderer::RenderCommand(
|
||||
}
|
||||
|
||||
const D2D1_SIZE_F targetSize = renderTarget.GetSize();
|
||||
const float originX = SnapToPixel(command.position.x, dpiScale);
|
||||
const float originY = SnapToPixel(command.position.y, dpiScale);
|
||||
const float lineHeight = std::ceil(scaledFontSize * 1.6f);
|
||||
const D2D1_RECT_F layoutRect = D2D1::RectF(
|
||||
command.position.x,
|
||||
command.position.y,
|
||||
originX,
|
||||
originY,
|
||||
targetSize.width,
|
||||
command.position.y + fontSize * 1.8f);
|
||||
originY + lineHeight);
|
||||
renderTarget.DrawTextW(
|
||||
text.c_str(),
|
||||
static_cast<UINT32>(text.size()),
|
||||
@@ -413,7 +505,7 @@ void NativeRenderer::RenderCommand(
|
||||
layoutRect,
|
||||
&solidBrush,
|
||||
D2D1_DRAW_TEXT_OPTIONS_CLIP,
|
||||
DWRITE_MEASURING_MODE_NATURAL);
|
||||
DWRITE_MEASURING_MODE_GDI_NATURAL);
|
||||
break;
|
||||
}
|
||||
case ::XCEngine::UI::UIDrawCommandType::Image: {
|
||||
@@ -421,12 +513,12 @@ void NativeRenderer::RenderCommand(
|
||||
break;
|
||||
}
|
||||
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
|
||||
break;
|
||||
}
|
||||
case ::XCEngine::UI::UIDrawCommandType::PushClipRect: {
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
||||
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
||||
renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
clipStack.push_back(rect);
|
||||
break;
|
||||
@@ -443,12 +535,13 @@ void NativeRenderer::RenderCommand(
|
||||
}
|
||||
}
|
||||
|
||||
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
|
||||
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) const {
|
||||
if (!m_dwriteFactory) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int key = static_cast<int>(std::lround(fontSize * 10.0f));
|
||||
const float resolvedFontSize = ResolveFontSize(fontSize);
|
||||
const int key = static_cast<int>(std::lround(resolvedFontSize * 10.0f));
|
||||
const auto found = m_textFormats.find(key);
|
||||
if (found != m_textFormats.end()) {
|
||||
return found->second.Get();
|
||||
@@ -461,7 +554,7 @@ IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
|
||||
DWRITE_FONT_WEIGHT_REGULAR,
|
||||
DWRITE_FONT_STYLE_NORMAL,
|
||||
DWRITE_FONT_STRETCH_NORMAL,
|
||||
fontSize,
|
||||
resolvedFontSize,
|
||||
L"",
|
||||
textFormat.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <d2d1.h>
|
||||
@@ -20,13 +22,17 @@
|
||||
|
||||
namespace XCEngine::UI::Editor::Host {
|
||||
|
||||
class NativeRenderer {
|
||||
class NativeRenderer : public ::XCEngine::UI::Editor::UIEditorTextMeasurer {
|
||||
public:
|
||||
bool Initialize(HWND hwnd);
|
||||
void Shutdown();
|
||||
void SetDpiScale(float dpiScale);
|
||||
float GetDpiScale() const;
|
||||
void Resize(UINT width, UINT height);
|
||||
bool Render(const ::XCEngine::UI::UIDrawData& drawData);
|
||||
const std::string& GetLastRenderError() const;
|
||||
float MeasureTextWidth(
|
||||
const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const override;
|
||||
bool CaptureToPng(
|
||||
const ::XCEngine::UI::UIDrawData& drawData,
|
||||
UINT width,
|
||||
@@ -49,7 +55,7 @@ private:
|
||||
const ::XCEngine::UI::UIDrawCommand& command,
|
||||
std::vector<D2D1_RECT_F>& clipStack);
|
||||
|
||||
IDWriteTextFormat* GetTextFormat(float fontSize);
|
||||
IDWriteTextFormat* GetTextFormat(float fontSize) const;
|
||||
static D2D1_COLOR_F ToD2DColor(const ::XCEngine::UI::UIColor& color);
|
||||
static std::wstring Utf8ToWide(std::string_view text);
|
||||
|
||||
@@ -59,9 +65,10 @@ private:
|
||||
Microsoft::WRL::ComPtr<IWICImagingFactory> m_wicFactory;
|
||||
Microsoft::WRL::ComPtr<ID2D1HwndRenderTarget> m_renderTarget;
|
||||
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> m_solidBrush;
|
||||
std::unordered_map<int, Microsoft::WRL::ComPtr<IDWriteTextFormat>> m_textFormats;
|
||||
mutable std::unordered_map<int, Microsoft::WRL::ComPtr<IDWriteTextFormat>> m_textFormats;
|
||||
std::string m_lastRenderError = {};
|
||||
bool m_wicComInitialized = false;
|
||||
float m_dpiScale = 1.0f;
|
||||
};
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Host
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "Shell/ProductShellAsset.h"
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
@@ -10,8 +11,11 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <shellscalingapi.h>
|
||||
|
||||
#ifndef XCUIEDITOR_REPO_ROOT
|
||||
#define XCUIEDITOR_REPO_ROOT "."
|
||||
#endif
|
||||
@@ -34,11 +38,112 @@ using App::BuildProductShellInteractionDefinition;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost";
|
||||
constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor";
|
||||
constexpr UINT kDefaultDpi = 96u;
|
||||
constexpr float kBaseDpiScale = 96.0f;
|
||||
|
||||
Application* GetApplicationFromWindow(HWND hwnd) {
|
||||
return reinterpret_cast<Application*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
UINT QuerySystemDpi() {
|
||||
HDC screenDc = GetDC(nullptr);
|
||||
if (screenDc == nullptr) {
|
||||
return kDefaultDpi;
|
||||
}
|
||||
|
||||
const int dpiX = GetDeviceCaps(screenDc, LOGPIXELSX);
|
||||
ReleaseDC(nullptr, screenDc);
|
||||
return dpiX > 0 ? static_cast<UINT>(dpiX) : kDefaultDpi;
|
||||
}
|
||||
|
||||
UINT QueryWindowDpi(HWND hwnd) {
|
||||
if (hwnd != nullptr) {
|
||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (user32 != nullptr) {
|
||||
using GetDpiForWindowFn = UINT(WINAPI*)(HWND);
|
||||
const auto getDpiForWindow =
|
||||
reinterpret_cast<GetDpiForWindowFn>(GetProcAddress(user32, "GetDpiForWindow"));
|
||||
if (getDpiForWindow != nullptr) {
|
||||
const UINT dpi = getDpiForWindow(hwnd);
|
||||
if (dpi != 0u) {
|
||||
return dpi;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QuerySystemDpi();
|
||||
}
|
||||
|
||||
void EnableDpiAwareness() {
|
||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (user32 != nullptr) {
|
||||
using SetProcessDpiAwarenessContextFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT);
|
||||
const auto setProcessDpiAwarenessContext =
|
||||
reinterpret_cast<SetProcessDpiAwarenessContextFn>(
|
||||
GetProcAddress(user32, "SetProcessDpiAwarenessContext"));
|
||||
if (setProcessDpiAwarenessContext != nullptr) {
|
||||
if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) {
|
||||
return;
|
||||
}
|
||||
if (GetLastError() == ERROR_ACCESS_DENIED) {
|
||||
return;
|
||||
}
|
||||
if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) {
|
||||
return;
|
||||
}
|
||||
if (GetLastError() == ERROR_ACCESS_DENIED) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HMODULE shcore = LoadLibraryW(L"shcore.dll");
|
||||
if (shcore != nullptr) {
|
||||
using SetProcessDpiAwarenessFn = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS);
|
||||
const auto setProcessDpiAwareness =
|
||||
reinterpret_cast<SetProcessDpiAwarenessFn>(
|
||||
GetProcAddress(shcore, "SetProcessDpiAwareness"));
|
||||
if (setProcessDpiAwareness != nullptr) {
|
||||
const HRESULT hr = setProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
|
||||
FreeLibrary(shcore);
|
||||
if (SUCCEEDED(hr) || hr == E_ACCESSDENIED) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
FreeLibrary(shcore);
|
||||
}
|
||||
}
|
||||
|
||||
if (user32 != nullptr) {
|
||||
using SetProcessDPIAwareFn = BOOL(WINAPI*)();
|
||||
const auto setProcessDPIAware =
|
||||
reinterpret_cast<SetProcessDPIAwareFn>(GetProcAddress(user32, "SetProcessDPIAware"));
|
||||
if (setProcessDPIAware != nullptr) {
|
||||
setProcessDPIAware();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TryEnableNonClientDpiScaling(HWND hwnd) {
|
||||
if (hwnd == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (user32 == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
using EnableNonClientDpiScalingFn = BOOL(WINAPI*)(HWND);
|
||||
const auto enableNonClientDpiScaling =
|
||||
reinterpret_cast<EnableNonClientDpiScalingFn>(
|
||||
GetProcAddress(user32, "EnableNonClientDpiScaling"));
|
||||
if (enableNonClientDpiScaling != nullptr) {
|
||||
enableNonClientDpiScaling(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
std::string TruncateText(const std::string& text, std::size_t maxLength) {
|
||||
if (text.size() <= maxLength) {
|
||||
return text;
|
||||
@@ -146,6 +251,44 @@ bool IsRepeatKeyMessage(LPARAM lParam) {
|
||||
return (static_cast<unsigned long>(lParam) & (1ul << 30)) != 0ul;
|
||||
}
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
const DWORD copied = ::GetModuleFileNameW(
|
||||
nullptr,
|
||||
buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (copied == 0u) {
|
||||
return std::filesystem::current_path().lexically_normal();
|
||||
}
|
||||
|
||||
if (copied < buffer.size() - 1u) {
|
||||
return std::filesystem::path(std::wstring(buffer.data(), copied))
|
||||
.parent_path()
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
buffer.resize(buffer.size() * 2u);
|
||||
}
|
||||
}
|
||||
|
||||
std::string DescribeInputEventType(const UIInputEvent& event) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove: return "PointerMove";
|
||||
case UIInputEventType::PointerEnter: return "PointerEnter";
|
||||
case UIInputEventType::PointerLeave: return "PointerLeave";
|
||||
case UIInputEventType::PointerButtonDown: return "PointerDown";
|
||||
case UIInputEventType::PointerButtonUp: return "PointerUp";
|
||||
case UIInputEventType::PointerWheel: return "PointerWheel";
|
||||
case UIInputEventType::KeyDown: return "KeyDown";
|
||||
case UIInputEventType::KeyUp: return "KeyUp";
|
||||
case UIInputEventType::Character: return "Character";
|
||||
case UIInputEventType::FocusGained: return "FocusGained";
|
||||
case UIInputEventType::FocusLost: return "FocusLost";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
@@ -172,10 +315,17 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
|
||||
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
m_hInstance = hInstance;
|
||||
EnableDpiAwareness();
|
||||
const std::filesystem::path logRoot =
|
||||
GetExecutableDirectory() / "logs";
|
||||
InitializeUIEditorRuntimeTrace(logRoot);
|
||||
SetUnhandledExceptionFilter(&Application::HandleUnhandledException);
|
||||
LogRuntimeTrace("app", "initialize begin");
|
||||
m_shellAsset = BuildProductShellAsset(ResolveRepoRootPath());
|
||||
m_shellValidation = ValidateEditorShellAsset(m_shellAsset);
|
||||
m_validationMessage = m_shellValidation.message;
|
||||
if (!m_shellValidation.IsValid()) {
|
||||
LogRuntimeTrace("app", "shell asset validation failed: " + m_validationMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -188,8 +338,10 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
m_shellServices = {};
|
||||
m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher();
|
||||
m_shellServices.shortcutManager = &m_shortcutManager;
|
||||
m_shellServices.textMeasurer = &m_renderer;
|
||||
m_lastStatus = "Ready";
|
||||
m_lastMessage = "Old editor shell baseline loaded.";
|
||||
LogRuntimeTrace("app", "workspace initialized: " + DescribeWorkspaceState());
|
||||
|
||||
WNDCLASSEXW windowClass = {};
|
||||
windowClass.cbSize = sizeof(windowClass);
|
||||
@@ -200,6 +352,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
windowClass.lpszClassName = kWindowClassName;
|
||||
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||
if (m_windowClassAtom == 0) {
|
||||
LogRuntimeTrace("app", "window class registration failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -207,7 +360,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
0,
|
||||
kWindowClassName,
|
||||
kWindowTitle,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
1540,
|
||||
@@ -217,26 +370,37 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
hInstance,
|
||||
this);
|
||||
if (m_hwnd == nullptr) {
|
||||
LogRuntimeTrace("app", "window creation failed");
|
||||
return false;
|
||||
}
|
||||
m_windowDpi = QueryWindowDpi(m_hwnd);
|
||||
m_dpiScale = GetDpiScale();
|
||||
m_renderer.SetDpiScale(m_dpiScale);
|
||||
|
||||
std::ostringstream dpiTrace = {};
|
||||
dpiTrace << "initial dpi=" << m_windowDpi << " scale=" << m_dpiScale;
|
||||
LogRuntimeTrace("window", dpiTrace.str());
|
||||
|
||||
if (!m_renderer.Initialize(m_hwnd)) {
|
||||
LogRuntimeTrace("app", "renderer initialization failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
ShowWindow(m_hwnd, nCmdShow);
|
||||
UpdateWindow(m_hwnd);
|
||||
|
||||
if (!m_renderer.Initialize(m_hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_autoScreenshot.Initialize(m_shellAsset.captureRootPath);
|
||||
if (IsAutoCaptureOnStartupEnabled()) {
|
||||
m_autoScreenshot.RequestCapture("startup");
|
||||
m_lastStatus = "Capture";
|
||||
m_lastMessage = "Startup capture requested.";
|
||||
}
|
||||
LogRuntimeTrace("app", "initialize completed");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::Shutdown() {
|
||||
LogRuntimeTrace("app", "shutdown begin");
|
||||
if (GetCapture() == m_hwnd) {
|
||||
ReleaseCapture();
|
||||
}
|
||||
@@ -253,6 +417,9 @@ void Application::Shutdown() {
|
||||
UnregisterClassW(kWindowClassName, m_hInstance);
|
||||
m_windowClassAtom = 0;
|
||||
}
|
||||
|
||||
LogRuntimeTrace("app", "shutdown end");
|
||||
ShutdownUIEditorRuntimeTrace();
|
||||
}
|
||||
|
||||
void Application::RenderFrame() {
|
||||
@@ -262,8 +429,12 @@ void Application::RenderFrame() {
|
||||
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
|
||||
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||
const unsigned int pixelWidth =
|
||||
static_cast<unsigned int>((std::max)(clientRect.right - clientRect.left, 1L));
|
||||
const unsigned int pixelHeight =
|
||||
static_cast<unsigned int>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
||||
const float width = PixelsToDips(static_cast<float>(pixelWidth));
|
||||
const float height = PixelsToDips(static_cast<float>(pixelHeight));
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell");
|
||||
@@ -277,6 +448,11 @@ void Application::RenderFrame() {
|
||||
const UIEditorShellInteractionDefinition definition = BuildShellDefinition();
|
||||
std::vector<UIInputEvent> frameEvents = std::move(m_pendingInputEvents);
|
||||
m_pendingInputEvents.clear();
|
||||
if (!frameEvents.empty()) {
|
||||
LogRuntimeTrace(
|
||||
"input",
|
||||
DescribeInputEvents(frameEvents) + " | " + DescribeWorkspaceState());
|
||||
}
|
||||
|
||||
m_shellFrame = UpdateUIEditorShellInteraction(
|
||||
m_shellInteractionState,
|
||||
@@ -286,6 +462,24 @@ void Application::RenderFrame() {
|
||||
frameEvents,
|
||||
m_shellServices,
|
||||
metrics);
|
||||
if (!frameEvents.empty() ||
|
||||
m_shellFrame.result.workspaceResult.dockHostResult.layoutChanged ||
|
||||
m_shellFrame.result.workspaceResult.dockHostResult.commandExecuted) {
|
||||
std::ostringstream frameTrace = {};
|
||||
frameTrace << "result consumed="
|
||||
<< (m_shellFrame.result.consumed ? "true" : "false")
|
||||
<< " layoutChanged="
|
||||
<< (m_shellFrame.result.workspaceResult.dockHostResult.layoutChanged ? "true" : "false")
|
||||
<< " commandExecuted="
|
||||
<< (m_shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false")
|
||||
<< " active="
|
||||
<< m_workspaceController.GetWorkspace().activePanelId
|
||||
<< " message="
|
||||
<< m_shellFrame.result.workspaceResult.dockHostResult.layoutResult.message;
|
||||
LogRuntimeTrace(
|
||||
"frame",
|
||||
frameTrace.str());
|
||||
}
|
||||
ApplyHostCaptureRequests(m_shellFrame.result);
|
||||
UpdateLastStatus(m_shellFrame.result);
|
||||
AppendUIEditorShellInteraction(
|
||||
@@ -311,11 +505,78 @@ void Application::RenderFrame() {
|
||||
m_autoScreenshot.CaptureIfRequested(
|
||||
m_renderer,
|
||||
drawData,
|
||||
static_cast<unsigned int>(width),
|
||||
static_cast<unsigned int>(height),
|
||||
pixelWidth,
|
||||
pixelHeight,
|
||||
framePresented);
|
||||
}
|
||||
|
||||
float Application::GetDpiScale() const {
|
||||
const UINT dpi = m_windowDpi == 0u ? kDefaultDpi : m_windowDpi;
|
||||
return static_cast<float>(dpi) / kBaseDpiScale;
|
||||
}
|
||||
|
||||
float Application::PixelsToDips(float pixels) const {
|
||||
const float dpiScale = GetDpiScale();
|
||||
return dpiScale > 0.0f ? pixels / dpiScale : pixels;
|
||||
}
|
||||
|
||||
UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const {
|
||||
return UIPoint(
|
||||
PixelsToDips(static_cast<float>(x)),
|
||||
PixelsToDips(static_cast<float>(y)));
|
||||
}
|
||||
|
||||
void Application::LogRuntimeTrace(
|
||||
std::string_view channel,
|
||||
std::string_view message) const {
|
||||
AppendUIEditorRuntimeTrace(channel, message);
|
||||
}
|
||||
|
||||
std::string Application::DescribeWorkspaceState() const {
|
||||
std::ostringstream stream = {};
|
||||
stream << "active=" << m_workspaceController.GetWorkspace().activePanelId;
|
||||
const auto visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(
|
||||
m_workspaceController.GetWorkspace(),
|
||||
m_workspaceController.GetSession());
|
||||
stream << " visible=[";
|
||||
for (std::size_t index = 0; index < visiblePanels.size(); ++index) {
|
||||
if (index > 0u) {
|
||||
stream << ',';
|
||||
}
|
||||
stream << visiblePanels[index].panelId;
|
||||
}
|
||||
stream << ']';
|
||||
|
||||
const auto& dockState =
|
||||
m_shellInteractionState.workspaceInteractionState.dockHostInteractionState;
|
||||
stream << " dragNode=" << dockState.activeTabDragNodeId;
|
||||
stream << " dragPanel=" << dockState.activeTabDragPanelId;
|
||||
if (dockState.dockHostState.dropPreview.visible) {
|
||||
stream << " dropTarget=" << dockState.dockHostState.dropPreview.targetNodeId;
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string Application::DescribeInputEvents(
|
||||
const std::vector<UIInputEvent>& events) const {
|
||||
std::ostringstream stream = {};
|
||||
stream << "events=[";
|
||||
for (std::size_t index = 0; index < events.size(); ++index) {
|
||||
if (index > 0u) {
|
||||
stream << " | ";
|
||||
}
|
||||
const UIInputEvent& event = events[index];
|
||||
stream << DescribeInputEventType(event)
|
||||
<< '@'
|
||||
<< static_cast<int>(event.position.x)
|
||||
<< ','
|
||||
<< static_cast<int>(event.position.y);
|
||||
}
|
||||
stream << ']';
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
void Application::OnResize(UINT width, UINT height) {
|
||||
if (width == 0 || height == 0) {
|
||||
return;
|
||||
@@ -324,6 +585,27 @@ void Application::OnResize(UINT width, UINT height) {
|
||||
m_renderer.Resize(width, height);
|
||||
}
|
||||
|
||||
void Application::OnDpiChanged(UINT dpi, const RECT& suggestedRect) {
|
||||
m_windowDpi = dpi == 0u ? kDefaultDpi : dpi;
|
||||
m_dpiScale = GetDpiScale();
|
||||
m_renderer.SetDpiScale(m_dpiScale);
|
||||
if (m_hwnd != nullptr) {
|
||||
SetWindowPos(
|
||||
m_hwnd,
|
||||
nullptr,
|
||||
suggestedRect.left,
|
||||
suggestedRect.top,
|
||||
suggestedRect.right - suggestedRect.left,
|
||||
suggestedRect.bottom - suggestedRect.top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
std::ostringstream trace = {};
|
||||
trace << "dpi changed to " << m_windowDpi << " scale=" << m_dpiScale;
|
||||
LogRuntimeTrace("window", trace.str());
|
||||
}
|
||||
|
||||
void Application::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) {
|
||||
if (result.requestPointerCapture && GetCapture() != m_hwnd) {
|
||||
SetCapture(m_hwnd);
|
||||
@@ -338,6 +620,10 @@ bool Application::HasInteractiveCaptureState() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!m_shellInteractionState.workspaceInteractionState.dockHostInteractionState.activeTabDragNodeId.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto& panelState : m_shellInteractionState.workspaceInteractionState.composeState.panelStates) {
|
||||
if (panelState.viewportShellState.inputBridgeState.captured) {
|
||||
return true;
|
||||
@@ -434,9 +720,9 @@ void Application::QueuePointerEvent(
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.pointerButton = button;
|
||||
event.position = UIPoint(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
event.position = ConvertClientPixelsToDips(
|
||||
GET_X_LPARAM(lParam),
|
||||
GET_Y_LPARAM(lParam));
|
||||
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
||||
m_pendingInputEvents.push_back(event);
|
||||
}
|
||||
@@ -448,9 +734,7 @@ void Application::QueuePointerLeaveEvent() {
|
||||
POINT clientPoint = {};
|
||||
GetCursorPos(&clientPoint);
|
||||
ScreenToClient(m_hwnd, &clientPoint);
|
||||
event.position = UIPoint(
|
||||
static_cast<float>(clientPoint.x),
|
||||
static_cast<float>(clientPoint.y));
|
||||
event.position = ConvertClientPixelsToDips(clientPoint.x, clientPoint.y);
|
||||
}
|
||||
m_pendingInputEvents.push_back(event);
|
||||
}
|
||||
@@ -468,9 +752,7 @@ void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerWheel;
|
||||
event.position = UIPoint(
|
||||
static_cast<float>(screenPoint.x),
|
||||
static_cast<float>(screenPoint.y));
|
||||
event.position = ConvertClientPixelsToDips(screenPoint.x, screenPoint.y);
|
||||
event.wheelDelta = static_cast<float>(wheelDelta);
|
||||
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
|
||||
m_pendingInputEvents.push_back(event);
|
||||
@@ -507,6 +789,19 @@ std::filesystem::path Application::ResolveRepoRootPath() {
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
LONG WINAPI Application::HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo) {
|
||||
if (exceptionInfo != nullptr &&
|
||||
exceptionInfo->ExceptionRecord != nullptr) {
|
||||
AppendUIEditorCrashTrace(
|
||||
exceptionInfo->ExceptionRecord->ExceptionCode,
|
||||
exceptionInfo->ExceptionRecord->ExceptionAddress);
|
||||
} else {
|
||||
AppendUIEditorCrashTrace(0u, nullptr);
|
||||
}
|
||||
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
UIEditorHostCommandEvaluationResult Application::EvaluateHostCommand(
|
||||
std::string_view commandId) const {
|
||||
UIEditorHostCommandEvaluationResult result = {};
|
||||
@@ -573,6 +868,7 @@ UIEditorHostCommandDispatchResult Application::DispatchHostCommand(
|
||||
|
||||
LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
if (message == WM_NCCREATE) {
|
||||
TryEnableNonClientDpiScaling(hwnd);
|
||||
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||
auto* application = reinterpret_cast<Application*>(createStruct->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(application));
|
||||
@@ -581,6 +877,14 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
|
||||
|
||||
Application* application = GetApplicationFromWindow(hwnd);
|
||||
switch (message) {
|
||||
case WM_DPICHANGED:
|
||||
if (application != nullptr && lParam != 0) {
|
||||
application->OnDpiChanged(
|
||||
static_cast<UINT>(LOWORD(wParam)),
|
||||
*reinterpret_cast<RECT*>(lParam));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_SIZE:
|
||||
if (application != nullptr && wParam != SIZE_MINIMIZED) {
|
||||
application->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||
|
||||
@@ -42,10 +42,18 @@ private:
|
||||
void Shutdown();
|
||||
void RenderFrame();
|
||||
void OnResize(UINT width, UINT height);
|
||||
void OnDpiChanged(UINT dpi, const RECT& suggestedRect);
|
||||
float GetDpiScale() const;
|
||||
float PixelsToDips(float pixels) const;
|
||||
::XCEngine::UI::UIPoint ConvertClientPixelsToDips(LONG x, LONG y) const;
|
||||
void LogRuntimeTrace(std::string_view channel, std::string_view message) const;
|
||||
void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result);
|
||||
bool HasInteractiveCaptureState() const;
|
||||
UIEditorShellInteractionDefinition BuildShellDefinition() const;
|
||||
void UpdateLastStatus(const UIEditorShellInteractionResult& result);
|
||||
std::string DescribeWorkspaceState() const;
|
||||
std::string DescribeInputEvents(
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& events) const;
|
||||
void QueuePointerEvent(
|
||||
::XCEngine::UI::UIInputEventType type,
|
||||
::XCEngine::UI::UIPointerButton button,
|
||||
@@ -57,6 +65,7 @@ private:
|
||||
void QueueCharacterEvent(WPARAM wParam, LPARAM lParam);
|
||||
void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type);
|
||||
static std::filesystem::path ResolveRepoRootPath();
|
||||
static LONG WINAPI HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo);
|
||||
|
||||
HWND m_hwnd = nullptr;
|
||||
HINSTANCE m_hInstance = nullptr;
|
||||
@@ -76,6 +85,8 @@ private:
|
||||
std::string m_validationMessage = {};
|
||||
std::string m_lastStatus = {};
|
||||
std::string m_lastMessage = {};
|
||||
UINT m_windowDpi = 96u;
|
||||
float m_dpiScale = 1.0f;
|
||||
};
|
||||
|
||||
int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow);
|
||||
|
||||
@@ -22,11 +22,21 @@ struct UIEditorTabStripItem {
|
||||
float desiredHeaderLabelWidth = 0.0f;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripReorderState {
|
||||
::XCEngine::UI::UIPoint pressPosition = {};
|
||||
std::size_t pressedIndex = UIEditorTabStripInvalidIndex;
|
||||
std::size_t sourceIndex = UIEditorTabStripInvalidIndex;
|
||||
std::size_t previewInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
bool armed = false;
|
||||
bool dragging = false;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripState {
|
||||
std::size_t selectedIndex = UIEditorTabStripInvalidIndex;
|
||||
std::size_t hoveredIndex = UIEditorTabStripInvalidIndex;
|
||||
std::size_t closeHoveredIndex = UIEditorTabStripInvalidIndex;
|
||||
bool focused = false;
|
||||
UIEditorTabStripReorderState reorder = {};
|
||||
};
|
||||
|
||||
struct UIEditorTabStripMetrics {
|
||||
@@ -38,21 +48,26 @@ struct UIEditorTabStripMetrics {
|
||||
float closeInsetRight = 6.0f;
|
||||
float closeInsetY = 0.0f;
|
||||
float labelInsetX = 8.0f;
|
||||
float labelInsetY = -2.5f;
|
||||
float labelInsetY = -0.5f;
|
||||
float baseBorderThickness = 1.0f;
|
||||
float selectedBorderThickness = 1.0f;
|
||||
float focusedBorderThickness = 1.0f;
|
||||
float reorderDragThreshold = 6.0f;
|
||||
float reorderPreviewThickness = 2.0f;
|
||||
float reorderPreviewInsetY = 3.0f;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripPalette {
|
||||
::XCEngine::UI::UIColor stripBackgroundColor =
|
||||
::XCEngine::UI::UIColor(0.16f, 0.16f, 0.16f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor headerBackgroundColor =
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor contentBackgroundColor =
|
||||
::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor stripBorderColor =
|
||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
::XCEngine::UI::UIColor headerContentSeparatorColor =
|
||||
::XCEngine::UI::UIColor(0.27f, 0.27f, 0.27f, 1.0f);
|
||||
::XCEngine::UI::UIColor focusedBorderColor =
|
||||
::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f);
|
||||
::XCEngine::UI::UIColor tabColor =
|
||||
@@ -60,7 +75,7 @@ struct UIEditorTabStripPalette {
|
||||
::XCEngine::UI::UIColor tabHoveredColor =
|
||||
::XCEngine::UI::UIColor(0.23f, 0.23f, 0.23f, 1.0f);
|
||||
::XCEngine::UI::UIColor tabSelectedColor =
|
||||
::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor tabBorderColor =
|
||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
::XCEngine::UI::UIColor tabHoveredBorderColor =
|
||||
@@ -81,6 +96,14 @@ struct UIEditorTabStripPalette {
|
||||
::XCEngine::UI::UIColor(0.30f, 0.30f, 0.30f, 1.0f);
|
||||
::XCEngine::UI::UIColor closeGlyphColor =
|
||||
::XCEngine::UI::UIColor(0.83f, 0.83f, 0.83f, 1.0f);
|
||||
::XCEngine::UI::UIColor reorderPreviewColor =
|
||||
::XCEngine::UI::UIColor(0.82f, 0.82f, 0.82f, 1.0f);
|
||||
};
|
||||
|
||||
struct UIEditorTabStripInsertionPreview {
|
||||
bool visible = false;
|
||||
std::size_t insertionIndex = UIEditorTabStripInvalidIndex;
|
||||
::XCEngine::UI::UIRect indicatorRect = {};
|
||||
};
|
||||
|
||||
struct UIEditorTabStripLayout {
|
||||
@@ -91,6 +114,7 @@ struct UIEditorTabStripLayout {
|
||||
std::vector<::XCEngine::UI::UIRect> closeButtonRects = {};
|
||||
std::vector<bool> showCloseButtons = {};
|
||||
std::size_t selectedIndex = UIEditorTabStripInvalidIndex;
|
||||
UIEditorTabStripInsertionPreview insertionPreview = {};
|
||||
};
|
||||
|
||||
enum class UIEditorTabStripHitTargetKind : std::uint8_t {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <XCEditor/Collections/UIEditorTabStrip.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIDragDropInteraction.h>
|
||||
#include <XCEngine/UI/Widgets/UITabStripModel.h>
|
||||
|
||||
#include <string>
|
||||
@@ -13,9 +14,13 @@ namespace XCEngine::UI::Editor {
|
||||
struct UIEditorTabStripInteractionState {
|
||||
Widgets::UIEditorTabStripState tabStripState = {};
|
||||
::XCEngine::UI::Widgets::UITabStripModel navigationModel = {};
|
||||
::XCEngine::UI::Widgets::UIDragDropState reorderDragState = {};
|
||||
Widgets::UIEditorTabStripHitTarget pressedTarget = {};
|
||||
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||
std::size_t reorderSourceIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::size_t reorderPreviewIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
bool hasPointerPosition = false;
|
||||
bool reorderCaptureActive = false;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripInteractionResult {
|
||||
@@ -23,11 +28,23 @@ struct UIEditorTabStripInteractionResult {
|
||||
bool selectionChanged = false;
|
||||
bool closeRequested = false;
|
||||
bool keyboardNavigated = false;
|
||||
bool requestPointerCapture = false;
|
||||
bool releasePointerCapture = false;
|
||||
bool dragStarted = false;
|
||||
bool dragEnded = false;
|
||||
bool dragCanceled = false;
|
||||
bool reorderRequested = false;
|
||||
bool reorderPreviewActive = false;
|
||||
Widgets::UIEditorTabStripHitTarget hitTarget = {};
|
||||
std::string selectedTabId = {};
|
||||
std::size_t selectedIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::string closedTabId = {};
|
||||
std::size_t closedIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::string draggedTabId = {};
|
||||
std::size_t dragSourceIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::size_t dropInsertionIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::size_t reorderToIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::size_t reorderPreviewIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
};
|
||||
|
||||
struct UIEditorTabStripInteractionFrame {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
void InitializeUIEditorRuntimeTrace(const std::filesystem::path& logRoot);
|
||||
void ShutdownUIEditorRuntimeTrace();
|
||||
|
||||
void AppendUIEditorRuntimeTrace(
|
||||
std::string_view channel,
|
||||
std::string_view message);
|
||||
|
||||
void AppendUIEditorCrashTrace(
|
||||
std::uint32_t exceptionCode,
|
||||
const void* exceptionAddress);
|
||||
|
||||
std::filesystem::path GetUIEditorRuntimeTracePath();
|
||||
std::filesystem::path GetUIEditorCrashTracePath();
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct UIEditorTextMeasureRequest {
|
||||
std::string_view text = {};
|
||||
float fontSize = 0.0f;
|
||||
};
|
||||
|
||||
class UIEditorTextMeasurer {
|
||||
public:
|
||||
virtual ~UIEditorTextMeasurer() = default;
|
||||
|
||||
virtual float MeasureTextWidth(const UIEditorTextMeasureRequest& request) const = 0;
|
||||
};
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -40,16 +40,27 @@ struct UIEditorDockHostTabStripVisualState {
|
||||
UIEditorTabStripState state = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostDropPreviewState {
|
||||
bool visible = false;
|
||||
std::string sourceNodeId = {};
|
||||
std::string sourcePanelId = {};
|
||||
std::string targetNodeId = {};
|
||||
UIEditorWorkspaceDockPlacement placement =
|
||||
UIEditorWorkspaceDockPlacement::Center;
|
||||
std::size_t insertionIndex = UIEditorTabStripInvalidIndex;
|
||||
};
|
||||
|
||||
struct UIEditorDockHostState {
|
||||
bool focused = false;
|
||||
UIEditorDockHostHitTarget hoveredTarget = {};
|
||||
std::string activeSplitterNodeId = {};
|
||||
std::vector<UIEditorDockHostTabStripVisualState> tabStripStates = {};
|
||||
UIEditorDockHostDropPreviewState dropPreview = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostMetrics {
|
||||
::XCEngine::UI::Layout::UISplitterMetrics splitterMetrics =
|
||||
::XCEngine::UI::Layout::UISplitterMetrics{ 4.0f, 12.0f };
|
||||
::XCEngine::UI::Layout::UISplitterMetrics{ 1.0f, 10.0f };
|
||||
UIEditorTabStripMetrics tabStripMetrics = {};
|
||||
UIEditorPanelFrameMetrics panelFrameMetrics = {};
|
||||
::XCEngine::UI::UISize minimumStandalonePanelBodySize =
|
||||
@@ -75,6 +86,10 @@ struct UIEditorDockHostPalette {
|
||||
::XCEngine::UI::UIColor(0.70f, 0.72f, 0.74f, 1.0f);
|
||||
::XCEngine::UI::UIColor placeholderMutedColor =
|
||||
::XCEngine::UI::UIColor(0.58f, 0.59f, 0.62f, 1.0f);
|
||||
::XCEngine::UI::UIColor dropPreviewFillColor =
|
||||
::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 0.14f);
|
||||
::XCEngine::UI::UIColor dropPreviewBorderColor =
|
||||
::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 0.78f);
|
||||
};
|
||||
|
||||
struct UIEditorDockHostTabItemLayout {
|
||||
@@ -107,10 +122,20 @@ struct UIEditorDockHostTabStackLayout {
|
||||
UIEditorPanelFrameLayout contentFrameLayout = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostDropPreviewLayout {
|
||||
bool visible = false;
|
||||
std::string targetNodeId = {};
|
||||
UIEditorWorkspaceDockPlacement placement =
|
||||
UIEditorWorkspaceDockPlacement::Center;
|
||||
std::size_t insertionIndex = UIEditorTabStripInvalidIndex;
|
||||
::XCEngine::UI::UIRect previewRect = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostLayout {
|
||||
::XCEngine::UI::UIRect bounds = {};
|
||||
std::vector<UIEditorDockHostSplitterLayout> splitters = {};
|
||||
std::vector<UIEditorDockHostTabStackLayout> tabStacks = {};
|
||||
UIEditorDockHostDropPreviewLayout dropPreview = {};
|
||||
};
|
||||
|
||||
// Allows higher-level compose to own panel body presentation while DockHost
|
||||
|
||||
@@ -21,6 +21,8 @@ struct UIEditorDockHostInteractionState {
|
||||
Widgets::UIEditorDockHostState dockHostState = {};
|
||||
::XCEngine::UI::Widgets::UISplitterDragState splitterDragState = {};
|
||||
std::vector<UIEditorDockHostTabStripInteractionEntry> tabStripInteractions = {};
|
||||
std::string activeTabDragNodeId = {};
|
||||
std::string activeTabDragPanelId = {};
|
||||
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||
bool hasPointerPosition = false;
|
||||
};
|
||||
|
||||
@@ -29,12 +29,13 @@ struct UIEditorMenuBarMetrics {
|
||||
float barHeight = 24.0f;
|
||||
float horizontalInset = 0.0f;
|
||||
float verticalInset = 2.0f;
|
||||
float buttonGap = 2.0f;
|
||||
float buttonPaddingX = 10.0f;
|
||||
float buttonGap = 1.0f;
|
||||
float buttonPaddingX = 4.0f;
|
||||
float labelFontSize = 13.0f;
|
||||
float estimatedGlyphWidth = 6.5f;
|
||||
float labelInsetY = -1.5f;
|
||||
float barCornerRounding = 0.0f;
|
||||
float buttonCornerRounding = 0.0f;
|
||||
float buttonCornerRounding = 0.75f;
|
||||
float baseBorderThickness = 1.0f;
|
||||
float focusedBorderThickness = 1.0f;
|
||||
float openBorderThickness = 1.0f;
|
||||
@@ -42,23 +43,23 @@ struct UIEditorMenuBarMetrics {
|
||||
|
||||
struct UIEditorMenuBarPalette {
|
||||
::XCEngine::UI::UIColor barColor =
|
||||
::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f);
|
||||
::XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonColor =
|
||||
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
::XCEngine::UI::UIColor buttonHoveredColor =
|
||||
::XCEngine::UI::UIColor(0.23f, 0.23f, 0.23f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonOpenColor =
|
||||
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f);
|
||||
::XCEngine::UI::UIColor borderColor =
|
||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
::XCEngine::UI::UIColor focusedBorderColor =
|
||||
::XCEngine::UI::UIColor(0.38f, 0.38f, 0.38f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
::XCEngine::UI::UIColor openBorderColor =
|
||||
::XCEngine::UI::UIColor(0.34f, 0.34f, 0.34f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
::XCEngine::UI::UIColor textPrimary =
|
||||
::XCEngine::UI::UIColor(0.85f, 0.85f, 0.85f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.08f, 0.08f, 0.08f, 1.0f);
|
||||
::XCEngine::UI::UIColor textMuted =
|
||||
::XCEngine::UI::UIColor(0.67f, 0.67f, 0.67f, 1.0f);
|
||||
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
::XCEngine::UI::UIColor textDisabled =
|
||||
::XCEngine::UI::UIColor(0.50f, 0.50f, 0.50f, 1.0f);
|
||||
};
|
||||
|
||||
@@ -34,19 +34,19 @@ struct UIEditorMenuPopupState {
|
||||
};
|
||||
|
||||
struct UIEditorMenuPopupMetrics {
|
||||
float contentPaddingX = 6.0f;
|
||||
float contentPaddingX = 2.0f;
|
||||
float contentPaddingY = 6.0f;
|
||||
float itemHeight = 28.0f;
|
||||
float separatorHeight = 9.0f;
|
||||
float checkColumnWidth = 18.0f;
|
||||
float shortcutGap = 20.0f;
|
||||
float submenuIndicatorWidth = 14.0f;
|
||||
float rowCornerRounding = 5.0f;
|
||||
float popupCornerRounding = 8.0f;
|
||||
float labelInsetX = 14.0f;
|
||||
float checkColumnWidth = 12.0f;
|
||||
float shortcutGap = 14.0f;
|
||||
float submenuIndicatorWidth = 10.0f;
|
||||
float rowCornerRounding = 2.5f;
|
||||
float popupCornerRounding = 4.0f;
|
||||
float labelInsetX = 4.0f;
|
||||
float labelInsetY = -1.0f;
|
||||
float labelFontSize = 13.0f;
|
||||
float shortcutInsetRight = 24.0f;
|
||||
float shortcutInsetRight = 8.0f;
|
||||
float estimatedGlyphWidth = 7.0f;
|
||||
float glyphFontSize = 12.0f;
|
||||
float separatorThickness = 1.0f;
|
||||
|
||||
@@ -110,6 +110,12 @@ struct UIEditorShellComposeFrame {
|
||||
UIEditorWorkspaceComposeFrame workspaceFrame = {};
|
||||
};
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
const std::vector<Widgets::UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorShellComposeMetrics& metrics = {});
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
||||
#include <XCEditor/Shell/UIEditorMenuModel.h>
|
||||
#include <XCEditor/Shell/UIEditorMenuSession.h>
|
||||
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
||||
@@ -50,6 +51,7 @@ struct UIEditorShellInteractionPalette {
|
||||
struct UIEditorShellInteractionServices {
|
||||
const UIEditorCommandDispatcher* commandDispatcher = nullptr;
|
||||
const UIEditorShortcutManager* shortcutManager = nullptr;
|
||||
const UIEditorTextMeasurer* textMeasurer = nullptr;
|
||||
};
|
||||
|
||||
struct UIEditorShellInteractionMenuButtonRequest {
|
||||
|
||||
@@ -105,6 +105,21 @@ public:
|
||||
UIEditorWorkspaceLayoutOperationResult SetSplitRatio(
|
||||
std::string_view nodeId,
|
||||
float splitRatio);
|
||||
UIEditorWorkspaceLayoutOperationResult ReorderTab(
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
UIEditorWorkspaceLayoutOperationResult MoveTabToStack(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
UIEditorWorkspaceLayoutOperationResult DockTabRelative(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio = 0.5f);
|
||||
UIEditorWorkspaceCommandResult Dispatch(const UIEditorWorkspaceCommand& command);
|
||||
|
||||
private:
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct UIEditorWorkspaceSession;
|
||||
|
||||
enum class UIEditorWorkspaceNodeKind : std::uint8_t {
|
||||
Panel = 0,
|
||||
TabStack,
|
||||
@@ -19,6 +21,14 @@ enum class UIEditorWorkspaceSplitAxis : std::uint8_t {
|
||||
Vertical
|
||||
};
|
||||
|
||||
enum class UIEditorWorkspaceDockPlacement : std::uint8_t {
|
||||
Center = 0,
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom
|
||||
};
|
||||
|
||||
struct UIEditorWorkspacePanelState {
|
||||
std::string panelId = {};
|
||||
std::string title = {};
|
||||
@@ -133,4 +143,28 @@ bool TrySetUIEditorWorkspaceSplitRatio(
|
||||
std::string_view nodeId,
|
||||
float splitRatio);
|
||||
|
||||
bool TryReorderUIEditorWorkspaceTab(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
|
||||
bool TryMoveUIEditorWorkspaceTabToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
|
||||
bool TryDockUIEditorWorkspaceTabRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio = 0.5f);
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
|
||||
@@ -18,8 +18,7 @@ using ::XCEngine::UI::Layout::MeasureUITabStripHeaderWidth;
|
||||
|
||||
constexpr float kTabRounding = 0.0f;
|
||||
constexpr float kStripRounding = 0.0f;
|
||||
constexpr float kHeaderFontSize = 11.0f;
|
||||
constexpr float kCloseFontSize = 10.0f;
|
||||
constexpr float kHeaderFontSize = 13.0f;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
@@ -44,10 +43,6 @@ float ResolveTabRounding(const UIEditorTabStripMetrics& metrics) {
|
||||
return kTabRounding;
|
||||
}
|
||||
|
||||
float ResolveCloseButtonRounding(const UIEditorTabStripMetrics& metrics) {
|
||||
return (std::min)(ClampNonNegative(metrics.closeButtonExtent) * 0.2f, 2.0f);
|
||||
}
|
||||
|
||||
std::size_t ResolveSelectedIndex(
|
||||
std::size_t itemCount,
|
||||
std::size_t selectedIndex) {
|
||||
@@ -78,20 +73,18 @@ float ResolveTabTextTop(
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kHeaderFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
}
|
||||
|
||||
float ResolveCloseTextTop(const UIRect& rect) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kCloseFontSize) * 0.5f) - 0.5f;
|
||||
}
|
||||
|
||||
UIColor ResolveStripBorderColor(
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
return state.focused ? palette.focusedBorderColor : palette.stripBorderColor;
|
||||
(void)state;
|
||||
return palette.stripBorderColor;
|
||||
}
|
||||
|
||||
float ResolveStripBorderThickness(
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
return state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness;
|
||||
(void)state;
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
UIColor ResolveTabFillColor(
|
||||
@@ -115,7 +108,8 @@ UIColor ResolveTabBorderColor(
|
||||
bool focused,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
if (selected) {
|
||||
return focused ? palette.focusedBorderColor : palette.tabSelectedBorderColor;
|
||||
(void)focused;
|
||||
return palette.tabSelectedBorderColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
@@ -125,33 +119,121 @@ UIColor ResolveTabBorderColor(
|
||||
return palette.tabBorderColor;
|
||||
}
|
||||
|
||||
float ResolveTabBorderThickness(
|
||||
bool selected,
|
||||
bool focused,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
float ResolveTabBorderThickness(bool selected, bool focused, const UIEditorTabStripMetrics& metrics) {
|
||||
if (selected) {
|
||||
return focused ? metrics.focusedBorderThickness : metrics.selectedBorderThickness;
|
||||
(void)focused;
|
||||
return metrics.selectedBorderThickness;
|
||||
}
|
||||
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
UIRect BuildCloseButtonRect(
|
||||
const UIRect& headerRect,
|
||||
void AppendHeaderContentSeparator(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorTabStripLayout& layout,
|
||||
const UIEditorTabStripPalette& palette,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
const float insetY = ClampNonNegative(metrics.closeInsetY);
|
||||
const float extent = (std::min)(
|
||||
ClampNonNegative(metrics.closeButtonExtent),
|
||||
(std::max)(headerRect.height - insetY * 2.0f, 0.0f));
|
||||
if (extent <= 0.0f) {
|
||||
return {};
|
||||
if (layout.headerRect.width <= 0.0f ||
|
||||
layout.headerRect.height <= 0.0f ||
|
||||
layout.contentRect.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
return UIRect(
|
||||
headerRect.x + headerRect.width - ClampNonNegative(metrics.closeInsetRight) - extent,
|
||||
headerRect.y + insetY + (std::max)(0.0f, headerRect.height - insetY * 2.0f - extent) * 0.5f,
|
||||
extent,
|
||||
extent);
|
||||
const float thickness = (std::max)(ClampNonNegative(metrics.baseBorderThickness), 1.0f);
|
||||
const float separatorY = layout.contentRect.y;
|
||||
const float separatorLeft = layout.headerRect.x;
|
||||
const float separatorRight = layout.headerRect.x + layout.headerRect.width;
|
||||
|
||||
if (layout.selectedIndex == UIEditorTabStripInvalidIndex ||
|
||||
layout.selectedIndex >= layout.tabHeaderRects.size()) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(
|
||||
separatorLeft,
|
||||
separatorY,
|
||||
(std::max)(separatorRight - separatorLeft, 0.0f),
|
||||
thickness),
|
||||
palette.headerContentSeparatorColor);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIRect& selectedRect = layout.tabHeaderRects[layout.selectedIndex];
|
||||
const float gapLeft = (std::max)(separatorLeft, selectedRect.x + 1.0f);
|
||||
const float gapRight = (std::min)(separatorRight, selectedRect.x + selectedRect.width - 1.0f);
|
||||
|
||||
if (gapLeft > separatorLeft) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(
|
||||
separatorLeft,
|
||||
separatorY,
|
||||
(std::max)(gapLeft - separatorLeft, 0.0f),
|
||||
thickness),
|
||||
palette.headerContentSeparatorColor);
|
||||
}
|
||||
|
||||
if (gapRight < separatorRight) {
|
||||
drawList.AddFilledRect(
|
||||
UIRect(
|
||||
gapRight,
|
||||
separatorY,
|
||||
(std::max)(separatorRight - gapRight, 0.0f),
|
||||
thickness),
|
||||
palette.headerContentSeparatorColor);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendSelectedTabBottomBorderMask(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
float thickness,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
if (rect.width <= 0.0f || rect.height <= 0.0f || thickness <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float maskHeight = (std::max)(1.0f, thickness + 1.0f);
|
||||
drawList.AddFilledRect(
|
||||
UIRect(
|
||||
rect.x + 1.0f,
|
||||
rect.y + rect.height - maskHeight,
|
||||
(std::max)(rect.width - 2.0f, 0.0f),
|
||||
maskHeight + 1.0f),
|
||||
palette.contentBackgroundColor,
|
||||
0.0f);
|
||||
}
|
||||
|
||||
UIEditorTabStripInsertionPreview BuildInsertionPreview(
|
||||
const UIEditorTabStripLayout& layout,
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
UIEditorTabStripInsertionPreview preview = {};
|
||||
if (!state.reorder.dragging ||
|
||||
state.reorder.previewInsertionIndex == UIEditorTabStripInvalidIndex ||
|
||||
layout.tabHeaderRects.empty() ||
|
||||
state.reorder.previewInsertionIndex > layout.tabHeaderRects.size() ||
|
||||
layout.headerRect.height <= 0.0f) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
float indicatorX = layout.tabHeaderRects.front().x;
|
||||
if (state.reorder.previewInsertionIndex >= layout.tabHeaderRects.size()) {
|
||||
const UIRect& lastRect = layout.tabHeaderRects.back();
|
||||
indicatorX = lastRect.x + lastRect.width;
|
||||
} else {
|
||||
indicatorX = layout.tabHeaderRects[state.reorder.previewInsertionIndex].x;
|
||||
}
|
||||
|
||||
const float thickness = (std::max)(ClampNonNegative(metrics.reorderPreviewThickness), 1.0f);
|
||||
const float insetY = ClampNonNegative(metrics.reorderPreviewInsetY);
|
||||
const float indicatorHeight = (std::max)(layout.headerRect.height - insetY * 2.0f, thickness);
|
||||
|
||||
preview.visible = true;
|
||||
preview.insertionIndex = state.reorder.previewInsertionIndex;
|
||||
preview.indicatorRect = UIRect(
|
||||
indicatorX - thickness * 0.5f,
|
||||
layout.headerRect.y + insetY,
|
||||
thickness,
|
||||
indicatorHeight);
|
||||
return preview;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -162,13 +244,8 @@ float ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
const float labelWidth = ResolveEstimatedLabelWidth(item, metrics);
|
||||
const float horizontalPadding = ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||
const float extraLeftInset = (std::max)(ClampNonNegative(metrics.labelInsetX) - horizontalPadding, 0.0f);
|
||||
const float extraRightInset = (std::max)(ClampNonNegative(metrics.closeInsetRight) - horizontalPadding, 0.0f);
|
||||
const float closeBudget = item.closable
|
||||
? ClampNonNegative(metrics.closeButtonExtent) +
|
||||
ClampNonNegative(metrics.closeButtonGap) +
|
||||
extraRightInset
|
||||
: 0.0f;
|
||||
return labelWidth + extraLeftInset + closeBudget;
|
||||
(void)item;
|
||||
return labelWidth + extraLeftInset;
|
||||
}
|
||||
|
||||
std::size_t ResolveUIEditorTabStripSelectedIndex(
|
||||
@@ -188,34 +265,6 @@ std::size_t ResolveUIEditorTabStripSelectedIndex(
|
||||
return ResolveSelectedIndex(items.size(), fallbackIndex);
|
||||
}
|
||||
|
||||
std::size_t ResolveUIEditorTabStripSelectedIndexAfterClose(
|
||||
std::size_t selectedIndex,
|
||||
std::size_t closedIndex,
|
||||
std::size_t itemCountBeforeClose) {
|
||||
if (itemCountBeforeClose == 0u || closedIndex >= itemCountBeforeClose) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
const std::size_t itemCountAfterClose = itemCountBeforeClose - 1u;
|
||||
if (itemCountAfterClose == 0u) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
if (selectedIndex == UIEditorTabStripInvalidIndex || selectedIndex >= itemCountBeforeClose) {
|
||||
return (std::min)(closedIndex, itemCountAfterClose - 1u);
|
||||
}
|
||||
|
||||
if (closedIndex < selectedIndex) {
|
||||
return selectedIndex - 1u;
|
||||
}
|
||||
|
||||
if (closedIndex > selectedIndex) {
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
return (std::min)(selectedIndex, itemCountAfterClose - 1u);
|
||||
}
|
||||
|
||||
UIEditorTabStripLayout BuildUIEditorTabStripLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorTabStripItem>& items,
|
||||
@@ -245,17 +294,7 @@ UIEditorTabStripLayout BuildUIEditorTabStripLayout(
|
||||
layout.tabHeaderRects = arranged.tabHeaderRects;
|
||||
layout.closeButtonRects.resize(items.size());
|
||||
layout.showCloseButtons.resize(items.size(), false);
|
||||
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
if (!items[index].closable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
layout.closeButtonRects[index] = BuildCloseButtonRect(layout.tabHeaderRects[index], metrics);
|
||||
layout.showCloseButtons[index] =
|
||||
layout.closeButtonRects[index].width > 0.0f && layout.closeButtonRects[index].height > 0.0f;
|
||||
}
|
||||
|
||||
layout.insertionPreview = BuildInsertionPreview(layout, state, metrics);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -269,15 +308,6 @@ UIEditorTabStripHitTarget HitTestUIEditorTabStrip(
|
||||
return target;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.closeButtonRects.size(); ++index) {
|
||||
if (layout.showCloseButtons[index] &&
|
||||
IsPointInsideRect(layout.closeButtonRects[index], point)) {
|
||||
target.kind = UIEditorTabStripHitTargetKind::CloseButton;
|
||||
target.index = index;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
||||
if (IsPointInsideRect(layout.tabHeaderRects[index], point)) {
|
||||
target.kind = UIEditorTabStripHitTargetKind::Tab;
|
||||
@@ -322,7 +352,7 @@ void AppendUIEditorTabStripBackground(
|
||||
|
||||
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
||||
const bool selected = layout.selectedIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index || state.closeHoveredIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
drawList.AddFilledRect(
|
||||
layout.tabHeaderRects[index],
|
||||
ResolveTabFillColor(selected, hovered, palette),
|
||||
@@ -332,6 +362,20 @@ void AppendUIEditorTabStripBackground(
|
||||
ResolveTabBorderColor(selected, hovered, state.focused, palette),
|
||||
ResolveTabBorderThickness(selected, state.focused, metrics),
|
||||
tabRounding);
|
||||
if (selected) {
|
||||
AppendSelectedTabBottomBorderMask(
|
||||
drawList,
|
||||
layout.tabHeaderRects[index],
|
||||
ResolveTabBorderThickness(selected, state.focused, metrics),
|
||||
palette);
|
||||
}
|
||||
}
|
||||
|
||||
if (layout.insertionPreview.visible) {
|
||||
drawList.AddFilledRect(
|
||||
layout.insertionPreview.indicatorRect,
|
||||
palette.reorderPreviewColor,
|
||||
0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +386,8 @@ void AppendUIEditorTabStripForeground(
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripPalette& palette,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
AppendHeaderContentSeparator(drawList, layout, palette, metrics);
|
||||
|
||||
const float leftInset = (std::max)(
|
||||
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
|
||||
ClampNonNegative(metrics.labelInsetX));
|
||||
@@ -349,12 +395,9 @@ void AppendUIEditorTabStripForeground(
|
||||
for (std::size_t index = 0; index < items.size() && index < layout.tabHeaderRects.size(); ++index) {
|
||||
const UIRect& tabRect = layout.tabHeaderRects[index];
|
||||
const bool selected = layout.selectedIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index || state.closeHoveredIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
const float textLeft = tabRect.x + leftInset;
|
||||
float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||
if (layout.showCloseButtons[index]) {
|
||||
textRight = layout.closeButtonRects[index].x - ClampNonNegative(metrics.closeButtonGap);
|
||||
}
|
||||
const float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||
|
||||
if (textRight > textLeft) {
|
||||
const UIRect clipRect(
|
||||
@@ -370,30 +413,6 @@ void AppendUIEditorTabStripForeground(
|
||||
kHeaderFontSize);
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
|
||||
if (!layout.showCloseButtons[index]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool closeHovered = state.closeHoveredIndex == index;
|
||||
const UIRect& closeRect = layout.closeButtonRects[index];
|
||||
const float closeRounding = ResolveCloseButtonRounding(metrics);
|
||||
drawList.AddFilledRect(
|
||||
closeRect,
|
||||
closeHovered ? palette.closeButtonHoveredColor : palette.closeButtonColor,
|
||||
closeRounding);
|
||||
drawList.AddRectOutline(
|
||||
closeRect,
|
||||
palette.closeButtonBorderColor,
|
||||
1.0f,
|
||||
closeRounding);
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
closeRect.x + (std::max)(0.0f, (closeRect.width - 7.0f) * 0.5f),
|
||||
ResolveCloseTextTop(closeRect)),
|
||||
"X",
|
||||
palette.closeGlyphColor,
|
||||
kCloseFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
@@ -12,6 +13,16 @@ using ::XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::Widgets::BeginUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::CancelUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::EndUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropOperation;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropPayload;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropResult;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropSourceDescriptor;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropTargetDescriptor;
|
||||
using ::XCEngine::UI::Widgets::UpdateUIDragDropPointer;
|
||||
using ::XCEngine::UI::Widgets::UpdateUIDragDropTarget;
|
||||
using Widgets::BuildUIEditorTabStripLayout;
|
||||
using Widgets::HitTestUIEditorTabStrip;
|
||||
using Widgets::ResolveUIEditorTabStripSelectedIndex;
|
||||
@@ -19,6 +30,9 @@ using Widgets::UIEditorTabStripHitTarget;
|
||||
using Widgets::UIEditorTabStripHitTargetKind;
|
||||
using Widgets::UIEditorTabStripInvalidIndex;
|
||||
|
||||
constexpr ::XCEngine::UI::UIElementId kTabStripDragOwnerId = 0x58435441u;
|
||||
constexpr std::string_view kTabStripDragPayloadType = "xc.editor.tab";
|
||||
|
||||
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
@@ -53,6 +67,14 @@ void ClearHoverState(UIEditorTabStripInteractionState& state) {
|
||||
state.tabStripState.closeHoveredIndex = UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
void ClearReorderState(UIEditorTabStripInteractionState& state) {
|
||||
state.tabStripState.reorder = {};
|
||||
state.reorderDragState = {};
|
||||
state.reorderSourceIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderCaptureActive = false;
|
||||
}
|
||||
|
||||
void SyncHoverTarget(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
const Widgets::UIEditorTabStripLayout& layout) {
|
||||
@@ -68,11 +90,6 @@ void SyncHoverTarget(
|
||||
}
|
||||
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorTabStripHitTargetKind::CloseButton:
|
||||
state.tabStripState.hoveredIndex = hitTarget.index;
|
||||
state.tabStripState.closeHoveredIndex = hitTarget.index;
|
||||
break;
|
||||
|
||||
case UIEditorTabStripHitTargetKind::Tab:
|
||||
state.tabStripState.hoveredIndex = hitTarget.index;
|
||||
break;
|
||||
@@ -145,6 +162,205 @@ bool ApplyKeyboardNavigation(
|
||||
}
|
||||
}
|
||||
|
||||
bool HasReorderInteraction(const UIEditorTabStripInteractionState& state) {
|
||||
return state.reorderDragState.armed || state.reorderDragState.active;
|
||||
}
|
||||
|
||||
std::size_t ResolveVisibleDropInsertionIndex(
|
||||
const Widgets::UIEditorTabStripLayout& layout,
|
||||
const ::XCEngine::UI::UIPoint& pointerPosition) {
|
||||
if (!IsPointInside(layout.headerRect, pointerPosition)) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t insertionIndex = 0u;
|
||||
for (const ::XCEngine::UI::UIRect& rect : layout.tabHeaderRects) {
|
||||
const float midpoint = rect.x + rect.width * 0.5f;
|
||||
if (pointerPosition.x > midpoint) {
|
||||
++insertionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return insertionIndex;
|
||||
}
|
||||
|
||||
std::size_t ResolveReorderTargetIndex(
|
||||
std::size_t sourceIndex,
|
||||
std::size_t dropInsertionIndex,
|
||||
std::size_t itemCount) {
|
||||
if (sourceIndex >= itemCount || dropInsertionIndex > itemCount) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
if (dropInsertionIndex <= sourceIndex) {
|
||||
return dropInsertionIndex;
|
||||
}
|
||||
|
||||
return dropInsertionIndex - 1u;
|
||||
}
|
||||
|
||||
std::size_t ResolveDropInsertionIndexFromPreviewTargetIndex(
|
||||
std::size_t sourceIndex,
|
||||
std::size_t previewTargetIndex) {
|
||||
if (sourceIndex == UIEditorTabStripInvalidIndex ||
|
||||
previewTargetIndex == UIEditorTabStripInvalidIndex) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
return previewTargetIndex <= sourceIndex
|
||||
? previewTargetIndex
|
||||
: previewTargetIndex + 1u;
|
||||
}
|
||||
|
||||
std::size_t ResolveCommittedReorderInsertionIndex(
|
||||
const UIEditorTabStripInteractionState& state,
|
||||
const Widgets::UIEditorTabStripLayout& layout) {
|
||||
const std::size_t previewInsertionIndex =
|
||||
ResolveDropInsertionIndexFromPreviewTargetIndex(
|
||||
state.reorderSourceIndex,
|
||||
state.reorderPreviewIndex);
|
||||
if (previewInsertionIndex != UIEditorTabStripInvalidIndex) {
|
||||
return previewInsertionIndex;
|
||||
}
|
||||
|
||||
if (state.tabStripState.reorder.previewInsertionIndex != UIEditorTabStripInvalidIndex) {
|
||||
return state.tabStripState.reorder.previewInsertionIndex;
|
||||
}
|
||||
|
||||
if (!state.hasPointerPosition) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
return ResolveVisibleDropInsertionIndex(layout, state.pointerPosition);
|
||||
}
|
||||
|
||||
void SyncReorderPreview(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
const Widgets::UIEditorTabStripLayout& layout,
|
||||
const std::vector<Widgets::UIEditorTabStripItem>& items,
|
||||
UIEditorTabStripInteractionResult& result) {
|
||||
state.tabStripState.reorder.armed = state.reorderDragState.armed;
|
||||
state.tabStripState.reorder.dragging = state.reorderDragState.active;
|
||||
state.tabStripState.reorder.sourceIndex = state.reorderSourceIndex;
|
||||
state.tabStripState.reorder.pressedIndex = state.reorderSourceIndex;
|
||||
state.tabStripState.reorder.pressPosition = state.reorderDragState.pointerDownPosition;
|
||||
result.dragSourceIndex = state.reorderSourceIndex;
|
||||
if (state.reorderSourceIndex < items.size()) {
|
||||
result.draggedTabId = items[state.reorderSourceIndex].tabId;
|
||||
}
|
||||
|
||||
if (!state.reorderDragState.active || !state.hasPointerPosition || items.size() < 2u) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
UpdateUIDragDropTarget(state.reorderDragState, nullptr, &dragDropResult);
|
||||
state.tabStripState.reorder.previewInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
result.dropInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
result.reorderToIndex = UIEditorTabStripInvalidIndex;
|
||||
result.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t dropInsertionIndex =
|
||||
ResolveVisibleDropInsertionIndex(layout, state.pointerPosition);
|
||||
if (dropInsertionIndex == UIEditorTabStripInvalidIndex) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
UpdateUIDragDropTarget(state.reorderDragState, nullptr, &dragDropResult);
|
||||
state.tabStripState.reorder.previewInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
result.dropInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
result.reorderToIndex = UIEditorTabStripInvalidIndex;
|
||||
result.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t reorderToIndex =
|
||||
ResolveReorderTargetIndex(
|
||||
state.reorderSourceIndex,
|
||||
dropInsertionIndex,
|
||||
items.size());
|
||||
if (reorderToIndex == UIEditorTabStripInvalidIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr std::array<std::string_view, 1> kAcceptedPayloadTypes = {
|
||||
kTabStripDragPayloadType
|
||||
};
|
||||
const std::string targetId = "insert:" + std::to_string(dropInsertionIndex);
|
||||
UIDragDropTargetDescriptor target = {};
|
||||
target.ownerId = kTabStripDragOwnerId;
|
||||
target.targetId = targetId;
|
||||
target.acceptedPayloadTypes = kAcceptedPayloadTypes;
|
||||
target.acceptedOperations = UIDragDropOperation::Move;
|
||||
target.preferredOperation = UIDragDropOperation::Move;
|
||||
|
||||
UIDragDropResult dragDropResult = {};
|
||||
UpdateUIDragDropTarget(state.reorderDragState, &target, &dragDropResult);
|
||||
|
||||
state.tabStripState.reorder.previewInsertionIndex = dropInsertionIndex;
|
||||
state.reorderPreviewIndex = reorderToIndex;
|
||||
result.reorderPreviewActive = true;
|
||||
result.dropInsertionIndex = dropInsertionIndex;
|
||||
result.reorderToIndex = reorderToIndex;
|
||||
result.reorderPreviewIndex = reorderToIndex;
|
||||
}
|
||||
|
||||
void BeginTabReorder(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
const std::vector<Widgets::UIEditorTabStripItem>& items,
|
||||
std::size_t sourceIndex) {
|
||||
if (sourceIndex >= items.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIDragDropSourceDescriptor source = {};
|
||||
source.ownerId = kTabStripDragOwnerId;
|
||||
source.sourceId = items[sourceIndex].tabId;
|
||||
source.pointerDownPosition = state.pointerPosition;
|
||||
source.payload = UIDragDropPayload{
|
||||
std::string(kTabStripDragPayloadType),
|
||||
items[sourceIndex].tabId,
|
||||
items[sourceIndex].title
|
||||
};
|
||||
source.allowedOperations = UIDragDropOperation::Move;
|
||||
source.activationDistance = 4.0f;
|
||||
|
||||
UIDragDropResult dragDropResult = {};
|
||||
if (BeginUIDragDrop(source, state.reorderDragState, &dragDropResult)) {
|
||||
state.tabStripState.reorder.pressPosition = state.pointerPosition;
|
||||
state.tabStripState.reorder.pressedIndex = sourceIndex;
|
||||
state.tabStripState.reorder.sourceIndex = sourceIndex;
|
||||
state.tabStripState.reorder.previewInsertionIndex = UIEditorTabStripInvalidIndex;
|
||||
state.tabStripState.reorder.armed = true;
|
||||
state.tabStripState.reorder.dragging = false;
|
||||
state.reorderSourceIndex = sourceIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void CancelTabReorder(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
UIEditorTabStripInteractionResult& result,
|
||||
const std::vector<Widgets::UIEditorTabStripItem>& items) {
|
||||
if (!HasReorderInteraction(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.dragCanceled = true;
|
||||
result.consumed = state.reorderCaptureActive || state.reorderDragState.active;
|
||||
result.dragSourceIndex = state.reorderSourceIndex;
|
||||
result.reorderPreviewIndex = state.reorderPreviewIndex;
|
||||
if (state.reorderSourceIndex < items.size()) {
|
||||
result.draggedTabId = items[state.reorderSourceIndex].tabId;
|
||||
}
|
||||
if (state.reorderCaptureActive) {
|
||||
result.releasePointerCapture = true;
|
||||
}
|
||||
|
||||
UIDragDropResult dragDropResult = {};
|
||||
CancelUIDragDrop(state.reorderDragState, &dragDropResult);
|
||||
ClearReorderState(state);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
@@ -184,11 +400,33 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
state.hasPointerPosition = false;
|
||||
state.pressedTarget = {};
|
||||
ClearHoverState(state);
|
||||
CancelTabReorder(state, eventResult, items);
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
if (HasReorderInteraction(state)) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
UpdateUIDragDropPointer(state.reorderDragState, state.pointerPosition, &dragDropResult);
|
||||
if (dragDropResult.activated && !state.reorderCaptureActive) {
|
||||
state.reorderCaptureActive = true;
|
||||
eventResult.requestPointerCapture = true;
|
||||
eventResult.dragStarted = true;
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
|
||||
SyncReorderPreview(state, layout, items, eventResult);
|
||||
if (state.reorderDragState.active) {
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerLeave:
|
||||
if (state.reorderDragState.active) {
|
||||
SyncReorderPreview(state, layout, items, eventResult);
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonDown: {
|
||||
@@ -201,6 +439,9 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
(state.hasPointerPosition && IsPointInside(layout.bounds, state.pointerPosition))) {
|
||||
state.tabStripState.focused = true;
|
||||
eventResult.consumed = true;
|
||||
if (eventResult.hitTarget.kind == UIEditorTabStripHitTargetKind::Tab) {
|
||||
BeginTabReorder(state, items, eventResult.hitTarget.index);
|
||||
}
|
||||
} else {
|
||||
state.tabStripState.focused = false;
|
||||
state.pressedTarget = {};
|
||||
@@ -218,22 +459,49 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
const bool matchedPressedTarget =
|
||||
AreEquivalentTargets(state.pressedTarget, eventResult.hitTarget);
|
||||
|
||||
if (state.reorderDragState.active || state.reorderCaptureActive) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
EndUIDragDrop(state.reorderDragState, dragDropResult);
|
||||
|
||||
eventResult.dragEnded = state.reorderCaptureActive;
|
||||
eventResult.releasePointerCapture = state.reorderCaptureActive;
|
||||
eventResult.consumed = true;
|
||||
eventResult.dragSourceIndex = state.reorderSourceIndex;
|
||||
if (state.reorderSourceIndex < items.size()) {
|
||||
eventResult.draggedTabId = items[state.reorderSourceIndex].tabId;
|
||||
}
|
||||
eventResult.dropInsertionIndex =
|
||||
ResolveCommittedReorderInsertionIndex(state, layout);
|
||||
eventResult.reorderToIndex =
|
||||
ResolveReorderTargetIndex(
|
||||
state.reorderSourceIndex,
|
||||
eventResult.dropInsertionIndex,
|
||||
items.size());
|
||||
eventResult.reorderPreviewIndex = state.reorderPreviewIndex;
|
||||
if (dragDropResult.completed &&
|
||||
eventResult.dropInsertionIndex != UIEditorTabStripInvalidIndex &&
|
||||
eventResult.reorderToIndex != UIEditorTabStripInvalidIndex) {
|
||||
eventResult.reorderRequested =
|
||||
eventResult.dropInsertionIndex != state.reorderSourceIndex &&
|
||||
eventResult.dropInsertionIndex != state.reorderSourceIndex + 1u;
|
||||
} else {
|
||||
eventResult.dragCanceled = true;
|
||||
}
|
||||
|
||||
ClearReorderState(state);
|
||||
state.pressedTarget = {};
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.reorderDragState.armed) {
|
||||
UIDragDropResult dragDropResult = {};
|
||||
CancelUIDragDrop(state.reorderDragState, &dragDropResult);
|
||||
state.reorderSourceIndex = UIEditorTabStripInvalidIndex;
|
||||
state.reorderPreviewIndex = UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
if (matchedPressedTarget) {
|
||||
switch (eventResult.hitTarget.kind) {
|
||||
case UIEditorTabStripHitTargetKind::CloseButton:
|
||||
if (eventResult.hitTarget.index < items.size() &&
|
||||
items[eventResult.hitTarget.index].closable) {
|
||||
eventResult.closeRequested = true;
|
||||
eventResult.closedTabId = items[eventResult.hitTarget.index].tabId;
|
||||
eventResult.closedIndex = eventResult.hitTarget.index;
|
||||
eventResult.consumed = true;
|
||||
state.tabStripState.focused = true;
|
||||
} else if (insideStrip) {
|
||||
state.tabStripState.focused = true;
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIEditorTabStripHitTargetKind::Tab:
|
||||
SelectTab(
|
||||
state,
|
||||
@@ -271,6 +539,12 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
}
|
||||
|
||||
case UIInputEventType::KeyDown:
|
||||
if (HasReorderInteraction(state) &&
|
||||
static_cast<KeyCode>(event.keyCode) == KeyCode::Escape) {
|
||||
CancelTabReorder(state, eventResult, items);
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.tabStripState.focused &&
|
||||
!HasNavigationModifiers(event.modifiers) &&
|
||||
ApplyKeyboardNavigation(state, event.keyCode) &&
|
||||
@@ -299,13 +573,25 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition);
|
||||
}
|
||||
|
||||
if (state.reorderDragState.active) {
|
||||
SyncReorderPreview(state, layout, items, eventResult);
|
||||
}
|
||||
|
||||
if (eventResult.consumed ||
|
||||
eventResult.selectionChanged ||
|
||||
eventResult.closeRequested ||
|
||||
eventResult.keyboardNavigated ||
|
||||
eventResult.requestPointerCapture ||
|
||||
eventResult.releasePointerCapture ||
|
||||
eventResult.dragStarted ||
|
||||
eventResult.dragEnded ||
|
||||
eventResult.dragCanceled ||
|
||||
eventResult.reorderRequested ||
|
||||
eventResult.reorderPreviewActive ||
|
||||
eventResult.hitTarget.kind != UIEditorTabStripHitTargetKind::None ||
|
||||
!eventResult.selectedTabId.empty() ||
|
||||
!eventResult.closedTabId.empty()) {
|
||||
!eventResult.closedTabId.empty() ||
|
||||
!eventResult.draggedTabId.empty()) {
|
||||
interactionResult = std::move(eventResult);
|
||||
}
|
||||
}
|
||||
@@ -318,6 +604,9 @@ UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
interactionResult.hitTarget =
|
||||
HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition);
|
||||
}
|
||||
if (state.reorderDragState.active) {
|
||||
SyncReorderPreview(state, layout, items, interactionResult);
|
||||
}
|
||||
|
||||
return {
|
||||
std::move(layout),
|
||||
|
||||
130
new_editor/src/Foundation/UIEditorRuntimeTrace.cpp
Normal file
130
new_editor/src/Foundation/UIEditorRuntimeTrace.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex g_traceMutex = {};
|
||||
std::filesystem::path g_logRoot = {};
|
||||
std::filesystem::path g_runtimeTracePath = {};
|
||||
std::filesystem::path g_crashTracePath = {};
|
||||
bool g_traceInitialized = false;
|
||||
|
||||
std::string BuildTimestampString() {
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const std::time_t currentTime = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm localTime = {};
|
||||
localtime_s(&localTime, ¤tTime);
|
||||
|
||||
const auto milliseconds =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch()) %
|
||||
1000;
|
||||
|
||||
std::ostringstream stream = {};
|
||||
stream << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S")
|
||||
<< '.'
|
||||
<< std::setw(3)
|
||||
<< std::setfill('0')
|
||||
<< milliseconds.count();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
void AppendTraceLine(
|
||||
const std::filesystem::path& path,
|
||||
std::string_view channel,
|
||||
std::string_view message) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(path.parent_path(), errorCode);
|
||||
std::ofstream stream(path, std::ios::out | std::ios::app);
|
||||
if (!stream.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
stream << '['
|
||||
<< BuildTimestampString()
|
||||
<< "] ["
|
||||
<< channel
|
||||
<< "] "
|
||||
<< message
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void InitializeUIEditorRuntimeTrace(const std::filesystem::path& logRoot) {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
g_logRoot = logRoot.lexically_normal();
|
||||
g_runtimeTracePath = (g_logRoot / "runtime.log").lexically_normal();
|
||||
g_crashTracePath = (g_logRoot / "crash.log").lexically_normal();
|
||||
g_traceInitialized = true;
|
||||
|
||||
std::error_code errorCode = {};
|
||||
std::filesystem::create_directories(g_logRoot, errorCode);
|
||||
AppendTraceLine(g_runtimeTracePath, "trace", "trace session started");
|
||||
}
|
||||
|
||||
void ShutdownUIEditorRuntimeTrace() {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
if (!g_traceInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppendTraceLine(g_runtimeTracePath, "trace", "trace session ended");
|
||||
g_traceInitialized = false;
|
||||
}
|
||||
|
||||
void AppendUIEditorRuntimeTrace(
|
||||
std::string_view channel,
|
||||
std::string_view message) {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
if (!g_traceInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppendTraceLine(g_runtimeTracePath, channel, message);
|
||||
}
|
||||
|
||||
void AppendUIEditorCrashTrace(
|
||||
std::uint32_t exceptionCode,
|
||||
const void* exceptionAddress) {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
if (!g_traceInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[128] = {};
|
||||
std::snprintf(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
"Unhandled exception code=0x%08X address=%p",
|
||||
exceptionCode,
|
||||
exceptionAddress);
|
||||
AppendTraceLine(g_crashTracePath, "crash", buffer);
|
||||
AppendTraceLine(g_runtimeTracePath, "crash", buffer);
|
||||
}
|
||||
|
||||
std::filesystem::path GetUIEditorRuntimeTracePath() {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
return g_runtimeTracePath;
|
||||
}
|
||||
|
||||
std::filesystem::path GetUIEditorCrashTracePath() {
|
||||
std::lock_guard lock(g_traceMutex);
|
||||
return g_crashTracePath;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -501,6 +501,91 @@ UIColor ResolveSplitterColor(const UIEditorDockHostSplitterLayout& splitter, con
|
||||
return palette.splitterColor;
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackLayoutByNodeId(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIRect InsetRect(const UIRect& rect, float inset) {
|
||||
const float clampedInset = ClampNonNegative(inset);
|
||||
const float insetX = (std::min)(clampedInset, rect.width * 0.5f);
|
||||
const float insetY = (std::min)(clampedInset, rect.height * 0.5f);
|
||||
return UIRect(
|
||||
rect.x + insetX,
|
||||
rect.y + insetY,
|
||||
(std::max)(0.0f, rect.width - insetX * 2.0f),
|
||||
(std::max)(0.0f, rect.height - insetY * 2.0f));
|
||||
}
|
||||
|
||||
UIRect ResolveDropPreviewRect(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
UIEditorWorkspaceDockPlacement placement) {
|
||||
const UIRect bounds = tabStack.bounds;
|
||||
switch (placement) {
|
||||
case UIEditorWorkspaceDockPlacement::Left:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width * 0.35f,
|
||||
bounds.height);
|
||||
case UIEditorWorkspaceDockPlacement::Right:
|
||||
return UIRect(
|
||||
bounds.x + bounds.width * 0.65f,
|
||||
bounds.y,
|
||||
bounds.width * 0.35f,
|
||||
bounds.height);
|
||||
case UIEditorWorkspaceDockPlacement::Top:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width,
|
||||
bounds.height * 0.35f);
|
||||
case UIEditorWorkspaceDockPlacement::Bottom:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y + bounds.height * 0.65f,
|
||||
bounds.width,
|
||||
bounds.height * 0.35f);
|
||||
case UIEditorWorkspaceDockPlacement::Center:
|
||||
default:
|
||||
return InsetRect(bounds, 4.0f);
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorDockHostDropPreviewLayout ResolveDropPreviewLayout(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostState& state) {
|
||||
UIEditorDockHostDropPreviewLayout preview = {};
|
||||
if (!state.dropPreview.visible || state.dropPreview.targetNodeId.empty()) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* targetTabStack =
|
||||
FindTabStackLayoutByNodeId(layout, state.dropPreview.targetNodeId);
|
||||
if (targetTabStack == nullptr) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
preview.visible = true;
|
||||
preview.targetNodeId = state.dropPreview.targetNodeId;
|
||||
preview.placement = state.dropPreview.placement;
|
||||
preview.insertionIndex = state.dropPreview.insertionIndex;
|
||||
preview.previewRect =
|
||||
ResolveDropPreviewRect(*targetTabStack, state.dropPreview.placement);
|
||||
if (preview.previewRect.width <= 0.0f || preview.previewRect.height <= 0.0f) {
|
||||
preview = {};
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
|
||||
@@ -539,6 +624,7 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
|
||||
state,
|
||||
metrics,
|
||||
layout);
|
||||
layout.dropPreview = ResolveDropPreviewLayout(layout, state);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -607,8 +693,6 @@ void AppendUIEditorDockHostBackground(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
||||
BuildTabContentFrameMetrics(metrics);
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||
tabItems.reserve(tabStack.items.size());
|
||||
@@ -626,12 +710,6 @@ void AppendUIEditorDockHostBackground(
|
||||
tabStack.tabStripState,
|
||||
palette.tabStripPalette,
|
||||
metrics.tabStripMetrics);
|
||||
AppendUIEditorPanelFrameBackground(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
palette.panelFramePalette,
|
||||
tabContentFrameMetrics);
|
||||
}
|
||||
|
||||
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
|
||||
@@ -648,8 +726,6 @@ void AppendUIEditorDockHostForeground(
|
||||
const UIEditorDockHostForegroundOptions& options,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
||||
BuildTabContentFrameMetrics(metrics);
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||
tabItems.reserve(tabStack.items.size());
|
||||
@@ -668,18 +744,21 @@ void AppendUIEditorDockHostForeground(
|
||||
tabStack.tabStripState,
|
||||
palette.tabStripPalette,
|
||||
metrics.tabStripMetrics);
|
||||
AppendUIEditorPanelFrameForeground(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
{},
|
||||
palette.panelFramePalette,
|
||||
tabContentFrameMetrics);
|
||||
|
||||
if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (layout.dropPreview.visible) {
|
||||
drawList.AddFilledRect(
|
||||
layout.dropPreview.previewRect,
|
||||
palette.dropPreviewFillColor);
|
||||
drawList.AddRectOutline(
|
||||
layout.dropPreview.previewRect,
|
||||
palette.dropPreviewBorderColor,
|
||||
1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorDockHostForeground(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -13,6 +15,7 @@ namespace {
|
||||
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Widgets::BeginUISplitterDrag;
|
||||
using ::XCEngine::UI::Widgets::EndUISplitterDrag;
|
||||
@@ -30,8 +33,17 @@ using Widgets::UIEditorTabStripItem;
|
||||
struct DockHostTabStripEventResult {
|
||||
bool consumed = false;
|
||||
bool commandRequested = false;
|
||||
bool reorderRequested = false;
|
||||
bool dragStarted = false;
|
||||
bool dragEnded = false;
|
||||
bool dragCanceled = false;
|
||||
bool requestPointerCapture = false;
|
||||
bool releasePointerCapture = false;
|
||||
UIEditorWorkspaceCommandKind commandKind = UIEditorWorkspaceCommandKind::ActivatePanel;
|
||||
std::size_t dropInsertionIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::string panelId = {};
|
||||
std::string nodeId = {};
|
||||
std::string draggedTabId = {};
|
||||
UIEditorDockHostHitTarget hitTarget = {};
|
||||
int priority = 0;
|
||||
};
|
||||
@@ -130,6 +142,13 @@ void PruneTabStripInteractionEntries(
|
||||
return !isVisibleNodeId(entry.nodeId);
|
||||
}),
|
||||
state.dockHostState.tabStripStates.end());
|
||||
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
!isVisibleNodeId(state.activeTabDragNodeId)) {
|
||||
state.activeTabDragNodeId.clear();
|
||||
state.activeTabDragPanelId.clear();
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
}
|
||||
|
||||
void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state) {
|
||||
@@ -152,6 +171,33 @@ bool HasFocusedTabStrip(const UIEditorDockHostInteractionState& state) {
|
||||
}) != state.tabStripInteractions.end();
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackLayoutByNodeId(
|
||||
const Widgets::UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsPointInsideRect(
|
||||
const UIRect& rect,
|
||||
const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
void ClearTabDockDragState(UIEditorDockHostInteractionState& state) {
|
||||
state.activeTabDragNodeId.clear();
|
||||
state.activeTabDragPanelId.clear();
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
|
||||
std::vector<UIEditorTabStripItem> BuildTabStripItems(
|
||||
const UIEditorDockHostTabStackLayout& tabStack) {
|
||||
std::vector<UIEditorTabStripItem> items = {};
|
||||
@@ -198,6 +244,13 @@ UIEditorDockHostHitTarget MapTabStripHitTarget(
|
||||
}
|
||||
|
||||
int ResolveTabStripPriority(const UIEditorTabStripInteractionResult& result) {
|
||||
if (result.reorderRequested ||
|
||||
result.dragStarted ||
|
||||
result.dragEnded ||
|
||||
result.dragCanceled) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (result.closeRequested) {
|
||||
return 4;
|
||||
}
|
||||
@@ -221,6 +274,11 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
DockHostTabStripEventResult resolved = {};
|
||||
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
tabStack.nodeId != state.activeTabDragNodeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorDockHostTabStripInteractionEntry& entry =
|
||||
FindOrCreateTabStripInteractionEntry(state, tabStack.nodeId);
|
||||
std::string selectedTabId = tabStack.selectedPanelId;
|
||||
@@ -238,7 +296,15 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
continue;
|
||||
}
|
||||
|
||||
resolved.nodeId = tabStack.nodeId;
|
||||
resolved.hitTarget = MapTabStripHitTarget(tabStack, frame.result);
|
||||
resolved.requestPointerCapture = frame.result.requestPointerCapture;
|
||||
resolved.releasePointerCapture = frame.result.releasePointerCapture;
|
||||
resolved.dragStarted = frame.result.dragStarted;
|
||||
resolved.dragEnded = frame.result.dragEnded;
|
||||
resolved.dragCanceled = frame.result.dragCanceled;
|
||||
resolved.dropInsertionIndex = frame.result.dropInsertionIndex;
|
||||
resolved.draggedTabId = frame.result.draggedTabId;
|
||||
if ((frame.result.closeRequested && !frame.result.closedTabId.empty()) ||
|
||||
(event.type == UIInputEventType::PointerButtonUp &&
|
||||
frame.result.consumed &&
|
||||
@@ -250,6 +316,12 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
!frame.result.closedTabId.empty()
|
||||
? frame.result.closedTabId
|
||||
: resolved.hitTarget.panelId;
|
||||
} else if (frame.result.reorderRequested &&
|
||||
!frame.result.draggedTabId.empty() &&
|
||||
frame.result.dropInsertionIndex != Widgets::UIEditorTabStripInvalidIndex) {
|
||||
resolved.reorderRequested = true;
|
||||
resolved.panelId = frame.result.draggedTabId;
|
||||
resolved.dropInsertionIndex = frame.result.dropInsertionIndex;
|
||||
} else if ((frame.result.selectionChanged ||
|
||||
frame.result.keyboardNavigated ||
|
||||
(event.type == UIInputEventType::PointerButtonUp &&
|
||||
@@ -266,6 +338,7 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
continue;
|
||||
} else {
|
||||
resolved.commandRequested = false;
|
||||
resolved.reorderRequested = false;
|
||||
resolved.panelId.clear();
|
||||
}
|
||||
|
||||
@@ -277,6 +350,103 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
return resolved;
|
||||
}
|
||||
|
||||
std::size_t ResolveTabHeaderDropInsertionIndex(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
const UIPoint& point) {
|
||||
if (!IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) {
|
||||
return Widgets::UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t insertionIndex = 0u;
|
||||
for (const UIRect& rect : tabStack.tabStripLayout.tabHeaderRects) {
|
||||
const float midpoint = rect.x + rect.width * 0.5f;
|
||||
if (point.x > midpoint) {
|
||||
++insertionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return insertionIndex;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceDockPlacement ResolveDockPlacement(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
const UIPoint& point) {
|
||||
if (IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) {
|
||||
return UIEditorWorkspaceDockPlacement::Center;
|
||||
}
|
||||
|
||||
const float leftDistance = point.x - tabStack.bounds.x;
|
||||
const float rightDistance =
|
||||
tabStack.bounds.x + tabStack.bounds.width - point.x;
|
||||
const float topDistance = point.y - tabStack.bounds.y;
|
||||
const float bottomDistance =
|
||||
tabStack.bounds.y + tabStack.bounds.height - point.y;
|
||||
const float minHorizontalThreshold = tabStack.bounds.width * 0.25f;
|
||||
const float minVerticalThreshold = tabStack.bounds.height * 0.25f;
|
||||
const float nearestEdge =
|
||||
(std::min)((std::min)(leftDistance, rightDistance), (std::min)(topDistance, bottomDistance));
|
||||
|
||||
if (nearestEdge == leftDistance && leftDistance <= minHorizontalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Left;
|
||||
}
|
||||
if (nearestEdge == rightDistance && rightDistance <= minHorizontalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Right;
|
||||
}
|
||||
if (nearestEdge == topDistance && topDistance <= minVerticalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Top;
|
||||
}
|
||||
if (nearestEdge == bottomDistance && bottomDistance <= minVerticalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Bottom;
|
||||
}
|
||||
|
||||
return UIEditorWorkspaceDockPlacement::Center;
|
||||
}
|
||||
|
||||
void SyncDockPreview(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout) {
|
||||
state.dockHostState.dropPreview = {};
|
||||
if (state.activeTabDragNodeId.empty() ||
|
||||
state.activeTabDragPanelId.empty() ||
|
||||
!state.hasPointerPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (!IsPointInsideRect(tabStack.bounds, state.pointerPosition)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceDockPlacement placement =
|
||||
ResolveDockPlacement(tabStack, state.pointerPosition);
|
||||
if (tabStack.nodeId == state.activeTabDragNodeId &&
|
||||
placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabStack.nodeId == state.activeTabDragNodeId &&
|
||||
tabStack.items.size() <= 1u) {
|
||||
return;
|
||||
}
|
||||
|
||||
Widgets::UIEditorDockHostDropPreviewState preview = {};
|
||||
preview.visible = true;
|
||||
preview.sourceNodeId = state.activeTabDragNodeId;
|
||||
preview.sourcePanelId = state.activeTabDragPanelId;
|
||||
preview.targetNodeId = tabStack.nodeId;
|
||||
preview.placement = placement;
|
||||
if (placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
preview.insertionIndex =
|
||||
ResolveTabHeaderDropInsertionIndex(tabStack, state.pointerPosition);
|
||||
if (preview.insertionIndex == Widgets::UIEditorTabStripInvalidIndex) {
|
||||
preview.insertionIndex = tabStack.items.size();
|
||||
}
|
||||
}
|
||||
state.dockHostState.dropPreview = std::move(preview);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SyncHoverTarget(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout) {
|
||||
@@ -290,6 +460,11 @@ void SyncHoverTarget(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.activeTabDragNodeId.empty()) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.hasPointerPosition) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
return;
|
||||
@@ -340,6 +515,20 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
||||
? ProcessTabStripEvent(state, layout, event, metrics)
|
||||
: DockHostTabStripEventResult {};
|
||||
eventResult.requestPointerCapture = tabStripResult.requestPointerCapture;
|
||||
eventResult.releasePointerCapture = tabStripResult.releasePointerCapture;
|
||||
if (!tabStripResult.draggedTabId.empty() &&
|
||||
!state.activeTabDragNodeId.empty()) {
|
||||
state.activeTabDragPanelId = tabStripResult.draggedTabId;
|
||||
}
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
!state.activeTabDragPanelId.empty() &&
|
||||
!state.splitterDragState.active) {
|
||||
SyncDockPreview(state, layout);
|
||||
} else if (event.type == UIInputEventType::PointerLeave ||
|
||||
state.activeTabDragNodeId.empty()) {
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case UIInputEventType::FocusGained:
|
||||
@@ -355,6 +544,12 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture = true;
|
||||
}
|
||||
if (!state.activeTabDragNodeId.empty() || tabStripResult.dragCanceled) {
|
||||
ClearTabDockDragState(state);
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture =
|
||||
eventResult.releasePointerCapture || tabStripResult.releasePointerCapture;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
@@ -381,6 +576,21 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
eventResult.hitTarget.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
|
||||
eventResult.hitTarget.nodeId = state.dockHostState.activeSplitterNodeId;
|
||||
}
|
||||
} else if (tabStripResult.priority > 0) {
|
||||
eventResult.consumed = tabStripResult.consumed || tabStripResult.dragStarted;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
if (tabStripResult.dragStarted) {
|
||||
state.activeTabDragNodeId = tabStripResult.nodeId;
|
||||
state.activeTabDragPanelId = tabStripResult.draggedTabId;
|
||||
SyncDockPreview(state, layout);
|
||||
}
|
||||
if (tabStripResult.dragEnded || tabStripResult.dragCanceled) {
|
||||
ClearTabDockDragState(state);
|
||||
}
|
||||
if (eventResult.consumed ||
|
||||
eventResult.hitTarget.kind != UIEditorDockHostHitTargetKind::None) {
|
||||
state.dockHostState.focused = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -388,6 +598,7 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
if (!state.splitterDragState.active) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
}
|
||||
state.dockHostState.dropPreview = {};
|
||||
if (!HasFocusedTabStrip(state)) {
|
||||
state.dockHostState.focused = false;
|
||||
}
|
||||
@@ -464,6 +675,96 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.reorderRequested &&
|
||||
!tabStripResult.nodeId.empty() &&
|
||||
!tabStripResult.draggedTabId.empty() &&
|
||||
tabStripResult.dropInsertionIndex != Widgets::UIEditorTabStripInvalidIndex) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "same-stack reorder node=" << tabStripResult.nodeId
|
||||
<< " panel=" << tabStripResult.draggedTabId
|
||||
<< " insertion=" << tabStripResult.dropInsertionIndex;
|
||||
AppendUIEditorRuntimeTrace("dock", trace.str());
|
||||
}
|
||||
eventResult.layoutResult = controller.ReorderTab(
|
||||
tabStripResult.nodeId,
|
||||
tabStripResult.draggedTabId,
|
||||
tabStripResult.dropInsertionIndex);
|
||||
eventResult.layoutChanged =
|
||||
eventResult.layoutResult.status ==
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.dockHostState.dropPreview.visible &&
|
||||
!state.activeTabDragNodeId.empty() &&
|
||||
!state.activeTabDragPanelId.empty()) {
|
||||
const Widgets::UIEditorDockHostDropPreviewState preview =
|
||||
state.dockHostState.dropPreview;
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "drop commit sourceNode=" << state.activeTabDragNodeId
|
||||
<< " panel=" << state.activeTabDragPanelId
|
||||
<< " targetNode=" << preview.targetNodeId
|
||||
<< " placement=" << static_cast<int>(preview.placement)
|
||||
<< " insertion=" << preview.insertionIndex;
|
||||
AppendUIEditorRuntimeTrace("dock", trace.str());
|
||||
}
|
||||
if (preview.placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
std::size_t insertionIndex = preview.insertionIndex;
|
||||
if (insertionIndex == Widgets::UIEditorTabStripInvalidIndex) {
|
||||
if (const UIEditorDockHostTabStackLayout* targetTabStack =
|
||||
FindTabStackLayoutByNodeId(layout, preview.targetNodeId);
|
||||
targetTabStack != nullptr) {
|
||||
insertionIndex = targetTabStack->items.size();
|
||||
} else {
|
||||
insertionIndex = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
eventResult.layoutResult = controller.MoveTabToStack(
|
||||
state.activeTabDragNodeId,
|
||||
state.activeTabDragPanelId,
|
||||
preview.targetNodeId,
|
||||
insertionIndex);
|
||||
} else {
|
||||
eventResult.layoutResult = controller.DockTabRelative(
|
||||
state.activeTabDragNodeId,
|
||||
state.activeTabDragPanelId,
|
||||
preview.targetNodeId,
|
||||
preview.placement);
|
||||
}
|
||||
eventResult.layoutChanged =
|
||||
eventResult.layoutResult.status ==
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
AppendUIEditorRuntimeTrace(
|
||||
"dock",
|
||||
"drop result status=" +
|
||||
std::string(
|
||||
GetUIEditorWorkspaceLayoutOperationStatusName(
|
||||
eventResult.layoutResult.status)) +
|
||||
" message=" + eventResult.layoutResult.message);
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget.nodeId = preview.targetNodeId;
|
||||
eventResult.releasePointerCapture =
|
||||
eventResult.releasePointerCapture || tabStripResult.releasePointerCapture;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.dragEnded || tabStripResult.dragCanceled) {
|
||||
eventResult.consumed = tabStripResult.consumed;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
@@ -534,6 +835,14 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
break;
|
||||
|
||||
case UIInputEventType::KeyDown:
|
||||
if (tabStripResult.dragCanceled) {
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
|
||||
@@ -11,8 +11,6 @@ using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
constexpr float kMenuBarFontSize = 13.0f;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
@@ -24,6 +22,10 @@ bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) {
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool IsVisibleColor(const UIColor& color) {
|
||||
return color.a > 0.0f;
|
||||
}
|
||||
|
||||
float ResolveEstimatedLabelWidth(
|
||||
const UIEditorMenuBarItem& item,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
@@ -35,7 +37,9 @@ float ResolveEstimatedLabelWidth(
|
||||
}
|
||||
|
||||
float ResolveLabelTop(const UIRect& rect, const UIEditorMenuBarMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kMenuBarFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
return rect.y +
|
||||
(std::max)(0.0f, (rect.height - ClampNonNegative(metrics.labelFontSize)) * 0.5f) +
|
||||
metrics.labelInsetY;
|
||||
}
|
||||
|
||||
bool IsButtonFocused(
|
||||
@@ -164,25 +168,38 @@ void AppendUIEditorMenuBarBackground(
|
||||
const UIEditorMenuBarPalette& palette,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.barColor, metrics.barCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness,
|
||||
metrics.barCornerRounding);
|
||||
const UIColor barBorderColor =
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor;
|
||||
const float barBorderThickness =
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness;
|
||||
if (IsVisibleColor(barBorderColor) && barBorderThickness > 0.0f) {
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
barBorderColor,
|
||||
barBorderThickness,
|
||||
metrics.barCornerRounding);
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
|
||||
const bool open = state.openIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
const bool focused = IsButtonFocused(state, index);
|
||||
drawList.AddFilledRect(
|
||||
layout.buttonRects[index],
|
||||
ResolveButtonFillColor(open, hovered, palette),
|
||||
metrics.buttonCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.buttonRects[index],
|
||||
ResolveButtonBorderColor(open, focused, palette),
|
||||
ResolveButtonBorderThickness(open, focused, metrics),
|
||||
metrics.buttonCornerRounding);
|
||||
const UIColor buttonFillColor = ResolveButtonFillColor(open, hovered, palette);
|
||||
const UIColor buttonBorderColor = ResolveButtonBorderColor(open, focused, palette);
|
||||
const float buttonBorderThickness = ResolveButtonBorderThickness(open, focused, metrics);
|
||||
if (IsVisibleColor(buttonFillColor)) {
|
||||
drawList.AddFilledRect(
|
||||
layout.buttonRects[index],
|
||||
buttonFillColor,
|
||||
metrics.buttonCornerRounding);
|
||||
}
|
||||
if (IsVisibleColor(buttonBorderColor) && buttonBorderThickness > 0.0f) {
|
||||
drawList.AddRectOutline(
|
||||
layout.buttonRects[index],
|
||||
buttonBorderColor,
|
||||
buttonBorderThickness,
|
||||
metrics.buttonCornerRounding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +223,7 @@ void AppendUIEditorMenuBarForeground(
|
||||
UIPoint(textLeft, ResolveLabelTop(rect, metrics)),
|
||||
items[index].label,
|
||||
items[index].enabled ? palette.textPrimary : palette.textDisabled,
|
||||
kMenuBarFontSize);
|
||||
ClampNonNegative(metrics.labelFontSize));
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +171,14 @@ void AppendUIEditorMenuPopupBackground(
|
||||
const UIRect& rect = layout.itemRects[index];
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
const float lineY = rect.y + rect.height * 0.5f;
|
||||
const float separatorInset =
|
||||
ClampNonNegative(metrics.contentPaddingX) + 3.0f;
|
||||
drawList.AddFilledRect(
|
||||
UIRect(rect.x + 8.0f, lineY, (std::max)(rect.width - 16.0f, 0.0f), metrics.separatorThickness),
|
||||
UIRect(
|
||||
rect.x + separatorInset,
|
||||
lineY,
|
||||
(std::max)(rect.width - separatorInset * 2.0f, 0.0f),
|
||||
metrics.separatorThickness),
|
||||
palette.separatorColor);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -191,6 +191,19 @@ void AppendUIEditorShellToolbar(
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
const std::vector<Widgets::UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorShellComposeMetrics& metrics) {
|
||||
return BuildUIEditorShellComposeLayout(
|
||||
bounds,
|
||||
menuBarItems,
|
||||
{},
|
||||
statusSegments,
|
||||
metrics);
|
||||
}
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
|
||||
@@ -148,7 +148,9 @@ const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(
|
||||
}
|
||||
|
||||
std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
||||
const UIEditorResolvedMenuModel& model) {
|
||||
const UIEditorResolvedMenuModel& model,
|
||||
const UIEditorShellInteractionServices& services,
|
||||
const Widgets::UIEditorMenuBarMetrics& metrics) {
|
||||
std::vector<Widgets::UIEditorMenuBarItem> items = {};
|
||||
items.reserve(model.menus.size());
|
||||
|
||||
@@ -157,6 +159,10 @@ std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
||||
item.menuId = menu.menuId;
|
||||
item.label = menu.label;
|
||||
item.enabled = !menu.items.empty();
|
||||
if (services.textMeasurer != nullptr && !item.label.empty()) {
|
||||
item.desiredLabelWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { item.label, metrics.labelFontSize });
|
||||
}
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
|
||||
@@ -175,7 +181,9 @@ UIEditorShellComposeModel BuildShellComposeModel(
|
||||
}
|
||||
|
||||
std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
||||
const std::vector<UIEditorResolvedMenuItem>& items) {
|
||||
const std::vector<UIEditorResolvedMenuItem>& items,
|
||||
const UIEditorShellInteractionServices& services,
|
||||
const Widgets::UIEditorMenuPopupMetrics& metrics) {
|
||||
std::vector<Widgets::UIEditorMenuPopupItem> widgetItems = {};
|
||||
widgetItems.reserve(items.size());
|
||||
|
||||
@@ -188,6 +196,16 @@ std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
||||
widgetItem.enabled = item.enabled;
|
||||
widgetItem.checked = item.checked;
|
||||
widgetItem.hasSubmenu = item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty();
|
||||
if (services.textMeasurer != nullptr) {
|
||||
if (!widgetItem.label.empty()) {
|
||||
widgetItem.desiredLabelWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { widgetItem.label, metrics.labelFontSize });
|
||||
}
|
||||
if (!widgetItem.shortcutText.empty()) {
|
||||
widgetItem.desiredShortcutWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { widgetItem.shortcutText, metrics.labelFontSize });
|
||||
}
|
||||
}
|
||||
widgetItems.push_back(std::move(widgetItem));
|
||||
}
|
||||
|
||||
@@ -244,10 +262,14 @@ BuildRequestOutput BuildRequest(
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShellInteractionModel& model,
|
||||
const UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionMetrics& metrics) {
|
||||
const UIEditorShellInteractionMetrics& metrics,
|
||||
const UIEditorShellInteractionServices& services) {
|
||||
BuildRequestOutput output = {};
|
||||
UIEditorShellInteractionRequest& request = output.request;
|
||||
request.menuBarItems = BuildMenuBarItems(model.resolvedMenuModel);
|
||||
request.menuBarItems = BuildMenuBarItems(
|
||||
model.resolvedMenuModel,
|
||||
services,
|
||||
metrics.shellMetrics.menuBarMetrics);
|
||||
|
||||
const UIEditorShellComposeModel shellModel =
|
||||
BuildShellComposeModel(model, request.menuBarItems);
|
||||
@@ -293,7 +315,10 @@ BuildRequestOutput BuildRequest(
|
||||
popupRequest.sourceItemId = popupState.itemId;
|
||||
popupRequest.overlayEntry = *overlayEntry;
|
||||
popupRequest.resolvedItems = *resolvedItems;
|
||||
popupRequest.widgetItems = BuildPopupWidgetItems(popupRequest.resolvedItems);
|
||||
popupRequest.widgetItems = BuildPopupWidgetItems(
|
||||
popupRequest.resolvedItems,
|
||||
services,
|
||||
metrics.popupMetrics);
|
||||
|
||||
const float popupWidth =
|
||||
ResolveUIEditorMenuPopupDesiredWidth(popupRequest.widgetItems, metrics.popupMetrics);
|
||||
@@ -506,7 +531,8 @@ UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
@@ -525,7 +551,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics);
|
||||
metrics,
|
||||
services);
|
||||
UIEditorShellInteractionRequest request = std::move(requestBuild.request);
|
||||
|
||||
if (requestBuild.hadInvalidPopupState && state.menuSession.HasOpenMenu()) {
|
||||
@@ -539,7 +566,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics);
|
||||
metrics,
|
||||
services);
|
||||
request = std::move(requestBuild.request);
|
||||
}
|
||||
|
||||
@@ -691,7 +719,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +743,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
|
||||
const RequestHit finalHit =
|
||||
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
|
||||
|
||||
@@ -110,21 +110,13 @@ UIColor ResolveSurfaceBorderColor(
|
||||
return palette.surfaceCapturedBorderColor;
|
||||
}
|
||||
|
||||
if (state.surfaceActive) {
|
||||
return palette.surfaceActiveBorderColor;
|
||||
}
|
||||
|
||||
if (state.surfaceHovered) {
|
||||
return palette.surfaceHoveredBorderColor;
|
||||
}
|
||||
|
||||
return palette.surfaceBorderColor;
|
||||
}
|
||||
|
||||
float ResolveSurfaceBorderThickness(
|
||||
const UIEditorViewportSlotState& state,
|
||||
const UIEditorViewportSlotMetrics& metrics) {
|
||||
if (state.inputCaptured || state.focused) {
|
||||
if (state.inputCaptured) {
|
||||
return metrics.focusedSurfaceBorderThickness;
|
||||
}
|
||||
|
||||
@@ -356,8 +348,8 @@ void AppendUIEditorViewportSlotBackground(
|
||||
drawList.AddFilledRect(layout.bounds, palette.frameColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.outerBorderThickness,
|
||||
palette.borderColor,
|
||||
metrics.outerBorderThickness,
|
||||
metrics.cornerRounding);
|
||||
|
||||
if (layout.hasTopBar) {
|
||||
@@ -366,12 +358,6 @@ void AppendUIEditorViewportSlotBackground(
|
||||
|
||||
drawList.AddFilledRect(layout.surfaceRect, palette.surfaceColor);
|
||||
|
||||
if (state.surfaceHovered) {
|
||||
drawList.AddFilledRect(layout.inputRect, palette.surfaceHoverOverlayColor);
|
||||
}
|
||||
if (state.surfaceActive) {
|
||||
drawList.AddFilledRect(layout.inputRect, palette.surfaceActiveOverlayColor);
|
||||
}
|
||||
if (state.inputCaptured) {
|
||||
drawList.AddFilledRect(layout.inputRect, palette.captureOverlayColor);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
FindUIEditorPanelSessionState(session, panelId);
|
||||
return panelState != nullptr && panelState->open && panelState->visible;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
@@ -22,6 +32,58 @@ std::vector<std::string> CollectVisiblePanelIds(
|
||||
return ids;
|
||||
}
|
||||
|
||||
struct VisibleTabStackInfo {
|
||||
bool panelExists = false;
|
||||
bool panelVisible = false;
|
||||
std::size_t currentVisibleIndex = 0u;
|
||||
std::size_t visibleTabCount = 0u;
|
||||
};
|
||||
|
||||
VisibleTabStackInfo ResolveVisibleTabStackInfo(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
VisibleTabStackInfo info = {};
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool visible = IsPanelOpenAndVisible(session, child.panel.panelId);
|
||||
if (child.panel.panelId == panelId) {
|
||||
info.panelExists = true;
|
||||
info.panelVisible = visible;
|
||||
if (visible) {
|
||||
info.currentVisibleIndex = info.visibleTabCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
++info.visibleTabCount;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::size_t CountVisibleTabs(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
std::size_t visibleCount = 0u;
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisible(session, child.panel.panelId)) {
|
||||
++visibleCount;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleCount;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind) {
|
||||
@@ -297,6 +359,290 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRati
|
||||
"Split ratio updated.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::ReorderTab(
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (nodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab requires a tab stack node id.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab requires a panel id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* tabStack = FindUIEditorWorkspaceNode(m_workspace, nodeId);
|
||||
if (tabStack == nullptr || tabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo tabInfo =
|
||||
ResolveVisibleTabStackInfo(*tabStack, m_session, panelId);
|
||||
if (!tabInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target panel is missing from the specified tab stack.");
|
||||
}
|
||||
|
||||
if (!tabInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex > tabInfo.visibleTabCount) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target visible insertion index is out of range.");
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == tabInfo.currentVisibleIndex ||
|
||||
targetVisibleInsertionIndex == tabInfo.currentVisibleIndex + 1u) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Visible tab order already matches the requested insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryReorderUIEditorWorkspaceTab(
|
||||
m_workspace,
|
||||
m_session,
|
||||
nodeId,
|
||||
panelId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Tab reorder rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Visible tab order already matches the requested insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Tab reorder produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab reordered.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::MoveTabToStack(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "MoveTabToStack begin sourceNode=" << sourceNodeId
|
||||
<< " panel=" << panelId
|
||||
<< " targetNode=" << targetNodeId
|
||||
<< " insertion=" << targetVisibleInsertionIndex;
|
||||
AppendUIEditorRuntimeTrace("workspace", trace.str());
|
||||
}
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() || targetNodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires both source and target tab stack ids.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires a panel id.");
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId) {
|
||||
return ReorderTab(sourceNodeId, panelId, targetVisibleInsertionIndex);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, targetNodeId);
|
||||
if (sourceTabStack == nullptr ||
|
||||
targetTabStack == nullptr ||
|
||||
sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack source or target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo sourceInfo =
|
||||
ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId);
|
||||
if (!sourceInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack target panel is missing from the source tab stack.");
|
||||
}
|
||||
|
||||
if (!sourceInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
const std::size_t visibleTargetCount = CountVisibleTabs(*targetTabStack, m_session);
|
||||
if (targetVisibleInsertionIndex > visibleTargetCount) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack target visible insertion index is out of range.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryMoveUIEditorWorkspaceTabToStack(
|
||||
m_workspace,
|
||||
m_session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Tab already matches the requested target stack insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab moved to target stack.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::DockTabRelative(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "DockTabRelative begin sourceNode=" << sourceNodeId
|
||||
<< " panel=" << panelId
|
||||
<< " targetNode=" << targetNodeId
|
||||
<< " placement=" << static_cast<int>(placement)
|
||||
<< " splitRatio=" << splitRatio;
|
||||
AppendUIEditorRuntimeTrace("workspace", trace.str());
|
||||
}
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() || targetNodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative requires both source and target tab stack ids.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative requires a panel id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, targetNodeId);
|
||||
if (sourceTabStack == nullptr ||
|
||||
targetTabStack == nullptr ||
|
||||
sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative source or target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo sourceInfo =
|
||||
ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId);
|
||||
if (!sourceInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative target panel is missing from the source tab stack.");
|
||||
}
|
||||
|
||||
if (!sourceInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryDockUIEditorWorkspaceTabRelative(
|
||||
m_workspace,
|
||||
m_session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Dock layout already matches the requested placement.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab docked relative to target stack.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
|
||||
const UIEditorWorkspaceCommand& command) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
@@ -43,6 +44,19 @@ UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelN
|
||||
return tabStack;
|
||||
}
|
||||
|
||||
void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::Split ||
|
||||
node.children.size() != 1u) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the remaining child through a temporary object first. Assigning
|
||||
// directly from node.children.front() aliases a subobject of node and can
|
||||
// trigger use-after-move when the vector storage is torn down.
|
||||
UIEditorWorkspaceNode remainingChild = std::move(node.children.front());
|
||||
node = std::move(remainingChild);
|
||||
}
|
||||
|
||||
void CanonicalizeNodeRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
bool allowStandalonePanelLeaf) {
|
||||
@@ -67,7 +81,7 @@ void CanonicalizeNodeRecursive(
|
||||
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
||||
node.children.size() == 1u) {
|
||||
node = std::move(node.children.front());
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +145,256 @@ UIEditorWorkspaceNode* FindMutableNodeRecursive(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FindNodePathRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId,
|
||||
std::vector<std::size_t>& path) {
|
||||
if (node.nodeId == nodeId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
path.push_back(index);
|
||||
if (FindNodePathRecursive(node.children[index], nodeId, path)) {
|
||||
return true;
|
||||
}
|
||||
path.pop_back();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* ResolveMutableNodeByPath(
|
||||
UIEditorWorkspaceNode& node,
|
||||
const std::vector<std::size_t>& path) {
|
||||
UIEditorWorkspaceNode* current = &node;
|
||||
for (const std::size_t childIndex : path) {
|
||||
if (childIndex >= current->children.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
current = ¤t->children[childIndex];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
bool IsPanelOpenAndVisibleInSession(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId);
|
||||
return state != nullptr && state->open && state->visible;
|
||||
}
|
||||
|
||||
std::size_t CountVisibleChildren(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
std::size_t visibleCount = 0u;
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
++visibleCount;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleCount;
|
||||
}
|
||||
|
||||
std::size_t ResolveActualInsertionIndexForVisibleInsertion(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
std::vector<std::size_t> visibleIndices = {};
|
||||
visibleIndices.reserve(node.children.size());
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = node.children[index];
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
visibleIndices.push_back(index);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == 0u) {
|
||||
return visibleIndices.empty() ? 0u : visibleIndices.front();
|
||||
}
|
||||
|
||||
if (visibleIndices.empty()) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex >= visibleIndices.size()) {
|
||||
return visibleIndices.back() + 1u;
|
||||
}
|
||||
|
||||
return visibleIndices[targetVisibleInsertionIndex];
|
||||
}
|
||||
|
||||
void FixTabStackSelectedIndex(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view preferredPanelId) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack || node.children.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
if (node.children[index].kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
node.children[index].panel.panelId == preferredPanelId) {
|
||||
node.selectedTabIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.selectedTabIndex >= node.children.size()) {
|
||||
node.selectedTabIndex = node.children.size() - 1u;
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoveNodeByIdRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
if (node.children[index].nodeId == nodeId) {
|
||||
node.children.erase(node.children.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (node.children.size() == 1u) {
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (UIEditorWorkspaceNode& child : node.children) {
|
||||
if (RemoveNodeByIdRecursive(child, nodeId)) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
||||
node.children.size() == 1u) {
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
float ClampDockSplitRatio(float value) {
|
||||
constexpr float kMinRatio = 0.1f;
|
||||
constexpr float kMaxRatio = 0.9f;
|
||||
return (std::min)(kMaxRatio, (std::max)(kMinRatio, value));
|
||||
}
|
||||
|
||||
bool IsLeadingDockPlacement(UIEditorWorkspaceDockPlacement placement) {
|
||||
return placement == UIEditorWorkspaceDockPlacement::Left ||
|
||||
placement == UIEditorWorkspaceDockPlacement::Top;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSplitAxis ResolveDockSplitAxis(UIEditorWorkspaceDockPlacement placement) {
|
||||
return placement == UIEditorWorkspaceDockPlacement::Left ||
|
||||
placement == UIEditorWorkspaceDockPlacement::Right
|
||||
? UIEditorWorkspaceSplitAxis::Horizontal
|
||||
: UIEditorWorkspaceSplitAxis::Vertical;
|
||||
}
|
||||
|
||||
std::string MakeUniqueNodeId(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string base) {
|
||||
if (base.empty()) {
|
||||
base = "workspace-node";
|
||||
}
|
||||
|
||||
if (FindUIEditorWorkspaceNode(workspace, base) == nullptr) {
|
||||
return base;
|
||||
}
|
||||
|
||||
for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) {
|
||||
const std::string candidate = base + "-" + std::to_string(suffix);
|
||||
if (FindUIEditorWorkspaceNode(workspace, candidate) == nullptr) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return base + "-overflow";
|
||||
}
|
||||
|
||||
bool TryExtractVisiblePanelFromTabStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceNode& extractedPanel) {
|
||||
std::vector<std::size_t> sourcePath = {};
|
||||
if (!FindNodePathRecursive(workspace.root, sourceNodeId, sourcePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* sourceStack =
|
||||
ResolveMutableNodeByPath(workspace.root, sourcePath);
|
||||
if (sourceStack == nullptr ||
|
||||
sourceStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t panelIndex = sourceStack->children.size();
|
||||
for (std::size_t index = 0; index < sourceStack->children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = sourceStack->children[index];
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (child.panel.panelId == panelId) {
|
||||
if (!IsPanelOpenAndVisibleInSession(session, panelId)) {
|
||||
return false;
|
||||
}
|
||||
panelIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (panelIndex >= sourceStack->children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourcePath.empty() && sourceStack->children.size() == 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string fallbackSelectedPanelId = {};
|
||||
if (sourceStack->selectedTabIndex < sourceStack->children.size()) {
|
||||
fallbackSelectedPanelId =
|
||||
sourceStack->children[sourceStack->selectedTabIndex].panel.panelId;
|
||||
}
|
||||
|
||||
extractedPanel = std::move(sourceStack->children[panelIndex]);
|
||||
sourceStack->children.erase(
|
||||
sourceStack->children.begin() + static_cast<std::ptrdiff_t>(panelIndex));
|
||||
|
||||
if (sourceStack->children.empty()) {
|
||||
if (sourcePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RemoveNodeByIdRecursive(workspace.root, sourceNodeId)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (fallbackSelectedPanelId == panelId) {
|
||||
const std::size_t nextIndex =
|
||||
(std::min)(panelIndex, sourceStack->children.size() - 1u);
|
||||
fallbackSelectedPanelId =
|
||||
sourceStack->children[nextIndex].panel.panelId;
|
||||
}
|
||||
FixTabStackSelectedIndex(*sourceStack, fallbackSelectedPanelId);
|
||||
}
|
||||
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryActivateRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId) {
|
||||
@@ -484,4 +748,266 @@ bool TrySetUIEditorWorkspaceSplitRatio(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryReorderUIEditorWorkspaceTab(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
UIEditorWorkspaceNode* node = FindMutableNodeRecursive(workspace.root, nodeId);
|
||||
if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::size_t> visibleChildIndices = {};
|
||||
std::vector<UIEditorWorkspaceNode> reorderedVisibleChildren = {};
|
||||
visibleChildIndices.reserve(node->children.size());
|
||||
reorderedVisibleChildren.reserve(node->children.size());
|
||||
|
||||
std::size_t sourceVisibleIndex = node->children.size();
|
||||
for (std::size_t index = 0; index < node->children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = node->children[index];
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child.panel.panelId == panelId) {
|
||||
sourceVisibleIndex = visibleChildIndices.size();
|
||||
}
|
||||
|
||||
visibleChildIndices.push_back(index);
|
||||
reorderedVisibleChildren.push_back(child);
|
||||
}
|
||||
|
||||
if (sourceVisibleIndex >= reorderedVisibleChildren.size() ||
|
||||
targetVisibleInsertionIndex > reorderedVisibleChildren.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == sourceVisibleIndex ||
|
||||
targetVisibleInsertionIndex == sourceVisibleIndex + 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode movedChild =
|
||||
std::move(reorderedVisibleChildren[sourceVisibleIndex]);
|
||||
reorderedVisibleChildren.erase(
|
||||
reorderedVisibleChildren.begin() + static_cast<std::ptrdiff_t>(sourceVisibleIndex));
|
||||
|
||||
std::size_t adjustedInsertionIndex = targetVisibleInsertionIndex;
|
||||
if (adjustedInsertionIndex > sourceVisibleIndex) {
|
||||
--adjustedInsertionIndex;
|
||||
}
|
||||
if (adjustedInsertionIndex > reorderedVisibleChildren.size()) {
|
||||
adjustedInsertionIndex = reorderedVisibleChildren.size();
|
||||
}
|
||||
|
||||
reorderedVisibleChildren.insert(
|
||||
reorderedVisibleChildren.begin() +
|
||||
static_cast<std::ptrdiff_t>(adjustedInsertionIndex),
|
||||
std::move(movedChild));
|
||||
|
||||
std::string selectedPanelId = {};
|
||||
if (node->selectedTabIndex < node->children.size()) {
|
||||
selectedPanelId = node->children[node->selectedTabIndex].panel.panelId;
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceNode> originalChildren = node->children;
|
||||
std::size_t nextVisibleIndex = 0u;
|
||||
for (std::size_t index = 0; index < originalChildren.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& originalChild = originalChildren[index];
|
||||
if (!IsPanelOpenAndVisibleInSession(session, originalChild.panel.panelId)) {
|
||||
node->children[index] = originalChild;
|
||||
continue;
|
||||
}
|
||||
|
||||
node->children[index] = reorderedVisibleChildren[nextVisibleIndex];
|
||||
++nextVisibleIndex;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node->children.size(); ++index) {
|
||||
if (node->children[index].panel.panelId == selectedPanelId) {
|
||||
node->selectedTabIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMoveUIEditorWorkspaceTabToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
if (sourceNodeId.empty() ||
|
||||
panelId.empty() ||
|
||||
targetNodeId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId) {
|
||||
return TryReorderUIEditorWorkspaceTab(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetVisibleInsertionIndex);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (targetNode == nullptr ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex > CountVisibleChildren(*targetNode, session)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode extractedPanel = {};
|
||||
if (!TryExtractVisiblePanelFromTabStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t actualInsertionIndex =
|
||||
ResolveActualInsertionIndexForVisibleInsertion(
|
||||
*targetStack,
|
||||
session,
|
||||
targetVisibleInsertionIndex);
|
||||
if (actualInsertionIndex > targetStack->children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
targetStack->children.insert(
|
||||
targetStack->children.begin() +
|
||||
static_cast<std::ptrdiff_t>(actualInsertionIndex),
|
||||
std::move(extractedPanel));
|
||||
targetStack->selectedTabIndex = actualInsertionIndex;
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryDockUIEditorWorkspaceTabRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
if (placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (targetNode == nullptr ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryMoveUIEditorWorkspaceTabToStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
CountVisibleChildren(*targetNode, session));
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() ||
|
||||
panelId.empty() ||
|
||||
targetNodeId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceNode =
|
||||
FindUIEditorWorkspaceNode(workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (sourceNode == nullptr ||
|
||||
targetNode == nullptr ||
|
||||
sourceNode->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId &&
|
||||
sourceNode->children.size() <= 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode extractedPanel = {};
|
||||
if (!TryExtractVisiblePanelFromTabStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string movedStackNodeId = MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_" + std::string(panelId) + "_stack");
|
||||
UIEditorWorkspaceNode movedStack = {};
|
||||
movedStack.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
movedStack.nodeId = movedStackNodeId;
|
||||
movedStack.selectedTabIndex = 0u;
|
||||
movedStack.children.push_back(std::move(extractedPanel));
|
||||
|
||||
UIEditorWorkspaceNode existingTarget = std::move(*targetStack);
|
||||
UIEditorWorkspaceNode primary = {};
|
||||
UIEditorWorkspaceNode secondary = {};
|
||||
if (IsLeadingDockPlacement(placement)) {
|
||||
primary = std::move(movedStack);
|
||||
secondary = std::move(existingTarget);
|
||||
} else {
|
||||
primary = std::move(existingTarget);
|
||||
secondary = std::move(movedStack);
|
||||
}
|
||||
|
||||
const float requestedRatio = ClampDockSplitRatio(splitRatio);
|
||||
const float resolvedSplitRatio =
|
||||
IsLeadingDockPlacement(placement)
|
||||
? requestedRatio
|
||||
: (1.0f - requestedRatio);
|
||||
*targetStack = BuildUIEditorWorkspaceSplit(
|
||||
MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_split"),
|
||||
ResolveDockSplitAxis(placement),
|
||||
resolvedSplitRatio,
|
||||
std::move(primary),
|
||||
std::move(secondary));
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
|
||||
Reference in New Issue
Block a user