Refine XCEditor docking and DPI rendering
This commit is contained in:
@@ -501,6 +501,91 @@ UIColor ResolveSplitterColor(const UIEditorDockHostSplitterLayout& splitter, con
|
||||
return palette.splitterColor;
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackLayoutByNodeId(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIRect InsetRect(const UIRect& rect, float inset) {
|
||||
const float clampedInset = ClampNonNegative(inset);
|
||||
const float insetX = (std::min)(clampedInset, rect.width * 0.5f);
|
||||
const float insetY = (std::min)(clampedInset, rect.height * 0.5f);
|
||||
return UIRect(
|
||||
rect.x + insetX,
|
||||
rect.y + insetY,
|
||||
(std::max)(0.0f, rect.width - insetX * 2.0f),
|
||||
(std::max)(0.0f, rect.height - insetY * 2.0f));
|
||||
}
|
||||
|
||||
UIRect ResolveDropPreviewRect(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
UIEditorWorkspaceDockPlacement placement) {
|
||||
const UIRect bounds = tabStack.bounds;
|
||||
switch (placement) {
|
||||
case UIEditorWorkspaceDockPlacement::Left:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width * 0.35f,
|
||||
bounds.height);
|
||||
case UIEditorWorkspaceDockPlacement::Right:
|
||||
return UIRect(
|
||||
bounds.x + bounds.width * 0.65f,
|
||||
bounds.y,
|
||||
bounds.width * 0.35f,
|
||||
bounds.height);
|
||||
case UIEditorWorkspaceDockPlacement::Top:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width,
|
||||
bounds.height * 0.35f);
|
||||
case UIEditorWorkspaceDockPlacement::Bottom:
|
||||
return UIRect(
|
||||
bounds.x,
|
||||
bounds.y + bounds.height * 0.65f,
|
||||
bounds.width,
|
||||
bounds.height * 0.35f);
|
||||
case UIEditorWorkspaceDockPlacement::Center:
|
||||
default:
|
||||
return InsetRect(bounds, 4.0f);
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorDockHostDropPreviewLayout ResolveDropPreviewLayout(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostState& state) {
|
||||
UIEditorDockHostDropPreviewLayout preview = {};
|
||||
if (!state.dropPreview.visible || state.dropPreview.targetNodeId.empty()) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* targetTabStack =
|
||||
FindTabStackLayoutByNodeId(layout, state.dropPreview.targetNodeId);
|
||||
if (targetTabStack == nullptr) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
preview.visible = true;
|
||||
preview.targetNodeId = state.dropPreview.targetNodeId;
|
||||
preview.placement = state.dropPreview.placement;
|
||||
preview.insertionIndex = state.dropPreview.insertionIndex;
|
||||
preview.previewRect =
|
||||
ResolveDropPreviewRect(*targetTabStack, state.dropPreview.placement);
|
||||
if (preview.previewRect.width <= 0.0f || preview.previewRect.height <= 0.0f) {
|
||||
preview = {};
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
|
||||
@@ -539,6 +624,7 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
|
||||
state,
|
||||
metrics,
|
||||
layout);
|
||||
layout.dropPreview = ResolveDropPreviewLayout(layout, state);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -607,8 +693,6 @@ void AppendUIEditorDockHostBackground(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
||||
BuildTabContentFrameMetrics(metrics);
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||
tabItems.reserve(tabStack.items.size());
|
||||
@@ -626,12 +710,6 @@ void AppendUIEditorDockHostBackground(
|
||||
tabStack.tabStripState,
|
||||
palette.tabStripPalette,
|
||||
metrics.tabStripMetrics);
|
||||
AppendUIEditorPanelFrameBackground(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
palette.panelFramePalette,
|
||||
tabContentFrameMetrics);
|
||||
}
|
||||
|
||||
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
|
||||
@@ -648,8 +726,6 @@ void AppendUIEditorDockHostForeground(
|
||||
const UIEditorDockHostForegroundOptions& options,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
||||
BuildTabContentFrameMetrics(metrics);
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||
tabItems.reserve(tabStack.items.size());
|
||||
@@ -668,18 +744,21 @@ void AppendUIEditorDockHostForeground(
|
||||
tabStack.tabStripState,
|
||||
palette.tabStripPalette,
|
||||
metrics.tabStripMetrics);
|
||||
AppendUIEditorPanelFrameForeground(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
{},
|
||||
palette.panelFramePalette,
|
||||
tabContentFrameMetrics);
|
||||
|
||||
if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (layout.dropPreview.visible) {
|
||||
drawList.AddFilledRect(
|
||||
layout.dropPreview.previewRect,
|
||||
palette.dropPreviewFillColor);
|
||||
drawList.AddRectOutline(
|
||||
layout.dropPreview.previewRect,
|
||||
palette.dropPreviewBorderColor,
|
||||
1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorDockHostForeground(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -13,6 +15,7 @@ namespace {
|
||||
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Widgets::BeginUISplitterDrag;
|
||||
using ::XCEngine::UI::Widgets::EndUISplitterDrag;
|
||||
@@ -30,8 +33,17 @@ using Widgets::UIEditorTabStripItem;
|
||||
struct DockHostTabStripEventResult {
|
||||
bool consumed = false;
|
||||
bool commandRequested = false;
|
||||
bool reorderRequested = false;
|
||||
bool dragStarted = false;
|
||||
bool dragEnded = false;
|
||||
bool dragCanceled = false;
|
||||
bool requestPointerCapture = false;
|
||||
bool releasePointerCapture = false;
|
||||
UIEditorWorkspaceCommandKind commandKind = UIEditorWorkspaceCommandKind::ActivatePanel;
|
||||
std::size_t dropInsertionIndex = Widgets::UIEditorTabStripInvalidIndex;
|
||||
std::string panelId = {};
|
||||
std::string nodeId = {};
|
||||
std::string draggedTabId = {};
|
||||
UIEditorDockHostHitTarget hitTarget = {};
|
||||
int priority = 0;
|
||||
};
|
||||
@@ -130,6 +142,13 @@ void PruneTabStripInteractionEntries(
|
||||
return !isVisibleNodeId(entry.nodeId);
|
||||
}),
|
||||
state.dockHostState.tabStripStates.end());
|
||||
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
!isVisibleNodeId(state.activeTabDragNodeId)) {
|
||||
state.activeTabDragNodeId.clear();
|
||||
state.activeTabDragPanelId.clear();
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
}
|
||||
|
||||
void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state) {
|
||||
@@ -152,6 +171,33 @@ bool HasFocusedTabStrip(const UIEditorDockHostInteractionState& state) {
|
||||
}) != state.tabStripInteractions.end();
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStackLayout* FindTabStackLayoutByNodeId(
|
||||
const Widgets::UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.nodeId == nodeId) {
|
||||
return &tabStack;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsPointInsideRect(
|
||||
const UIRect& rect,
|
||||
const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
void ClearTabDockDragState(UIEditorDockHostInteractionState& state) {
|
||||
state.activeTabDragNodeId.clear();
|
||||
state.activeTabDragPanelId.clear();
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
|
||||
std::vector<UIEditorTabStripItem> BuildTabStripItems(
|
||||
const UIEditorDockHostTabStackLayout& tabStack) {
|
||||
std::vector<UIEditorTabStripItem> items = {};
|
||||
@@ -198,6 +244,13 @@ UIEditorDockHostHitTarget MapTabStripHitTarget(
|
||||
}
|
||||
|
||||
int ResolveTabStripPriority(const UIEditorTabStripInteractionResult& result) {
|
||||
if (result.reorderRequested ||
|
||||
result.dragStarted ||
|
||||
result.dragEnded ||
|
||||
result.dragCanceled) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (result.closeRequested) {
|
||||
return 4;
|
||||
}
|
||||
@@ -221,6 +274,11 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
DockHostTabStripEventResult resolved = {};
|
||||
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
tabStack.nodeId != state.activeTabDragNodeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorDockHostTabStripInteractionEntry& entry =
|
||||
FindOrCreateTabStripInteractionEntry(state, tabStack.nodeId);
|
||||
std::string selectedTabId = tabStack.selectedPanelId;
|
||||
@@ -238,7 +296,15 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
continue;
|
||||
}
|
||||
|
||||
resolved.nodeId = tabStack.nodeId;
|
||||
resolved.hitTarget = MapTabStripHitTarget(tabStack, frame.result);
|
||||
resolved.requestPointerCapture = frame.result.requestPointerCapture;
|
||||
resolved.releasePointerCapture = frame.result.releasePointerCapture;
|
||||
resolved.dragStarted = frame.result.dragStarted;
|
||||
resolved.dragEnded = frame.result.dragEnded;
|
||||
resolved.dragCanceled = frame.result.dragCanceled;
|
||||
resolved.dropInsertionIndex = frame.result.dropInsertionIndex;
|
||||
resolved.draggedTabId = frame.result.draggedTabId;
|
||||
if ((frame.result.closeRequested && !frame.result.closedTabId.empty()) ||
|
||||
(event.type == UIInputEventType::PointerButtonUp &&
|
||||
frame.result.consumed &&
|
||||
@@ -250,6 +316,12 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
!frame.result.closedTabId.empty()
|
||||
? frame.result.closedTabId
|
||||
: resolved.hitTarget.panelId;
|
||||
} else if (frame.result.reorderRequested &&
|
||||
!frame.result.draggedTabId.empty() &&
|
||||
frame.result.dropInsertionIndex != Widgets::UIEditorTabStripInvalidIndex) {
|
||||
resolved.reorderRequested = true;
|
||||
resolved.panelId = frame.result.draggedTabId;
|
||||
resolved.dropInsertionIndex = frame.result.dropInsertionIndex;
|
||||
} else if ((frame.result.selectionChanged ||
|
||||
frame.result.keyboardNavigated ||
|
||||
(event.type == UIInputEventType::PointerButtonUp &&
|
||||
@@ -266,6 +338,7 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
continue;
|
||||
} else {
|
||||
resolved.commandRequested = false;
|
||||
resolved.reorderRequested = false;
|
||||
resolved.panelId.clear();
|
||||
}
|
||||
|
||||
@@ -277,6 +350,103 @@ DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
return resolved;
|
||||
}
|
||||
|
||||
std::size_t ResolveTabHeaderDropInsertionIndex(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
const UIPoint& point) {
|
||||
if (!IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) {
|
||||
return Widgets::UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t insertionIndex = 0u;
|
||||
for (const UIRect& rect : tabStack.tabStripLayout.tabHeaderRects) {
|
||||
const float midpoint = rect.x + rect.width * 0.5f;
|
||||
if (point.x > midpoint) {
|
||||
++insertionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return insertionIndex;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceDockPlacement ResolveDockPlacement(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
const UIPoint& point) {
|
||||
if (IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) {
|
||||
return UIEditorWorkspaceDockPlacement::Center;
|
||||
}
|
||||
|
||||
const float leftDistance = point.x - tabStack.bounds.x;
|
||||
const float rightDistance =
|
||||
tabStack.bounds.x + tabStack.bounds.width - point.x;
|
||||
const float topDistance = point.y - tabStack.bounds.y;
|
||||
const float bottomDistance =
|
||||
tabStack.bounds.y + tabStack.bounds.height - point.y;
|
||||
const float minHorizontalThreshold = tabStack.bounds.width * 0.25f;
|
||||
const float minVerticalThreshold = tabStack.bounds.height * 0.25f;
|
||||
const float nearestEdge =
|
||||
(std::min)((std::min)(leftDistance, rightDistance), (std::min)(topDistance, bottomDistance));
|
||||
|
||||
if (nearestEdge == leftDistance && leftDistance <= minHorizontalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Left;
|
||||
}
|
||||
if (nearestEdge == rightDistance && rightDistance <= minHorizontalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Right;
|
||||
}
|
||||
if (nearestEdge == topDistance && topDistance <= minVerticalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Top;
|
||||
}
|
||||
if (nearestEdge == bottomDistance && bottomDistance <= minVerticalThreshold) {
|
||||
return UIEditorWorkspaceDockPlacement::Bottom;
|
||||
}
|
||||
|
||||
return UIEditorWorkspaceDockPlacement::Center;
|
||||
}
|
||||
|
||||
void SyncDockPreview(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout) {
|
||||
state.dockHostState.dropPreview = {};
|
||||
if (state.activeTabDragNodeId.empty() ||
|
||||
state.activeTabDragPanelId.empty() ||
|
||||
!state.hasPointerPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (!IsPointInsideRect(tabStack.bounds, state.pointerPosition)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceDockPlacement placement =
|
||||
ResolveDockPlacement(tabStack, state.pointerPosition);
|
||||
if (tabStack.nodeId == state.activeTabDragNodeId &&
|
||||
placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabStack.nodeId == state.activeTabDragNodeId &&
|
||||
tabStack.items.size() <= 1u) {
|
||||
return;
|
||||
}
|
||||
|
||||
Widgets::UIEditorDockHostDropPreviewState preview = {};
|
||||
preview.visible = true;
|
||||
preview.sourceNodeId = state.activeTabDragNodeId;
|
||||
preview.sourcePanelId = state.activeTabDragPanelId;
|
||||
preview.targetNodeId = tabStack.nodeId;
|
||||
preview.placement = placement;
|
||||
if (placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
preview.insertionIndex =
|
||||
ResolveTabHeaderDropInsertionIndex(tabStack, state.pointerPosition);
|
||||
if (preview.insertionIndex == Widgets::UIEditorTabStripInvalidIndex) {
|
||||
preview.insertionIndex = tabStack.items.size();
|
||||
}
|
||||
}
|
||||
state.dockHostState.dropPreview = std::move(preview);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SyncHoverTarget(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout) {
|
||||
@@ -290,6 +460,11 @@ void SyncHoverTarget(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.activeTabDragNodeId.empty()) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.hasPointerPosition) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
return;
|
||||
@@ -340,6 +515,20 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
||||
? ProcessTabStripEvent(state, layout, event, metrics)
|
||||
: DockHostTabStripEventResult {};
|
||||
eventResult.requestPointerCapture = tabStripResult.requestPointerCapture;
|
||||
eventResult.releasePointerCapture = tabStripResult.releasePointerCapture;
|
||||
if (!tabStripResult.draggedTabId.empty() &&
|
||||
!state.activeTabDragNodeId.empty()) {
|
||||
state.activeTabDragPanelId = tabStripResult.draggedTabId;
|
||||
}
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
!state.activeTabDragPanelId.empty() &&
|
||||
!state.splitterDragState.active) {
|
||||
SyncDockPreview(state, layout);
|
||||
} else if (event.type == UIInputEventType::PointerLeave ||
|
||||
state.activeTabDragNodeId.empty()) {
|
||||
state.dockHostState.dropPreview = {};
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case UIInputEventType::FocusGained:
|
||||
@@ -355,6 +544,12 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture = true;
|
||||
}
|
||||
if (!state.activeTabDragNodeId.empty() || tabStripResult.dragCanceled) {
|
||||
ClearTabDockDragState(state);
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture =
|
||||
eventResult.releasePointerCapture || tabStripResult.releasePointerCapture;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
@@ -381,6 +576,21 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
eventResult.hitTarget.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
|
||||
eventResult.hitTarget.nodeId = state.dockHostState.activeSplitterNodeId;
|
||||
}
|
||||
} else if (tabStripResult.priority > 0) {
|
||||
eventResult.consumed = tabStripResult.consumed || tabStripResult.dragStarted;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
if (tabStripResult.dragStarted) {
|
||||
state.activeTabDragNodeId = tabStripResult.nodeId;
|
||||
state.activeTabDragPanelId = tabStripResult.draggedTabId;
|
||||
SyncDockPreview(state, layout);
|
||||
}
|
||||
if (tabStripResult.dragEnded || tabStripResult.dragCanceled) {
|
||||
ClearTabDockDragState(state);
|
||||
}
|
||||
if (eventResult.consumed ||
|
||||
eventResult.hitTarget.kind != UIEditorDockHostHitTargetKind::None) {
|
||||
state.dockHostState.focused = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -388,6 +598,7 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
if (!state.splitterDragState.active) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
}
|
||||
state.dockHostState.dropPreview = {};
|
||||
if (!HasFocusedTabStrip(state)) {
|
||||
state.dockHostState.focused = false;
|
||||
}
|
||||
@@ -464,6 +675,96 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.reorderRequested &&
|
||||
!tabStripResult.nodeId.empty() &&
|
||||
!tabStripResult.draggedTabId.empty() &&
|
||||
tabStripResult.dropInsertionIndex != Widgets::UIEditorTabStripInvalidIndex) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "same-stack reorder node=" << tabStripResult.nodeId
|
||||
<< " panel=" << tabStripResult.draggedTabId
|
||||
<< " insertion=" << tabStripResult.dropInsertionIndex;
|
||||
AppendUIEditorRuntimeTrace("dock", trace.str());
|
||||
}
|
||||
eventResult.layoutResult = controller.ReorderTab(
|
||||
tabStripResult.nodeId,
|
||||
tabStripResult.draggedTabId,
|
||||
tabStripResult.dropInsertionIndex);
|
||||
eventResult.layoutChanged =
|
||||
eventResult.layoutResult.status ==
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.dockHostState.dropPreview.visible &&
|
||||
!state.activeTabDragNodeId.empty() &&
|
||||
!state.activeTabDragPanelId.empty()) {
|
||||
const Widgets::UIEditorDockHostDropPreviewState preview =
|
||||
state.dockHostState.dropPreview;
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "drop commit sourceNode=" << state.activeTabDragNodeId
|
||||
<< " panel=" << state.activeTabDragPanelId
|
||||
<< " targetNode=" << preview.targetNodeId
|
||||
<< " placement=" << static_cast<int>(preview.placement)
|
||||
<< " insertion=" << preview.insertionIndex;
|
||||
AppendUIEditorRuntimeTrace("dock", trace.str());
|
||||
}
|
||||
if (preview.placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
std::size_t insertionIndex = preview.insertionIndex;
|
||||
if (insertionIndex == Widgets::UIEditorTabStripInvalidIndex) {
|
||||
if (const UIEditorDockHostTabStackLayout* targetTabStack =
|
||||
FindTabStackLayoutByNodeId(layout, preview.targetNodeId);
|
||||
targetTabStack != nullptr) {
|
||||
insertionIndex = targetTabStack->items.size();
|
||||
} else {
|
||||
insertionIndex = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
eventResult.layoutResult = controller.MoveTabToStack(
|
||||
state.activeTabDragNodeId,
|
||||
state.activeTabDragPanelId,
|
||||
preview.targetNodeId,
|
||||
insertionIndex);
|
||||
} else {
|
||||
eventResult.layoutResult = controller.DockTabRelative(
|
||||
state.activeTabDragNodeId,
|
||||
state.activeTabDragPanelId,
|
||||
preview.targetNodeId,
|
||||
preview.placement);
|
||||
}
|
||||
eventResult.layoutChanged =
|
||||
eventResult.layoutResult.status ==
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
AppendUIEditorRuntimeTrace(
|
||||
"dock",
|
||||
"drop result status=" +
|
||||
std::string(
|
||||
GetUIEditorWorkspaceLayoutOperationStatusName(
|
||||
eventResult.layoutResult.status)) +
|
||||
" message=" + eventResult.layoutResult.message);
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget.nodeId = preview.targetNodeId;
|
||||
eventResult.releasePointerCapture =
|
||||
eventResult.releasePointerCapture || tabStripResult.releasePointerCapture;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.dragEnded || tabStripResult.dragCanceled) {
|
||||
eventResult.consumed = tabStripResult.consumed;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
@@ -534,6 +835,14 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
break;
|
||||
|
||||
case UIInputEventType::KeyDown:
|
||||
if (tabStripResult.dragCanceled) {
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
|
||||
@@ -11,8 +11,6 @@ using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
constexpr float kMenuBarFontSize = 13.0f;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
@@ -24,6 +22,10 @@ bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) {
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool IsVisibleColor(const UIColor& color) {
|
||||
return color.a > 0.0f;
|
||||
}
|
||||
|
||||
float ResolveEstimatedLabelWidth(
|
||||
const UIEditorMenuBarItem& item,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
@@ -35,7 +37,9 @@ float ResolveEstimatedLabelWidth(
|
||||
}
|
||||
|
||||
float ResolveLabelTop(const UIRect& rect, const UIEditorMenuBarMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kMenuBarFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
return rect.y +
|
||||
(std::max)(0.0f, (rect.height - ClampNonNegative(metrics.labelFontSize)) * 0.5f) +
|
||||
metrics.labelInsetY;
|
||||
}
|
||||
|
||||
bool IsButtonFocused(
|
||||
@@ -164,25 +168,38 @@ void AppendUIEditorMenuBarBackground(
|
||||
const UIEditorMenuBarPalette& palette,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.barColor, metrics.barCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness,
|
||||
metrics.barCornerRounding);
|
||||
const UIColor barBorderColor =
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor;
|
||||
const float barBorderThickness =
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness;
|
||||
if (IsVisibleColor(barBorderColor) && barBorderThickness > 0.0f) {
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
barBorderColor,
|
||||
barBorderThickness,
|
||||
metrics.barCornerRounding);
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
|
||||
const bool open = state.openIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
const bool focused = IsButtonFocused(state, index);
|
||||
drawList.AddFilledRect(
|
||||
layout.buttonRects[index],
|
||||
ResolveButtonFillColor(open, hovered, palette),
|
||||
metrics.buttonCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.buttonRects[index],
|
||||
ResolveButtonBorderColor(open, focused, palette),
|
||||
ResolveButtonBorderThickness(open, focused, metrics),
|
||||
metrics.buttonCornerRounding);
|
||||
const UIColor buttonFillColor = ResolveButtonFillColor(open, hovered, palette);
|
||||
const UIColor buttonBorderColor = ResolveButtonBorderColor(open, focused, palette);
|
||||
const float buttonBorderThickness = ResolveButtonBorderThickness(open, focused, metrics);
|
||||
if (IsVisibleColor(buttonFillColor)) {
|
||||
drawList.AddFilledRect(
|
||||
layout.buttonRects[index],
|
||||
buttonFillColor,
|
||||
metrics.buttonCornerRounding);
|
||||
}
|
||||
if (IsVisibleColor(buttonBorderColor) && buttonBorderThickness > 0.0f) {
|
||||
drawList.AddRectOutline(
|
||||
layout.buttonRects[index],
|
||||
buttonBorderColor,
|
||||
buttonBorderThickness,
|
||||
metrics.buttonCornerRounding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +223,7 @@ void AppendUIEditorMenuBarForeground(
|
||||
UIPoint(textLeft, ResolveLabelTop(rect, metrics)),
|
||||
items[index].label,
|
||||
items[index].enabled ? palette.textPrimary : palette.textDisabled,
|
||||
kMenuBarFontSize);
|
||||
ClampNonNegative(metrics.labelFontSize));
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +171,14 @@ void AppendUIEditorMenuPopupBackground(
|
||||
const UIRect& rect = layout.itemRects[index];
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
const float lineY = rect.y + rect.height * 0.5f;
|
||||
const float separatorInset =
|
||||
ClampNonNegative(metrics.contentPaddingX) + 3.0f;
|
||||
drawList.AddFilledRect(
|
||||
UIRect(rect.x + 8.0f, lineY, (std::max)(rect.width - 16.0f, 0.0f), metrics.separatorThickness),
|
||||
UIRect(
|
||||
rect.x + separatorInset,
|
||||
lineY,
|
||||
(std::max)(rect.width - separatorInset * 2.0f, 0.0f),
|
||||
metrics.separatorThickness),
|
||||
palette.separatorColor);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -191,6 +191,19 @@ void AppendUIEditorShellToolbar(
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
const std::vector<Widgets::UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorShellComposeMetrics& metrics) {
|
||||
return BuildUIEditorShellComposeLayout(
|
||||
bounds,
|
||||
menuBarItems,
|
||||
{},
|
||||
statusSegments,
|
||||
metrics);
|
||||
}
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
|
||||
@@ -148,7 +148,9 @@ const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(
|
||||
}
|
||||
|
||||
std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
||||
const UIEditorResolvedMenuModel& model) {
|
||||
const UIEditorResolvedMenuModel& model,
|
||||
const UIEditorShellInteractionServices& services,
|
||||
const Widgets::UIEditorMenuBarMetrics& metrics) {
|
||||
std::vector<Widgets::UIEditorMenuBarItem> items = {};
|
||||
items.reserve(model.menus.size());
|
||||
|
||||
@@ -157,6 +159,10 @@ std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
||||
item.menuId = menu.menuId;
|
||||
item.label = menu.label;
|
||||
item.enabled = !menu.items.empty();
|
||||
if (services.textMeasurer != nullptr && !item.label.empty()) {
|
||||
item.desiredLabelWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { item.label, metrics.labelFontSize });
|
||||
}
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
|
||||
@@ -175,7 +181,9 @@ UIEditorShellComposeModel BuildShellComposeModel(
|
||||
}
|
||||
|
||||
std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
||||
const std::vector<UIEditorResolvedMenuItem>& items) {
|
||||
const std::vector<UIEditorResolvedMenuItem>& items,
|
||||
const UIEditorShellInteractionServices& services,
|
||||
const Widgets::UIEditorMenuPopupMetrics& metrics) {
|
||||
std::vector<Widgets::UIEditorMenuPopupItem> widgetItems = {};
|
||||
widgetItems.reserve(items.size());
|
||||
|
||||
@@ -188,6 +196,16 @@ std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
||||
widgetItem.enabled = item.enabled;
|
||||
widgetItem.checked = item.checked;
|
||||
widgetItem.hasSubmenu = item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty();
|
||||
if (services.textMeasurer != nullptr) {
|
||||
if (!widgetItem.label.empty()) {
|
||||
widgetItem.desiredLabelWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { widgetItem.label, metrics.labelFontSize });
|
||||
}
|
||||
if (!widgetItem.shortcutText.empty()) {
|
||||
widgetItem.desiredShortcutWidth = services.textMeasurer->MeasureTextWidth(
|
||||
UIEditorTextMeasureRequest { widgetItem.shortcutText, metrics.labelFontSize });
|
||||
}
|
||||
}
|
||||
widgetItems.push_back(std::move(widgetItem));
|
||||
}
|
||||
|
||||
@@ -244,10 +262,14 @@ BuildRequestOutput BuildRequest(
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShellInteractionModel& model,
|
||||
const UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionMetrics& metrics) {
|
||||
const UIEditorShellInteractionMetrics& metrics,
|
||||
const UIEditorShellInteractionServices& services) {
|
||||
BuildRequestOutput output = {};
|
||||
UIEditorShellInteractionRequest& request = output.request;
|
||||
request.menuBarItems = BuildMenuBarItems(model.resolvedMenuModel);
|
||||
request.menuBarItems = BuildMenuBarItems(
|
||||
model.resolvedMenuModel,
|
||||
services,
|
||||
metrics.shellMetrics.menuBarMetrics);
|
||||
|
||||
const UIEditorShellComposeModel shellModel =
|
||||
BuildShellComposeModel(model, request.menuBarItems);
|
||||
@@ -293,7 +315,10 @@ BuildRequestOutput BuildRequest(
|
||||
popupRequest.sourceItemId = popupState.itemId;
|
||||
popupRequest.overlayEntry = *overlayEntry;
|
||||
popupRequest.resolvedItems = *resolvedItems;
|
||||
popupRequest.widgetItems = BuildPopupWidgetItems(popupRequest.resolvedItems);
|
||||
popupRequest.widgetItems = BuildPopupWidgetItems(
|
||||
popupRequest.resolvedItems,
|
||||
services,
|
||||
metrics.popupMetrics);
|
||||
|
||||
const float popupWidth =
|
||||
ResolveUIEditorMenuPopupDesiredWidth(popupRequest.widgetItems, metrics.popupMetrics);
|
||||
@@ -506,7 +531,8 @@ UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
@@ -525,7 +551,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics);
|
||||
metrics,
|
||||
services);
|
||||
UIEditorShellInteractionRequest request = std::move(requestBuild.request);
|
||||
|
||||
if (requestBuild.hadInvalidPopupState && state.menuSession.HasOpenMenu()) {
|
||||
@@ -539,7 +566,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics);
|
||||
metrics,
|
||||
services);
|
||||
request = std::move(requestBuild.request);
|
||||
}
|
||||
|
||||
@@ -691,7 +719,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +743,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
metrics,
|
||||
services).request;
|
||||
|
||||
const RequestHit finalHit =
|
||||
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
|
||||
|
||||
@@ -110,21 +110,13 @@ UIColor ResolveSurfaceBorderColor(
|
||||
return palette.surfaceCapturedBorderColor;
|
||||
}
|
||||
|
||||
if (state.surfaceActive) {
|
||||
return palette.surfaceActiveBorderColor;
|
||||
}
|
||||
|
||||
if (state.surfaceHovered) {
|
||||
return palette.surfaceHoveredBorderColor;
|
||||
}
|
||||
|
||||
return palette.surfaceBorderColor;
|
||||
}
|
||||
|
||||
float ResolveSurfaceBorderThickness(
|
||||
const UIEditorViewportSlotState& state,
|
||||
const UIEditorViewportSlotMetrics& metrics) {
|
||||
if (state.inputCaptured || state.focused) {
|
||||
if (state.inputCaptured) {
|
||||
return metrics.focusedSurfaceBorderThickness;
|
||||
}
|
||||
|
||||
@@ -356,8 +348,8 @@ void AppendUIEditorViewportSlotBackground(
|
||||
drawList.AddFilledRect(layout.bounds, palette.frameColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.outerBorderThickness,
|
||||
palette.borderColor,
|
||||
metrics.outerBorderThickness,
|
||||
metrics.cornerRounding);
|
||||
|
||||
if (layout.hasTopBar) {
|
||||
@@ -366,12 +358,6 @@ void AppendUIEditorViewportSlotBackground(
|
||||
|
||||
drawList.AddFilledRect(layout.surfaceRect, palette.surfaceColor);
|
||||
|
||||
if (state.surfaceHovered) {
|
||||
drawList.AddFilledRect(layout.inputRect, palette.surfaceHoverOverlayColor);
|
||||
}
|
||||
if (state.surfaceActive) {
|
||||
drawList.AddFilledRect(layout.inputRect, palette.surfaceActiveOverlayColor);
|
||||
}
|
||||
if (state.inputCaptured) {
|
||||
drawList.AddFilledRect(layout.inputRect, palette.captureOverlayColor);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
FindUIEditorPanelSessionState(session, panelId);
|
||||
return panelState != nullptr && panelState->open && panelState->visible;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
@@ -22,6 +32,58 @@ std::vector<std::string> CollectVisiblePanelIds(
|
||||
return ids;
|
||||
}
|
||||
|
||||
struct VisibleTabStackInfo {
|
||||
bool panelExists = false;
|
||||
bool panelVisible = false;
|
||||
std::size_t currentVisibleIndex = 0u;
|
||||
std::size_t visibleTabCount = 0u;
|
||||
};
|
||||
|
||||
VisibleTabStackInfo ResolveVisibleTabStackInfo(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
VisibleTabStackInfo info = {};
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool visible = IsPanelOpenAndVisible(session, child.panel.panelId);
|
||||
if (child.panel.panelId == panelId) {
|
||||
info.panelExists = true;
|
||||
info.panelVisible = visible;
|
||||
if (visible) {
|
||||
info.currentVisibleIndex = info.visibleTabCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
++info.visibleTabCount;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::size_t CountVisibleTabs(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
std::size_t visibleCount = 0u;
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisible(session, child.panel.panelId)) {
|
||||
++visibleCount;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleCount;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind) {
|
||||
@@ -297,6 +359,290 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRati
|
||||
"Split ratio updated.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::ReorderTab(
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (nodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab requires a tab stack node id.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab requires a panel id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* tabStack = FindUIEditorWorkspaceNode(m_workspace, nodeId);
|
||||
if (tabStack == nullptr || tabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo tabInfo =
|
||||
ResolveVisibleTabStackInfo(*tabStack, m_session, panelId);
|
||||
if (!tabInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target panel is missing from the specified tab stack.");
|
||||
}
|
||||
|
||||
if (!tabInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex > tabInfo.visibleTabCount) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"ReorderTab target visible insertion index is out of range.");
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == tabInfo.currentVisibleIndex ||
|
||||
targetVisibleInsertionIndex == tabInfo.currentVisibleIndex + 1u) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Visible tab order already matches the requested insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryReorderUIEditorWorkspaceTab(
|
||||
m_workspace,
|
||||
m_session,
|
||||
nodeId,
|
||||
panelId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Tab reorder rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Visible tab order already matches the requested insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Tab reorder produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab reordered.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::MoveTabToStack(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "MoveTabToStack begin sourceNode=" << sourceNodeId
|
||||
<< " panel=" << panelId
|
||||
<< " targetNode=" << targetNodeId
|
||||
<< " insertion=" << targetVisibleInsertionIndex;
|
||||
AppendUIEditorRuntimeTrace("workspace", trace.str());
|
||||
}
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() || targetNodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires both source and target tab stack ids.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires a panel id.");
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId) {
|
||||
return ReorderTab(sourceNodeId, panelId, targetVisibleInsertionIndex);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, targetNodeId);
|
||||
if (sourceTabStack == nullptr ||
|
||||
targetTabStack == nullptr ||
|
||||
sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack source or target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo sourceInfo =
|
||||
ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId);
|
||||
if (!sourceInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack target panel is missing from the source tab stack.");
|
||||
}
|
||||
|
||||
if (!sourceInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
const std::size_t visibleTargetCount = CountVisibleTabs(*targetTabStack, m_session);
|
||||
if (targetVisibleInsertionIndex > visibleTargetCount) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack target visible insertion index is out of range.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryMoveUIEditorWorkspaceTabToStack(
|
||||
m_workspace,
|
||||
m_session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Tab already matches the requested target stack insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab moved to target stack.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::DockTabRelative(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "DockTabRelative begin sourceNode=" << sourceNodeId
|
||||
<< " panel=" << panelId
|
||||
<< " targetNode=" << targetNodeId
|
||||
<< " placement=" << static_cast<int>(placement)
|
||||
<< " splitRatio=" << splitRatio;
|
||||
AppendUIEditorRuntimeTrace("workspace", trace.str());
|
||||
}
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() || targetNodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative requires both source and target tab stack ids.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative requires a panel id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, targetNodeId);
|
||||
if (sourceTabStack == nullptr ||
|
||||
targetTabStack == nullptr ||
|
||||
sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative source or target tab stack is missing.");
|
||||
}
|
||||
|
||||
const VisibleTabStackInfo sourceInfo =
|
||||
ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId);
|
||||
if (!sourceInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative target panel is missing from the source tab stack.");
|
||||
}
|
||||
|
||||
if (!sourceInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryDockUIEditorWorkspaceTabRelative(
|
||||
m_workspace,
|
||||
m_session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Dock layout already matches the requested placement.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab docked relative to target stack.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
|
||||
const UIEditorWorkspaceCommand& command) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
@@ -43,6 +44,19 @@ UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelN
|
||||
return tabStack;
|
||||
}
|
||||
|
||||
void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::Split ||
|
||||
node.children.size() != 1u) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the remaining child through a temporary object first. Assigning
|
||||
// directly from node.children.front() aliases a subobject of node and can
|
||||
// trigger use-after-move when the vector storage is torn down.
|
||||
UIEditorWorkspaceNode remainingChild = std::move(node.children.front());
|
||||
node = std::move(remainingChild);
|
||||
}
|
||||
|
||||
void CanonicalizeNodeRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
bool allowStandalonePanelLeaf) {
|
||||
@@ -67,7 +81,7 @@ void CanonicalizeNodeRecursive(
|
||||
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
||||
node.children.size() == 1u) {
|
||||
node = std::move(node.children.front());
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +145,256 @@ UIEditorWorkspaceNode* FindMutableNodeRecursive(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FindNodePathRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId,
|
||||
std::vector<std::size_t>& path) {
|
||||
if (node.nodeId == nodeId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
path.push_back(index);
|
||||
if (FindNodePathRecursive(node.children[index], nodeId, path)) {
|
||||
return true;
|
||||
}
|
||||
path.pop_back();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* ResolveMutableNodeByPath(
|
||||
UIEditorWorkspaceNode& node,
|
||||
const std::vector<std::size_t>& path) {
|
||||
UIEditorWorkspaceNode* current = &node;
|
||||
for (const std::size_t childIndex : path) {
|
||||
if (childIndex >= current->children.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
current = ¤t->children[childIndex];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
bool IsPanelOpenAndVisibleInSession(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId);
|
||||
return state != nullptr && state->open && state->visible;
|
||||
}
|
||||
|
||||
std::size_t CountVisibleChildren(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
std::size_t visibleCount = 0u;
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
++visibleCount;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleCount;
|
||||
}
|
||||
|
||||
std::size_t ResolveActualInsertionIndexForVisibleInsertion(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
std::vector<std::size_t> visibleIndices = {};
|
||||
visibleIndices.reserve(node.children.size());
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = node.children[index];
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
visibleIndices.push_back(index);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == 0u) {
|
||||
return visibleIndices.empty() ? 0u : visibleIndices.front();
|
||||
}
|
||||
|
||||
if (visibleIndices.empty()) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex >= visibleIndices.size()) {
|
||||
return visibleIndices.back() + 1u;
|
||||
}
|
||||
|
||||
return visibleIndices[targetVisibleInsertionIndex];
|
||||
}
|
||||
|
||||
void FixTabStackSelectedIndex(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view preferredPanelId) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack || node.children.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
if (node.children[index].kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
node.children[index].panel.panelId == preferredPanelId) {
|
||||
node.selectedTabIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.selectedTabIndex >= node.children.size()) {
|
||||
node.selectedTabIndex = node.children.size() - 1u;
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoveNodeByIdRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
if (node.children[index].nodeId == nodeId) {
|
||||
node.children.erase(node.children.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (node.children.size() == 1u) {
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (UIEditorWorkspaceNode& child : node.children) {
|
||||
if (RemoveNodeByIdRecursive(child, nodeId)) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
||||
node.children.size() == 1u) {
|
||||
CollapseSplitNodeToOnlyChild(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
float ClampDockSplitRatio(float value) {
|
||||
constexpr float kMinRatio = 0.1f;
|
||||
constexpr float kMaxRatio = 0.9f;
|
||||
return (std::min)(kMaxRatio, (std::max)(kMinRatio, value));
|
||||
}
|
||||
|
||||
bool IsLeadingDockPlacement(UIEditorWorkspaceDockPlacement placement) {
|
||||
return placement == UIEditorWorkspaceDockPlacement::Left ||
|
||||
placement == UIEditorWorkspaceDockPlacement::Top;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSplitAxis ResolveDockSplitAxis(UIEditorWorkspaceDockPlacement placement) {
|
||||
return placement == UIEditorWorkspaceDockPlacement::Left ||
|
||||
placement == UIEditorWorkspaceDockPlacement::Right
|
||||
? UIEditorWorkspaceSplitAxis::Horizontal
|
||||
: UIEditorWorkspaceSplitAxis::Vertical;
|
||||
}
|
||||
|
||||
std::string MakeUniqueNodeId(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string base) {
|
||||
if (base.empty()) {
|
||||
base = "workspace-node";
|
||||
}
|
||||
|
||||
if (FindUIEditorWorkspaceNode(workspace, base) == nullptr) {
|
||||
return base;
|
||||
}
|
||||
|
||||
for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) {
|
||||
const std::string candidate = base + "-" + std::to_string(suffix);
|
||||
if (FindUIEditorWorkspaceNode(workspace, candidate) == nullptr) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return base + "-overflow";
|
||||
}
|
||||
|
||||
bool TryExtractVisiblePanelFromTabStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceNode& extractedPanel) {
|
||||
std::vector<std::size_t> sourcePath = {};
|
||||
if (!FindNodePathRecursive(workspace.root, sourceNodeId, sourcePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* sourceStack =
|
||||
ResolveMutableNodeByPath(workspace.root, sourcePath);
|
||||
if (sourceStack == nullptr ||
|
||||
sourceStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t panelIndex = sourceStack->children.size();
|
||||
for (std::size_t index = 0; index < sourceStack->children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = sourceStack->children[index];
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (child.panel.panelId == panelId) {
|
||||
if (!IsPanelOpenAndVisibleInSession(session, panelId)) {
|
||||
return false;
|
||||
}
|
||||
panelIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (panelIndex >= sourceStack->children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourcePath.empty() && sourceStack->children.size() == 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string fallbackSelectedPanelId = {};
|
||||
if (sourceStack->selectedTabIndex < sourceStack->children.size()) {
|
||||
fallbackSelectedPanelId =
|
||||
sourceStack->children[sourceStack->selectedTabIndex].panel.panelId;
|
||||
}
|
||||
|
||||
extractedPanel = std::move(sourceStack->children[panelIndex]);
|
||||
sourceStack->children.erase(
|
||||
sourceStack->children.begin() + static_cast<std::ptrdiff_t>(panelIndex));
|
||||
|
||||
if (sourceStack->children.empty()) {
|
||||
if (sourcePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RemoveNodeByIdRecursive(workspace.root, sourceNodeId)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (fallbackSelectedPanelId == panelId) {
|
||||
const std::size_t nextIndex =
|
||||
(std::min)(panelIndex, sourceStack->children.size() - 1u);
|
||||
fallbackSelectedPanelId =
|
||||
sourceStack->children[nextIndex].panel.panelId;
|
||||
}
|
||||
FixTabStackSelectedIndex(*sourceStack, fallbackSelectedPanelId);
|
||||
}
|
||||
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryActivateRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId) {
|
||||
@@ -484,4 +748,266 @@ bool TrySetUIEditorWorkspaceSplitRatio(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryReorderUIEditorWorkspaceTab(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
UIEditorWorkspaceNode* node = FindMutableNodeRecursive(workspace.root, nodeId);
|
||||
if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::size_t> visibleChildIndices = {};
|
||||
std::vector<UIEditorWorkspaceNode> reorderedVisibleChildren = {};
|
||||
visibleChildIndices.reserve(node->children.size());
|
||||
reorderedVisibleChildren.reserve(node->children.size());
|
||||
|
||||
std::size_t sourceVisibleIndex = node->children.size();
|
||||
for (std::size_t index = 0; index < node->children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = node->children[index];
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child.panel.panelId == panelId) {
|
||||
sourceVisibleIndex = visibleChildIndices.size();
|
||||
}
|
||||
|
||||
visibleChildIndices.push_back(index);
|
||||
reorderedVisibleChildren.push_back(child);
|
||||
}
|
||||
|
||||
if (sourceVisibleIndex >= reorderedVisibleChildren.size() ||
|
||||
targetVisibleInsertionIndex > reorderedVisibleChildren.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex == sourceVisibleIndex ||
|
||||
targetVisibleInsertionIndex == sourceVisibleIndex + 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode movedChild =
|
||||
std::move(reorderedVisibleChildren[sourceVisibleIndex]);
|
||||
reorderedVisibleChildren.erase(
|
||||
reorderedVisibleChildren.begin() + static_cast<std::ptrdiff_t>(sourceVisibleIndex));
|
||||
|
||||
std::size_t adjustedInsertionIndex = targetVisibleInsertionIndex;
|
||||
if (adjustedInsertionIndex > sourceVisibleIndex) {
|
||||
--adjustedInsertionIndex;
|
||||
}
|
||||
if (adjustedInsertionIndex > reorderedVisibleChildren.size()) {
|
||||
adjustedInsertionIndex = reorderedVisibleChildren.size();
|
||||
}
|
||||
|
||||
reorderedVisibleChildren.insert(
|
||||
reorderedVisibleChildren.begin() +
|
||||
static_cast<std::ptrdiff_t>(adjustedInsertionIndex),
|
||||
std::move(movedChild));
|
||||
|
||||
std::string selectedPanelId = {};
|
||||
if (node->selectedTabIndex < node->children.size()) {
|
||||
selectedPanelId = node->children[node->selectedTabIndex].panel.panelId;
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceNode> originalChildren = node->children;
|
||||
std::size_t nextVisibleIndex = 0u;
|
||||
for (std::size_t index = 0; index < originalChildren.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& originalChild = originalChildren[index];
|
||||
if (!IsPanelOpenAndVisibleInSession(session, originalChild.panel.panelId)) {
|
||||
node->children[index] = originalChild;
|
||||
continue;
|
||||
}
|
||||
|
||||
node->children[index] = reorderedVisibleChildren[nextVisibleIndex];
|
||||
++nextVisibleIndex;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node->children.size(); ++index) {
|
||||
if (node->children[index].panel.panelId == selectedPanelId) {
|
||||
node->selectedTabIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMoveUIEditorWorkspaceTabToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
if (sourceNodeId.empty() ||
|
||||
panelId.empty() ||
|
||||
targetNodeId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId) {
|
||||
return TryReorderUIEditorWorkspaceTab(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetVisibleInsertionIndex);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (targetNode == nullptr ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex > CountVisibleChildren(*targetNode, session)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode extractedPanel = {};
|
||||
if (!TryExtractVisiblePanelFromTabStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t actualInsertionIndex =
|
||||
ResolveActualInsertionIndexForVisibleInsertion(
|
||||
*targetStack,
|
||||
session,
|
||||
targetVisibleInsertionIndex);
|
||||
if (actualInsertionIndex > targetStack->children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
targetStack->children.insert(
|
||||
targetStack->children.begin() +
|
||||
static_cast<std::ptrdiff_t>(actualInsertionIndex),
|
||||
std::move(extractedPanel));
|
||||
targetStack->selectedTabIndex = actualInsertionIndex;
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryDockUIEditorWorkspaceTabRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
if (placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (targetNode == nullptr ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryMoveUIEditorWorkspaceTabToStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
CountVisibleChildren(*targetNode, session));
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() ||
|
||||
panelId.empty() ||
|
||||
targetNodeId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceNode =
|
||||
FindUIEditorWorkspaceNode(workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (sourceNode == nullptr ||
|
||||
targetNode == nullptr ||
|
||||
sourceNode->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId &&
|
||||
sourceNode->children.size() <= 1u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode extractedPanel = {};
|
||||
if (!TryExtractVisiblePanelFromTabStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string movedStackNodeId = MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_" + std::string(panelId) + "_stack");
|
||||
UIEditorWorkspaceNode movedStack = {};
|
||||
movedStack.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
movedStack.nodeId = movedStackNodeId;
|
||||
movedStack.selectedTabIndex = 0u;
|
||||
movedStack.children.push_back(std::move(extractedPanel));
|
||||
|
||||
UIEditorWorkspaceNode existingTarget = std::move(*targetStack);
|
||||
UIEditorWorkspaceNode primary = {};
|
||||
UIEditorWorkspaceNode secondary = {};
|
||||
if (IsLeadingDockPlacement(placement)) {
|
||||
primary = std::move(movedStack);
|
||||
secondary = std::move(existingTarget);
|
||||
} else {
|
||||
primary = std::move(existingTarget);
|
||||
secondary = std::move(movedStack);
|
||||
}
|
||||
|
||||
const float requestedRatio = ClampDockSplitRatio(splitRatio);
|
||||
const float resolvedSplitRatio =
|
||||
IsLeadingDockPlacement(placement)
|
||||
? requestedRatio
|
||||
: (1.0f - requestedRatio);
|
||||
*targetStack = BuildUIEditorWorkspaceSplit(
|
||||
MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_split"),
|
||||
ResolveDockSplitAxis(placement),
|
||||
resolvedSplitRatio,
|
||||
std::move(primary),
|
||||
std::move(secondary));
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
|
||||
Reference in New Issue
Block a user