Add dock host interaction contract validation
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Core/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Widgets/UIEditorDockHost.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct UIEditorDockHostInteractionState {
|
||||
Widgets::UIEditorDockHostState dockHostState = {};
|
||||
::XCEngine::UI::Widgets::UISplitterDragState splitterDragState = {};
|
||||
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||
bool hasPointerPosition = false;
|
||||
};
|
||||
|
||||
struct UIEditorDockHostInteractionResult {
|
||||
bool consumed = false;
|
||||
bool commandExecuted = false;
|
||||
bool layoutChanged = false;
|
||||
bool requestPointerCapture = false;
|
||||
bool releasePointerCapture = false;
|
||||
Widgets::UIEditorDockHostHitTarget hitTarget = {};
|
||||
std::string activeSplitterNodeId = {};
|
||||
UIEditorWorkspaceCommandResult commandResult = {};
|
||||
UIEditorWorkspaceLayoutOperationResult layoutResult = {};
|
||||
};
|
||||
|
||||
struct UIEditorDockHostInteractionFrame {
|
||||
Widgets::UIEditorDockHostLayout layout = {};
|
||||
UIEditorDockHostInteractionResult result = {};
|
||||
bool focused = false;
|
||||
};
|
||||
|
||||
UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
UIEditorWorkspaceController& controller,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics = {});
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -102,6 +102,9 @@ public:
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot);
|
||||
UIEditorWorkspaceLayoutOperationResult RestoreSerializedLayout(
|
||||
std::string_view serializedLayout);
|
||||
UIEditorWorkspaceLayoutOperationResult SetSplitRatio(
|
||||
std::string_view nodeId,
|
||||
float splitRatio);
|
||||
UIEditorWorkspaceCommandResult Dispatch(const UIEditorWorkspaceCommand& command);
|
||||
|
||||
private:
|
||||
|
||||
304
new_editor/src/Core/UIEditorDockHostInteraction.cpp
Normal file
304
new_editor/src/Core/UIEditorDockHostInteraction.cpp
Normal file
@@ -0,0 +1,304 @@
|
||||
#include <XCEditor/Core/UIEditorDockHostInteraction.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Widgets::BeginUISplitterDrag;
|
||||
using ::XCEngine::UI::Widgets::EndUISplitterDrag;
|
||||
using ::XCEngine::UI::Widgets::UpdateUISplitterDrag;
|
||||
using Widgets::BuildUIEditorDockHostLayout;
|
||||
using Widgets::FindUIEditorDockHostSplitterLayout;
|
||||
using Widgets::HitTestUIEditorDockHost;
|
||||
using Widgets::UIEditorDockHostHitTarget;
|
||||
using Widgets::UIEditorDockHostHitTargetKind;
|
||||
|
||||
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult ApplySplitRatio(
|
||||
UIEditorWorkspaceController& controller,
|
||||
std::string_view nodeId,
|
||||
float splitRatio) {
|
||||
return controller.SetSplitRatio(nodeId, splitRatio);
|
||||
}
|
||||
|
||||
void SyncHoverTarget(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout) {
|
||||
if (state.splitterDragState.active) {
|
||||
state.dockHostState.hoveredTarget = {
|
||||
UIEditorDockHostHitTargetKind::SplitterHandle,
|
||||
state.dockHostState.activeSplitterNodeId,
|
||||
{},
|
||||
Widgets::UIEditorTabStripInvalidIndex
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.hasPointerPosition) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
return;
|
||||
}
|
||||
|
||||
state.dockHostState.hoveredTarget =
|
||||
HitTestUIEditorDockHost(layout, state.pointerPosition);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult DispatchPanelCommand(
|
||||
UIEditorWorkspaceController& controller,
|
||||
UIEditorWorkspaceCommandKind kind,
|
||||
std::string panelId) {
|
||||
UIEditorWorkspaceCommand command = {};
|
||||
command.kind = kind;
|
||||
command.panelId = std::move(panelId);
|
||||
return controller.Dispatch(command);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
UIEditorWorkspaceController& controller,
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics) {
|
||||
UIEditorDockHostInteractionResult interactionResult = {};
|
||||
Widgets::UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
state.dockHostState,
|
||||
metrics);
|
||||
SyncHoverTarget(state, layout);
|
||||
|
||||
for (const UIInputEvent& event : inputEvents) {
|
||||
if (ShouldUsePointerPosition(event)) {
|
||||
state.pointerPosition = event.position;
|
||||
state.hasPointerPosition = true;
|
||||
} else if (event.type == UIInputEventType::PointerLeave) {
|
||||
state.hasPointerPosition = false;
|
||||
}
|
||||
|
||||
UIEditorDockHostInteractionResult eventResult = {};
|
||||
|
||||
switch (event.type) {
|
||||
case UIInputEventType::FocusGained:
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
|
||||
case UIInputEventType::FocusLost:
|
||||
state.dockHostState.focused = false;
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
if (state.splitterDragState.active) {
|
||||
EndUISplitterDrag(state.splitterDragState);
|
||||
state.dockHostState.activeSplitterNodeId.clear();
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
if (state.splitterDragState.active) {
|
||||
const auto* splitter = FindUIEditorDockHostSplitterLayout(
|
||||
layout,
|
||||
state.dockHostState.activeSplitterNodeId);
|
||||
if (splitter != nullptr) {
|
||||
::XCEngine::UI::Layout::UISplitterLayoutResult draggedLayout = {};
|
||||
if (UpdateUISplitterDrag(
|
||||
state.splitterDragState,
|
||||
state.pointerPosition,
|
||||
draggedLayout)) {
|
||||
eventResult.layoutResult = ApplySplitRatio(
|
||||
controller,
|
||||
state.dockHostState.activeSplitterNodeId,
|
||||
draggedLayout.splitRatio);
|
||||
eventResult.layoutChanged =
|
||||
eventResult.layoutResult.status ==
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
}
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
|
||||
eventResult.hitTarget.nodeId = state.dockHostState.activeSplitterNodeId;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerLeave:
|
||||
if (!state.splitterDragState.active) {
|
||||
state.dockHostState.hoveredTarget = {};
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
if (event.pointerButton != ::XCEngine::UI::UIPointerButton::Left) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.dockHostState.hoveredTarget.kind ==
|
||||
UIEditorDockHostHitTargetKind::SplitterHandle) {
|
||||
const auto* splitter = FindUIEditorDockHostSplitterLayout(
|
||||
layout,
|
||||
state.dockHostState.hoveredTarget.nodeId);
|
||||
if (splitter != nullptr &&
|
||||
BeginUISplitterDrag(
|
||||
1u,
|
||||
splitter->axis == UIEditorWorkspaceSplitAxis::Horizontal
|
||||
? ::XCEngine::UI::Layout::UILayoutAxis::Horizontal
|
||||
: ::XCEngine::UI::Layout::UILayoutAxis::Vertical,
|
||||
splitter->bounds,
|
||||
splitter->splitterLayout,
|
||||
splitter->constraints,
|
||||
splitter->metrics,
|
||||
state.pointerPosition,
|
||||
state.splitterDragState)) {
|
||||
state.dockHostState.activeSplitterNodeId = splitter->nodeId;
|
||||
state.dockHostState.focused = true;
|
||||
eventResult.consumed = true;
|
||||
eventResult.requestPointerCapture = true;
|
||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
||||
eventResult.activeSplitterNodeId = splitter->nodeId;
|
||||
}
|
||||
} else {
|
||||
state.dockHostState.focused =
|
||||
state.dockHostState.hoveredTarget.kind !=
|
||||
UIEditorDockHostHitTargetKind::None;
|
||||
eventResult.consumed = state.dockHostState.focused;
|
||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
if (event.pointerButton != ::XCEngine::UI::UIPointerButton::Left) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.splitterDragState.active) {
|
||||
::XCEngine::UI::Layout::UISplitterLayoutResult draggedLayout = {};
|
||||
if (UpdateUISplitterDrag(
|
||||
state.splitterDragState,
|
||||
state.pointerPosition,
|
||||
draggedLayout)) {
|
||||
eventResult.layoutResult = ApplySplitRatio(
|
||||
controller,
|
||||
state.dockHostState.activeSplitterNodeId,
|
||||
draggedLayout.splitRatio);
|
||||
eventResult.layoutChanged =
|
||||
eventResult.layoutResult.status ==
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
}
|
||||
EndUISplitterDrag(state.splitterDragState);
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture = true;
|
||||
eventResult.activeSplitterNodeId = state.dockHostState.activeSplitterNodeId;
|
||||
state.dockHostState.activeSplitterNodeId.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
||||
switch (state.dockHostState.hoveredTarget.kind) {
|
||||
case UIEditorDockHostHitTargetKind::Tab:
|
||||
case UIEditorDockHostHitTargetKind::PanelHeader:
|
||||
case UIEditorDockHostHitTargetKind::PanelBody:
|
||||
case UIEditorDockHostHitTargetKind::PanelFooter:
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
UIEditorWorkspaceCommandKind::ActivatePanel,
|
||||
state.dockHostState.hoveredTarget.panelId);
|
||||
eventResult.commandExecuted =
|
||||
eventResult.commandResult.status !=
|
||||
UIEditorWorkspaceCommandStatus::Rejected;
|
||||
eventResult.consumed = true;
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
|
||||
case UIEditorDockHostHitTargetKind::TabCloseButton:
|
||||
case UIEditorDockHostHitTargetKind::PanelCloseButton:
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
UIEditorWorkspaceCommandKind::ClosePanel,
|
||||
state.dockHostState.hoveredTarget.panelId);
|
||||
eventResult.commandExecuted =
|
||||
eventResult.commandResult.status !=
|
||||
UIEditorWorkspaceCommandStatus::Rejected;
|
||||
eventResult.consumed = true;
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
|
||||
case UIEditorDockHostHitTargetKind::TabStripBackground:
|
||||
state.dockHostState.focused = true;
|
||||
eventResult.consumed = true;
|
||||
break;
|
||||
|
||||
case UIEditorDockHostHitTargetKind::None:
|
||||
default:
|
||||
state.dockHostState.focused = false;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
layout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
state.dockHostState,
|
||||
metrics);
|
||||
SyncHoverTarget(state, layout);
|
||||
if (eventResult.hitTarget.kind == UIEditorDockHostHitTargetKind::None) {
|
||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
||||
}
|
||||
|
||||
if (eventResult.consumed ||
|
||||
eventResult.commandExecuted ||
|
||||
eventResult.layoutChanged ||
|
||||
eventResult.requestPointerCapture ||
|
||||
eventResult.releasePointerCapture ||
|
||||
eventResult.layoutResult.status != UIEditorWorkspaceLayoutOperationStatus::Rejected ||
|
||||
eventResult.hitTarget.kind != UIEditorDockHostHitTargetKind::None ||
|
||||
!eventResult.activeSplitterNodeId.empty()) {
|
||||
interactionResult = std::move(eventResult);
|
||||
}
|
||||
}
|
||||
|
||||
layout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
state.dockHostState,
|
||||
metrics);
|
||||
SyncHoverTarget(state, layout);
|
||||
if (interactionResult.hitTarget.kind == UIEditorDockHostHitTargetKind::None) {
|
||||
interactionResult.hitTarget = state.dockHostState.hoveredTarget;
|
||||
}
|
||||
return {
|
||||
std::move(layout),
|
||||
std::move(interactionResult),
|
||||
state.dockHostState.focused
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <XCEditor/Core/UIEditorWorkspaceController.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
@@ -240,6 +241,55 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreSeria
|
||||
return RestoreLayoutSnapshot(loadResult.snapshot);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRatio(
|
||||
std::string_view nodeId,
|
||||
float splitRatio) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (nodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"SetSplitRatio requires a split node id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* splitNode = FindUIEditorWorkspaceNode(m_workspace, nodeId);
|
||||
if (splitNode == nullptr || splitNode->kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"SetSplitRatio target split node is missing.");
|
||||
}
|
||||
|
||||
if (std::fabs(splitNode->splitRatio - splitRatio) <= 0.0001f) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Split ratio already matches the requested value.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TrySetUIEditorWorkspaceSplitRatio(m_workspace, nodeId, splitRatio)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Split ratio update rejected.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Split ratio update produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Split ratio updated.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
|
||||
const UIEditorWorkspaceCommand& command) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
|
||||
Reference in New Issue
Block a user