Refine XCEditor docking and DPI rendering

This commit is contained in:
2026-04-11 17:07:37 +08:00
parent 35d3d6328b
commit 2958dcc491
46 changed files with 4839 additions and 471 deletions

View File

@@ -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
)

View File

@@ -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)) {

View File

@@ -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

View File

@@ -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)));

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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),

View 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, &currentTime);
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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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 = &current->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