Refactor XCUI editor module layout
This commit is contained in:
237
new_editor/src/Shell/EditorShellAsset.cpp
Normal file
237
new_editor/src/Shell/EditorShellAsset.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
#include "EditorShellAsset.h"
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
Widgets::UIEditorStatusBarSegment BuildDefaultShellModeSegment() {
|
||||
Widgets::UIEditorStatusBarSegment segment = {};
|
||||
segment.segmentId = "mode";
|
||||
segment.label = "Editor Shell";
|
||||
segment.slot = Widgets::UIEditorStatusBarSlot::Leading;
|
||||
segment.tone = Widgets::UIEditorStatusBarTextTone::Primary;
|
||||
segment.interactive = false;
|
||||
segment.showSeparator = true;
|
||||
segment.desiredWidth = 112.0f;
|
||||
return segment;
|
||||
}
|
||||
|
||||
Widgets::UIEditorStatusBarSegment BuildDefaultActivePanelSegment(
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
Widgets::UIEditorStatusBarSegment segment = {};
|
||||
segment.segmentId = "active-panel";
|
||||
segment.label = workspace.activePanelId.empty() ? std::string("(none)") : workspace.activePanelId;
|
||||
segment.slot = Widgets::UIEditorStatusBarSlot::Trailing;
|
||||
segment.tone = Widgets::UIEditorStatusBarTextTone::Muted;
|
||||
segment.interactive = false;
|
||||
segment.showSeparator = false;
|
||||
segment.desiredWidth = 144.0f;
|
||||
return segment;
|
||||
}
|
||||
|
||||
EditorShellAssetValidationResult MakeValidationError(
|
||||
EditorShellAssetValidationCode code,
|
||||
std::string message) {
|
||||
EditorShellAssetValidationResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
EditorShellAssetValidationResult ValidateWorkspacePanelsAgainstRegistry(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorPanelRegistry& panelRegistry) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(panelRegistry, node.panel.panelId);
|
||||
if (descriptor == nullptr) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::MissingPanelDescriptor,
|
||||
"Workspace panel '" + node.panel.panelId + "' is missing from the panel registry.");
|
||||
}
|
||||
|
||||
if (node.panel.title != descriptor->defaultTitle) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::PanelTitleMismatch,
|
||||
"Workspace panel '" + node.panel.panelId + "' title does not match the registry default title.");
|
||||
}
|
||||
|
||||
if (node.panel.placeholder != descriptor->placeholder) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::PanelPlaceholderMismatch,
|
||||
"Workspace panel '" + node.panel.panelId + "' placeholder flag does not match the registry descriptor.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
EditorShellAssetValidationResult result =
|
||||
ValidateWorkspacePanelsAgainstRegistry(child, panelRegistry);
|
||||
if (!result.IsValid()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
EditorShellAssetValidationResult ValidateShellDefinitionAgainstRegistry(
|
||||
const UIEditorShellInteractionDefinition& definition,
|
||||
const UIEditorPanelRegistry& panelRegistry) {
|
||||
std::unordered_set<std::string> panelIds = {};
|
||||
for (const UIEditorWorkspacePanelPresentationModel& presentation :
|
||||
definition.workspacePresentations) {
|
||||
if (!panelIds.insert(presentation.panelId).second) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::DuplicateShellPresentationPanelId,
|
||||
"Shell definition presentation panel '" + presentation.panelId +
|
||||
"' is duplicated.");
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId);
|
||||
if (descriptor == nullptr) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::MissingShellPresentationPanelDescriptor,
|
||||
"Shell definition presentation panel '" + presentation.panelId +
|
||||
"' is missing from the panel registry.");
|
||||
}
|
||||
|
||||
if (presentation.kind != descriptor->presentationKind) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::ShellPresentationKindMismatch,
|
||||
"Shell definition presentation panel '" + presentation.panelId +
|
||||
"' kind does not match the panel registry.");
|
||||
}
|
||||
}
|
||||
|
||||
for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) {
|
||||
if (panelIds.find(descriptor.panelId) == panelIds.end()) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::MissingRequiredShellPresentation,
|
||||
"Shell definition is missing presentation panel '" + descriptor.panelId +
|
||||
"' required by the panel registry.");
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationModel BuildShellPresentation(
|
||||
const UIEditorPanelDescriptor& descriptor) {
|
||||
UIEditorWorkspacePanelPresentationModel presentation = {};
|
||||
presentation.panelId = descriptor.panelId;
|
||||
presentation.kind = descriptor.presentationKind;
|
||||
if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) {
|
||||
presentation.viewportShellModel.spec.chrome.title = descriptor.defaultTitle;
|
||||
presentation.viewportShellModel.spec.chrome.subtitle = "Editor Shell";
|
||||
presentation.viewportShellModel.spec.chrome.showTopBar = true;
|
||||
presentation.viewportShellModel.spec.chrome.showBottomBar = true;
|
||||
presentation.viewportShellModel.frame.statusText = descriptor.defaultTitle;
|
||||
}
|
||||
return presentation;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionDefinition BuildDefaultShellDefinition(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
UIEditorShellInteractionDefinition definition = {};
|
||||
definition.statusSegments = {
|
||||
BuildDefaultShellModeSegment(),
|
||||
BuildDefaultActivePanelSegment(workspace)
|
||||
};
|
||||
definition.workspacePresentations.reserve(panelRegistry.panels.size());
|
||||
for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) {
|
||||
definition.workspacePresentations.push_back(BuildShellPresentation(descriptor));
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot) {
|
||||
EditorShellAsset asset = {};
|
||||
asset.captureRootPath = (repoRoot / "new_editor/captures").lexically_normal();
|
||||
asset.panelRegistry = BuildDefaultEditorShellPanelRegistry();
|
||||
asset.workspace = BuildDefaultEditorShellWorkspaceModel();
|
||||
asset.workspaceSession = BuildDefaultUIEditorWorkspaceSession(asset.panelRegistry, asset.workspace);
|
||||
asset.shellDefinition = BuildDefaultShellDefinition(asset.panelRegistry, asset.workspace);
|
||||
return asset;
|
||||
}
|
||||
|
||||
UIEditorShortcutManager BuildEditorShellShortcutManager(const EditorShellAsset& asset) {
|
||||
UIEditorShortcutManager manager(asset.shortcutAsset.commandRegistry);
|
||||
for (const XCEngine::UI::UIShortcutBinding& binding : asset.shortcutAsset.bindings) {
|
||||
manager.RegisterBinding(binding);
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
EditorShellAssetValidationResult ValidateEditorShellAsset(const EditorShellAsset& asset) {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(asset.panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::InvalidPanelRegistry,
|
||||
registryValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(asset.workspace);
|
||||
if (!workspaceValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::InvalidWorkspace,
|
||||
workspaceValidation.message);
|
||||
}
|
||||
|
||||
const EditorShellAssetValidationResult panelRegistryConsistency =
|
||||
ValidateWorkspacePanelsAgainstRegistry(asset.workspace.root, asset.panelRegistry);
|
||||
if (!panelRegistryConsistency.IsValid()) {
|
||||
return panelRegistryConsistency;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSessionValidationResult workspaceSessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(
|
||||
asset.panelRegistry,
|
||||
asset.workspace,
|
||||
asset.workspaceSession);
|
||||
if (!workspaceSessionValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::InvalidWorkspaceSession,
|
||||
workspaceSessionValidation.message);
|
||||
}
|
||||
|
||||
const EditorShellAssetValidationResult shellDefinitionValidation =
|
||||
ValidateShellDefinitionAgainstRegistry(asset.shellDefinition, asset.panelRegistry);
|
||||
if (!shellDefinitionValidation.IsValid()) {
|
||||
return shellDefinitionValidation;
|
||||
}
|
||||
|
||||
const UIEditorMenuModelValidationResult shellMenuValidation =
|
||||
ValidateUIEditorMenuModel(
|
||||
asset.shellDefinition.menuModel,
|
||||
asset.shortcutAsset.commandRegistry);
|
||||
if (!shellMenuValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::InvalidShellMenuModel,
|
||||
shellMenuValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorShortcutManager shortcutManager =
|
||||
BuildEditorShellShortcutManager(asset);
|
||||
const UIEditorShortcutManagerValidationResult shortcutValidation =
|
||||
shortcutManager.ValidateConfiguration();
|
||||
if (!shortcutValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
EditorShellAssetValidationCode::InvalidShortcutConfiguration,
|
||||
shortcutValidation.message);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
61
new_editor/src/Shell/EditorShellAsset.h
Normal file
61
new_editor/src/Shell/EditorShellAsset.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
|
||||
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Shell/UIEditorShellInteraction.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct EditorShellShortcutAsset {
|
||||
UIEditorCommandRegistry commandRegistry = {};
|
||||
std::vector<XCEngine::UI::UIShortcutBinding> bindings = {};
|
||||
};
|
||||
|
||||
struct EditorShellAsset {
|
||||
std::string screenId = "editor.shell";
|
||||
std::filesystem::path documentPath = {};
|
||||
std::filesystem::path captureRootPath = {};
|
||||
UIEditorPanelRegistry panelRegistry = {};
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
UIEditorWorkspaceSession workspaceSession = {};
|
||||
UIEditorShellInteractionDefinition shellDefinition = {};
|
||||
EditorShellShortcutAsset shortcutAsset = {};
|
||||
};
|
||||
|
||||
enum class EditorShellAssetValidationCode : std::uint8_t {
|
||||
None = 0,
|
||||
InvalidPanelRegistry,
|
||||
InvalidWorkspace,
|
||||
InvalidWorkspaceSession,
|
||||
InvalidShellMenuModel,
|
||||
InvalidShortcutConfiguration,
|
||||
MissingPanelDescriptor,
|
||||
PanelTitleMismatch,
|
||||
PanelPlaceholderMismatch,
|
||||
DuplicateShellPresentationPanelId,
|
||||
MissingShellPresentationPanelDescriptor,
|
||||
MissingRequiredShellPresentation,
|
||||
ShellPresentationKindMismatch
|
||||
};
|
||||
|
||||
struct EditorShellAssetValidationResult {
|
||||
EditorShellAssetValidationCode code = EditorShellAssetValidationCode::None;
|
||||
std::string message = {};
|
||||
|
||||
[[nodiscard]] bool IsValid() const {
|
||||
return code == EditorShellAssetValidationCode::None;
|
||||
}
|
||||
};
|
||||
|
||||
EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot);
|
||||
UIEditorShortcutManager BuildEditorShellShortcutManager(const EditorShellAsset& asset);
|
||||
EditorShellAssetValidationResult ValidateEditorShellAsset(const EditorShellAsset& asset);
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
905
new_editor/src/Shell/UIEditorDockHost.cpp
Normal file
905
new_editor/src/Shell/UIEditorDockHost.cpp
Normal file
@@ -0,0 +1,905 @@
|
||||
#include <XCEditor/Shell/UIEditorDockHost.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::UISize;
|
||||
using ::XCEngine::UI::Layout::ArrangeUISplitter;
|
||||
using ::XCEngine::UI::Layout::MeasureSplitterDesiredSize;
|
||||
using ::XCEngine::UI::Layout::MeasureUITabStrip;
|
||||
using ::XCEngine::UI::Layout::UITabStripMeasureItem;
|
||||
using ::XCEngine::UI::Layout::UILayoutAxis;
|
||||
using ::XCEngine::UI::Widgets::ExpandUISplitterHandleHitRect;
|
||||
|
||||
constexpr std::string_view kStandalonePanelPlaceholder = "DockHost standalone panel";
|
||||
constexpr std::string_view kStandalonePanelActiveFooter = "Active panel";
|
||||
constexpr std::string_view kStandalonePanelInactiveFooter = "Panel placeholder";
|
||||
constexpr std::string_view kStandalonePanelActiveDetail = "Active panel body is ready for composition";
|
||||
constexpr std::string_view kStandalonePanelIdleDetail = "Select the header or body to activate this panel";
|
||||
constexpr std::string_view kTabContentPlaceholder = "DockHost tab content placeholder";
|
||||
constexpr std::string_view kTabContentDetailPrefix = "Selected panel: ";
|
||||
|
||||
struct DockMeasureResult {
|
||||
bool visible = false;
|
||||
UISize minimumSize = {};
|
||||
};
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
UILayoutAxis ToLayoutAxis(UIEditorWorkspaceSplitAxis axis) {
|
||||
return axis == UIEditorWorkspaceSplitAxis::Horizontal
|
||||
? UILayoutAxis::Horizontal
|
||||
: UILayoutAxis::Vertical;
|
||||
}
|
||||
|
||||
float GetMainExtent(const UISize& size, UIEditorWorkspaceSplitAxis axis) {
|
||||
return axis == UIEditorWorkspaceSplitAxis::Horizontal ? size.width : size.height;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool UsesExternalBodyPresentation(
|
||||
const UIEditorDockHostForegroundOptions& options,
|
||||
std::string_view panelId) {
|
||||
return std::find(
|
||||
options.externalBodyPanelIds.begin(),
|
||||
options.externalBodyPanelIds.end(),
|
||||
panelId) != options.externalBodyPanelIds.end();
|
||||
}
|
||||
|
||||
const UIEditorDockHostTabStripVisualState* FindTabStripVisualState(
|
||||
const UIEditorDockHostState& state,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostTabStripVisualState& entry : state.tabStripStates) {
|
||||
if (entry.nodeId == nodeId) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
FindUIEditorPanelSessionState(session, panelId);
|
||||
return panelState != nullptr && panelState->open && panelState->visible;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* FindPanelDescriptor(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
std::string_view panelId) {
|
||||
return FindUIEditorPanelDescriptor(panelRegistry, panelId);
|
||||
}
|
||||
|
||||
UIEditorPanelFrameMetrics BuildTabContentFrameMetrics(
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
UIEditorPanelFrameMetrics frameMetrics = metrics.panelFrameMetrics;
|
||||
frameMetrics.headerHeight = 0.0f;
|
||||
frameMetrics.footerHeight = 0.0f;
|
||||
frameMetrics.actionButtonExtent = 0.0f;
|
||||
frameMetrics.actionInsetX = 0.0f;
|
||||
frameMetrics.actionGap = 0.0f;
|
||||
frameMetrics.cornerRounding = (std::max)(frameMetrics.cornerRounding - 1.0f, 0.0f);
|
||||
return frameMetrics;
|
||||
}
|
||||
|
||||
UISize MeasurePanelMinimumSize(
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorPanelFrameMetrics& frameMetrics = metrics.panelFrameMetrics;
|
||||
return UISize(
|
||||
metrics.minimumStandalonePanelBodySize.width +
|
||||
ClampNonNegative(frameMetrics.contentPadding) * 2.0f,
|
||||
ClampNonNegative(frameMetrics.headerHeight) +
|
||||
metrics.minimumStandalonePanelBodySize.height +
|
||||
ClampNonNegative(frameMetrics.contentPadding) * 2.0f);
|
||||
}
|
||||
|
||||
UISize MeasureTabContentMinimumSize(
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorPanelFrameMetrics frameMetrics = BuildTabContentFrameMetrics(metrics);
|
||||
return UISize(
|
||||
metrics.minimumTabContentBodySize.width +
|
||||
ClampNonNegative(frameMetrics.contentPadding) * 2.0f,
|
||||
metrics.minimumTabContentBodySize.height +
|
||||
ClampNonNegative(frameMetrics.contentPadding) * 2.0f);
|
||||
}
|
||||
|
||||
DockMeasureResult MeasureNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostMetrics& metrics);
|
||||
|
||||
DockMeasureResult MeasureTabStackNode(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
std::vector<UITabStripMeasureItem> measureItems = {};
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
!IsPanelOpenAndVisible(session, child.panel.panelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorTabStripItem item = {};
|
||||
item.tabId = child.panel.panelId;
|
||||
item.title = child.panel.title;
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindPanelDescriptor(panelRegistry, child.panel.panelId);
|
||||
item.closable = descriptor != nullptr ? descriptor->canClose : true;
|
||||
|
||||
UITabStripMeasureItem measureItem = {};
|
||||
measureItem.desiredHeaderLabelWidth =
|
||||
ResolveUIEditorTabStripDesiredHeaderLabelWidth(item, metrics.tabStripMetrics);
|
||||
measureItem.desiredContentSize = MeasureTabContentMinimumSize(metrics);
|
||||
measureItem.minimumContentSize = MeasureTabContentMinimumSize(metrics);
|
||||
measureItems.push_back(std::move(measureItem));
|
||||
}
|
||||
|
||||
if (measureItems.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto measured = MeasureUITabStrip(measureItems, metrics.tabStripMetrics.layoutMetrics);
|
||||
DockMeasureResult result = {};
|
||||
result.visible = true;
|
||||
result.minimumSize = measured.minimumSize;
|
||||
return result;
|
||||
}
|
||||
|
||||
DockMeasureResult MeasureSplitNode(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const DockMeasureResult primary =
|
||||
MeasureNodeRecursive(node.children[0], panelRegistry, session, metrics);
|
||||
const DockMeasureResult secondary =
|
||||
MeasureNodeRecursive(node.children[1], panelRegistry, session, metrics);
|
||||
|
||||
if (!primary.visible && !secondary.visible) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!primary.visible) {
|
||||
return secondary;
|
||||
}
|
||||
|
||||
if (!secondary.visible) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
DockMeasureResult result = {};
|
||||
result.visible = true;
|
||||
result.minimumSize = MeasureSplitterDesiredSize(
|
||||
ToLayoutAxis(node.splitAxis),
|
||||
primary.minimumSize,
|
||||
secondary.minimumSize,
|
||||
metrics.splitterMetrics.thickness);
|
||||
return result;
|
||||
}
|
||||
|
||||
DockMeasureResult MeasureNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel: {
|
||||
DockMeasureResult result = {};
|
||||
result.visible = IsPanelOpenAndVisible(session, node.panel.panelId);
|
||||
if (result.visible) {
|
||||
result.minimumSize = MeasurePanelMinimumSize(metrics);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case UIEditorWorkspaceNodeKind::TabStack:
|
||||
return MeasureTabStackNode(node, panelRegistry, session, metrics);
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
return MeasureSplitNode(node, panelRegistry, session, metrics);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::size_t ResolveSelectedVisibleTabIndex(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const std::vector<std::size_t>& visibleChildIndices) {
|
||||
if (visibleChildIndices.empty()) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
for (std::size_t visibleIndex = 0; visibleIndex < visibleChildIndices.size(); ++visibleIndex) {
|
||||
if (visibleChildIndices[visibleIndex] == node.selectedTabIndex) {
|
||||
return visibleIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return 0u;
|
||||
}
|
||||
|
||||
UIEditorTabStripState BuildTabStripState(
|
||||
const UIEditorDockHostState& state,
|
||||
std::string_view nodeId,
|
||||
std::size_t selectedIndex) {
|
||||
UIEditorTabStripState tabState = {};
|
||||
if (const auto* visualState = FindTabStripVisualState(state, nodeId)) {
|
||||
tabState = visualState->state;
|
||||
} else {
|
||||
tabState.focused = state.focused;
|
||||
}
|
||||
tabState.selectedIndex = selectedIndex;
|
||||
|
||||
if (FindTabStripVisualState(state, nodeId) != nullptr || state.hoveredTarget.nodeId != nodeId) {
|
||||
return tabState;
|
||||
}
|
||||
|
||||
switch (state.hoveredTarget.kind) {
|
||||
case UIEditorDockHostHitTargetKind::Tab:
|
||||
tabState.hoveredIndex = state.hoveredTarget.index;
|
||||
break;
|
||||
case UIEditorDockHostHitTargetKind::TabCloseButton:
|
||||
tabState.hoveredIndex = state.hoveredTarget.index;
|
||||
tabState.closeHoveredIndex = state.hoveredTarget.index;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return tabState;
|
||||
}
|
||||
|
||||
UIEditorPanelFrameState BuildStandalonePanelFrameState(
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorPanelDescriptor* descriptor,
|
||||
const UIEditorWorkspaceNode& node) {
|
||||
UIEditorPanelFrameState frameState = {};
|
||||
frameState.active = workspace.activePanelId == node.panel.panelId;
|
||||
frameState.focused = state.focused && frameState.active;
|
||||
frameState.closable = descriptor != nullptr ? descriptor->canClose : true;
|
||||
frameState.pinnable = false;
|
||||
frameState.showFooter = true;
|
||||
|
||||
if (state.hoveredTarget.panelId != node.panel.panelId) {
|
||||
return frameState;
|
||||
}
|
||||
|
||||
switch (state.hoveredTarget.kind) {
|
||||
case UIEditorDockHostHitTargetKind::PanelHeader:
|
||||
case UIEditorDockHostHitTargetKind::PanelBody:
|
||||
case UIEditorDockHostHitTargetKind::PanelFooter:
|
||||
case UIEditorDockHostHitTargetKind::PanelCloseButton:
|
||||
frameState.hovered = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
frameState.closeHovered =
|
||||
state.hoveredTarget.kind == UIEditorDockHostHitTargetKind::PanelCloseButton;
|
||||
return frameState;
|
||||
}
|
||||
|
||||
UIEditorPanelFrameState BuildTabContentFrameState(
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string_view panelId) {
|
||||
UIEditorPanelFrameState frameState = {};
|
||||
frameState.active = workspace.activePanelId == panelId;
|
||||
frameState.focused = state.focused && frameState.active;
|
||||
frameState.closable = false;
|
||||
frameState.pinnable = false;
|
||||
|
||||
if (state.hoveredTarget.panelId != panelId) {
|
||||
return frameState;
|
||||
}
|
||||
|
||||
switch (state.hoveredTarget.kind) {
|
||||
case UIEditorDockHostHitTargetKind::PanelHeader:
|
||||
case UIEditorDockHostHitTargetKind::PanelBody:
|
||||
case UIEditorDockHostHitTargetKind::PanelFooter:
|
||||
frameState.hovered = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return frameState;
|
||||
}
|
||||
|
||||
void LayoutNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorDockHostMetrics& metrics,
|
||||
UIEditorDockHostLayout& layout);
|
||||
|
||||
void LayoutPanelNode(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorDockHostMetrics& metrics,
|
||||
UIEditorDockHostLayout& layout) {
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindPanelDescriptor(panelRegistry, node.panel.panelId);
|
||||
|
||||
UIEditorDockHostPanelLayout panelLayout = {};
|
||||
panelLayout.nodeId = node.nodeId;
|
||||
panelLayout.panelId = node.panel.panelId;
|
||||
panelLayout.title = node.panel.title;
|
||||
panelLayout.active = workspace.activePanelId == node.panel.panelId;
|
||||
panelLayout.frameState =
|
||||
BuildStandalonePanelFrameState(state, workspace, descriptor, node);
|
||||
panelLayout.frameLayout =
|
||||
BuildUIEditorPanelFrameLayout(bounds, panelLayout.frameState, metrics.panelFrameMetrics);
|
||||
layout.panels.push_back(std::move(panelLayout));
|
||||
}
|
||||
|
||||
void LayoutTabStackNode(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorDockHostMetrics& metrics,
|
||||
UIEditorDockHostLayout& layout) {
|
||||
std::vector<std::size_t> visibleChildIndices = {};
|
||||
visibleChildIndices.reserve(node.children.size());
|
||||
|
||||
UIEditorDockHostTabStackLayout tabStackLayout = {};
|
||||
tabStackLayout.nodeId = node.nodeId;
|
||||
tabStackLayout.bounds = bounds;
|
||||
|
||||
std::vector<UIEditorTabStripItem> tabStripItems = {};
|
||||
for (std::size_t childIndex = 0; childIndex < node.children.size(); ++childIndex) {
|
||||
const UIEditorWorkspaceNode& child = node.children[childIndex];
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
!IsPanelOpenAndVisible(session, child.panel.panelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visibleChildIndices.push_back(childIndex);
|
||||
|
||||
UIEditorDockHostTabItemLayout itemLayout = {};
|
||||
itemLayout.panelId = child.panel.panelId;
|
||||
itemLayout.title = child.panel.title;
|
||||
itemLayout.active = workspace.activePanelId == child.panel.panelId;
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindPanelDescriptor(panelRegistry, child.panel.panelId);
|
||||
itemLayout.closable = descriptor != nullptr ? descriptor->canClose : true;
|
||||
tabStackLayout.items.push_back(itemLayout);
|
||||
|
||||
UIEditorTabStripItem tabItem = {};
|
||||
tabItem.tabId = itemLayout.panelId;
|
||||
tabItem.title = itemLayout.title;
|
||||
tabItem.closable = itemLayout.closable;
|
||||
tabStripItems.push_back(std::move(tabItem));
|
||||
}
|
||||
|
||||
if (tabStripItems.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t selectedVisibleIndex =
|
||||
ResolveSelectedVisibleTabIndex(node, visibleChildIndices);
|
||||
tabStackLayout.selectedPanelId =
|
||||
tabStackLayout.items[selectedVisibleIndex].panelId;
|
||||
tabStackLayout.tabStripState =
|
||||
BuildTabStripState(state, node.nodeId, selectedVisibleIndex);
|
||||
tabStackLayout.tabStripLayout =
|
||||
BuildUIEditorTabStripLayout(bounds, tabStripItems, tabStackLayout.tabStripState, metrics.tabStripMetrics);
|
||||
tabStackLayout.contentFrameState =
|
||||
BuildTabContentFrameState(state, workspace, tabStackLayout.selectedPanelId);
|
||||
tabStackLayout.contentFrameLayout = BuildUIEditorPanelFrameLayout(
|
||||
tabStackLayout.tabStripLayout.contentRect,
|
||||
tabStackLayout.contentFrameState,
|
||||
BuildTabContentFrameMetrics(metrics));
|
||||
|
||||
layout.tabStacks.push_back(std::move(tabStackLayout));
|
||||
}
|
||||
|
||||
void LayoutSplitNode(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorDockHostMetrics& metrics,
|
||||
UIEditorDockHostLayout& layout) {
|
||||
const DockMeasureResult primaryMeasure =
|
||||
MeasureNodeRecursive(node.children[0], panelRegistry, session, metrics);
|
||||
const DockMeasureResult secondaryMeasure =
|
||||
MeasureNodeRecursive(node.children[1], panelRegistry, session, metrics);
|
||||
|
||||
if (!primaryMeasure.visible && !secondaryMeasure.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!primaryMeasure.visible) {
|
||||
LayoutNodeRecursive(
|
||||
node.children[1],
|
||||
bounds,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
state,
|
||||
metrics,
|
||||
layout);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!secondaryMeasure.visible) {
|
||||
LayoutNodeRecursive(
|
||||
node.children[0],
|
||||
bounds,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
state,
|
||||
metrics,
|
||||
layout);
|
||||
return;
|
||||
}
|
||||
|
||||
UIEditorDockHostSplitterLayout splitterLayout = {};
|
||||
splitterLayout.nodeId = node.nodeId;
|
||||
splitterLayout.axis = node.splitAxis;
|
||||
splitterLayout.bounds = bounds;
|
||||
splitterLayout.metrics = metrics.splitterMetrics;
|
||||
splitterLayout.constraints.primaryMin =
|
||||
GetMainExtent(primaryMeasure.minimumSize, node.splitAxis);
|
||||
splitterLayout.constraints.secondaryMin =
|
||||
GetMainExtent(secondaryMeasure.minimumSize, node.splitAxis);
|
||||
splitterLayout.splitterLayout = ArrangeUISplitter(
|
||||
bounds,
|
||||
ToLayoutAxis(node.splitAxis),
|
||||
node.splitRatio,
|
||||
splitterLayout.constraints,
|
||||
splitterLayout.metrics);
|
||||
splitterLayout.handleHitRect = ExpandUISplitterHandleHitRect(
|
||||
splitterLayout.splitterLayout.handleRect,
|
||||
ToLayoutAxis(node.splitAxis),
|
||||
(std::max)(0.0f, (splitterLayout.metrics.hitThickness - splitterLayout.metrics.thickness) * 0.5f));
|
||||
splitterLayout.hovered =
|
||||
state.hoveredTarget.kind == UIEditorDockHostHitTargetKind::SplitterHandle &&
|
||||
state.hoveredTarget.nodeId == node.nodeId;
|
||||
splitterLayout.active = state.activeSplitterNodeId == node.nodeId;
|
||||
layout.splitters.push_back(splitterLayout);
|
||||
|
||||
LayoutNodeRecursive(
|
||||
node.children[0],
|
||||
splitterLayout.splitterLayout.primaryRect,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
state,
|
||||
metrics,
|
||||
layout);
|
||||
LayoutNodeRecursive(
|
||||
node.children[1],
|
||||
splitterLayout.splitterLayout.secondaryRect,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
state,
|
||||
metrics,
|
||||
layout);
|
||||
}
|
||||
|
||||
void LayoutNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorDockHostMetrics& metrics,
|
||||
UIEditorDockHostLayout& layout) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel:
|
||||
if (IsPanelOpenAndVisible(session, node.panel.panelId)) {
|
||||
LayoutPanelNode(node, bounds, panelRegistry, workspace, state, metrics, layout);
|
||||
}
|
||||
return;
|
||||
case UIEditorWorkspaceNodeKind::TabStack:
|
||||
LayoutTabStackNode(node, bounds, panelRegistry, workspace, session, state, metrics, layout);
|
||||
return;
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
LayoutSplitNode(node, bounds, panelRegistry, workspace, session, state, metrics, layout);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorDockHostHitTarget MakePanelHitTarget(
|
||||
UIEditorDockHostHitTargetKind kind,
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId) {
|
||||
UIEditorDockHostHitTarget target = {};
|
||||
target.kind = kind;
|
||||
target.nodeId = std::string(nodeId);
|
||||
target.panelId = std::string(panelId);
|
||||
return target;
|
||||
}
|
||||
|
||||
UIEditorDockHostHitTarget MapPanelFrameHitTarget(
|
||||
UIEditorPanelFrameHitTarget hitTarget,
|
||||
std::string_view nodeId,
|
||||
std::string_view panelId) {
|
||||
switch (hitTarget) {
|
||||
case UIEditorPanelFrameHitTarget::Header:
|
||||
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelHeader, nodeId, panelId);
|
||||
case UIEditorPanelFrameHitTarget::Body:
|
||||
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelBody, nodeId, panelId);
|
||||
case UIEditorPanelFrameHitTarget::Footer:
|
||||
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelFooter, nodeId, panelId);
|
||||
case UIEditorPanelFrameHitTarget::CloseButton:
|
||||
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelCloseButton, nodeId, panelId);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
UIColor ResolveSplitterColor(const UIEditorDockHostSplitterLayout& splitter, const UIEditorDockHostPalette& palette) {
|
||||
if (splitter.active) {
|
||||
return palette.splitterActiveColor;
|
||||
}
|
||||
|
||||
if (splitter.hovered) {
|
||||
return palette.splitterHoveredColor;
|
||||
}
|
||||
|
||||
return palette.splitterColor;
|
||||
}
|
||||
|
||||
void AppendPlaceholderText(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bodyRect,
|
||||
std::string title,
|
||||
std::string detailLine,
|
||||
std::string extraLine,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
if (bodyRect.width <= 0.0f || bodyRect.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawList.AddText(
|
||||
UIPoint(bodyRect.x, bodyRect.y),
|
||||
std::move(title),
|
||||
palette.placeholderTitleColor,
|
||||
16.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(bodyRect.x, bodyRect.y + metrics.placeholderLineGap),
|
||||
std::move(detailLine),
|
||||
palette.placeholderTextColor,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(bodyRect.x, bodyRect.y + metrics.placeholderLineGap * 2.0f),
|
||||
std::move(extraLine),
|
||||
palette.placeholderMutedColor,
|
||||
12.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
|
||||
if (splitter.nodeId == nodeId) {
|
||||
return &splitter;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorDockHostLayout BuildUIEditorDockHostLayout(
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
UIEditorDockHostLayout layout = {};
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
LayoutNodeRecursive(
|
||||
workspace.root,
|
||||
layout.bounds,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
state,
|
||||
metrics,
|
||||
layout);
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorDockHostHitTarget HitTestUIEditorDockHost(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIPoint& point) {
|
||||
for (std::size_t index = layout.splitters.size(); index > 0u; --index) {
|
||||
const UIEditorDockHostSplitterLayout& splitter = layout.splitters[index - 1u];
|
||||
if (IsPointInsideRect(splitter.handleHitRect, point)) {
|
||||
UIEditorDockHostHitTarget target = {};
|
||||
target.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
|
||||
target.nodeId = splitter.nodeId;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = layout.tabStacks.size(); index > 0u; --index) {
|
||||
const UIEditorDockHostTabStackLayout& tabStack = layout.tabStacks[index - 1u];
|
||||
const UIEditorTabStripHitTarget tabHit =
|
||||
HitTestUIEditorTabStrip(tabStack.tabStripLayout, tabStack.tabStripState, point);
|
||||
switch (tabHit.kind) {
|
||||
case UIEditorTabStripHitTargetKind::CloseButton: {
|
||||
UIEditorDockHostHitTarget target = {};
|
||||
target.kind = UIEditorDockHostHitTargetKind::TabCloseButton;
|
||||
target.nodeId = tabStack.nodeId;
|
||||
target.index = tabHit.index;
|
||||
if (tabHit.index < tabStack.items.size()) {
|
||||
target.panelId = tabStack.items[tabHit.index].panelId;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
case UIEditorTabStripHitTargetKind::Tab: {
|
||||
UIEditorDockHostHitTarget target = {};
|
||||
target.kind = UIEditorDockHostHitTargetKind::Tab;
|
||||
target.nodeId = tabStack.nodeId;
|
||||
target.index = tabHit.index;
|
||||
if (tabHit.index < tabStack.items.size()) {
|
||||
target.panelId = tabStack.items[tabHit.index].panelId;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
case UIEditorTabStripHitTargetKind::HeaderBackground: {
|
||||
UIEditorDockHostHitTarget target = {};
|
||||
target.kind = UIEditorDockHostHitTargetKind::TabStripBackground;
|
||||
target.nodeId = tabStack.nodeId;
|
||||
return target;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const UIEditorPanelFrameHitTarget panelHit = HitTestUIEditorPanelFrame(
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
point);
|
||||
if (panelHit != UIEditorPanelFrameHitTarget::None) {
|
||||
return MapPanelFrameHitTarget(panelHit, tabStack.nodeId, tabStack.selectedPanelId);
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = layout.panels.size(); index > 0u; --index) {
|
||||
const UIEditorDockHostPanelLayout& panel = layout.panels[index - 1u];
|
||||
const UIEditorPanelFrameHitTarget hitTarget = HitTestUIEditorPanelFrame(
|
||||
panel.frameLayout,
|
||||
panel.frameState,
|
||||
point);
|
||||
if (hitTarget != UIEditorPanelFrameHitTarget::None) {
|
||||
return MapPanelFrameHitTarget(hitTarget, panel.nodeId, panel.panelId);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void AppendUIEditorDockHostBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
for (const UIEditorDockHostPanelLayout& panel : layout.panels) {
|
||||
AppendUIEditorPanelFrameBackground(
|
||||
drawList,
|
||||
panel.frameLayout,
|
||||
panel.frameState,
|
||||
palette.panelFramePalette,
|
||||
metrics.panelFrameMetrics);
|
||||
}
|
||||
|
||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
||||
BuildTabContentFrameMetrics(metrics);
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||
tabItems.reserve(tabStack.items.size());
|
||||
for (const UIEditorDockHostTabItemLayout& item : tabStack.items) {
|
||||
UIEditorTabStripItem tabItem = {};
|
||||
tabItem.tabId = item.panelId;
|
||||
tabItem.title = item.title;
|
||||
tabItem.closable = item.closable;
|
||||
tabItems.push_back(std::move(tabItem));
|
||||
}
|
||||
|
||||
AppendUIEditorTabStripBackground(
|
||||
drawList,
|
||||
tabStack.tabStripLayout,
|
||||
tabStack.tabStripState,
|
||||
palette.tabStripPalette,
|
||||
metrics.tabStripMetrics);
|
||||
AppendUIEditorPanelFrameBackground(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
palette.panelFramePalette,
|
||||
tabContentFrameMetrics);
|
||||
}
|
||||
|
||||
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
|
||||
drawList.AddFilledRect(
|
||||
splitter.splitterLayout.handleRect,
|
||||
ResolveSplitterColor(splitter, palette),
|
||||
metrics.splitterHandleRounding);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorDockHostForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostForegroundOptions& options,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
for (const UIEditorDockHostPanelLayout& panel : layout.panels) {
|
||||
AppendUIEditorPanelFrameForeground(
|
||||
drawList,
|
||||
panel.frameLayout,
|
||||
panel.frameState,
|
||||
UIEditorPanelFrameText{
|
||||
panel.title,
|
||||
panel.panelId,
|
||||
panel.active ? kStandalonePanelActiveFooter : kStandalonePanelInactiveFooter
|
||||
},
|
||||
palette.panelFramePalette,
|
||||
metrics.panelFrameMetrics);
|
||||
if (UsesExternalBodyPresentation(options, panel.panelId)) {
|
||||
continue;
|
||||
}
|
||||
AppendPlaceholderText(
|
||||
drawList,
|
||||
panel.frameLayout.bodyRect,
|
||||
panel.title,
|
||||
std::string(kStandalonePanelPlaceholder),
|
||||
panel.active ? std::string(kStandalonePanelActiveDetail) : std::string(kStandalonePanelIdleDetail),
|
||||
palette,
|
||||
metrics);
|
||||
}
|
||||
|
||||
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
|
||||
BuildTabContentFrameMetrics(metrics);
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
std::vector<UIEditorTabStripItem> tabItems = {};
|
||||
tabItems.reserve(tabStack.items.size());
|
||||
for (const UIEditorDockHostTabItemLayout& item : tabStack.items) {
|
||||
UIEditorTabStripItem tabItem = {};
|
||||
tabItem.tabId = item.panelId;
|
||||
tabItem.title = item.title;
|
||||
tabItem.closable = item.closable;
|
||||
tabItems.push_back(std::move(tabItem));
|
||||
}
|
||||
|
||||
AppendUIEditorTabStripForeground(
|
||||
drawList,
|
||||
tabStack.tabStripLayout,
|
||||
tabItems,
|
||||
tabStack.tabStripState,
|
||||
palette.tabStripPalette,
|
||||
metrics.tabStripMetrics);
|
||||
AppendUIEditorPanelFrameForeground(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout,
|
||||
tabStack.contentFrameState,
|
||||
{},
|
||||
palette.panelFramePalette,
|
||||
tabContentFrameMetrics);
|
||||
|
||||
std::string selectedTitle = "(none)";
|
||||
for (const UIEditorDockHostTabItemLayout& item : tabStack.items) {
|
||||
if (item.panelId == tabStack.selectedPanelId) {
|
||||
selectedTitle = item.title;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) {
|
||||
continue;
|
||||
}
|
||||
AppendPlaceholderText(
|
||||
drawList,
|
||||
tabStack.contentFrameLayout.bodyRect,
|
||||
selectedTitle,
|
||||
std::string(kTabContentPlaceholder),
|
||||
std::string(kTabContentDetailPrefix) + tabStack.selectedPanelId,
|
||||
palette,
|
||||
metrics);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorDockHostForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorDockHostLayout& layout,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
AppendUIEditorDockHostForeground(
|
||||
drawList,
|
||||
layout,
|
||||
UIEditorDockHostForegroundOptions{},
|
||||
palette,
|
||||
metrics);
|
||||
}
|
||||
|
||||
void AppendUIEditorDockHost(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorDockHostForegroundOptions& foregroundOptions,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
const UIEditorDockHostLayout layout =
|
||||
BuildUIEditorDockHostLayout(bounds, panelRegistry, workspace, session, state, metrics);
|
||||
AppendUIEditorDockHostBackground(drawList, layout, palette, metrics);
|
||||
AppendUIEditorDockHostForeground(drawList, layout, foregroundOptions, palette, metrics);
|
||||
}
|
||||
|
||||
void AppendUIEditorDockHost(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorDockHostState& state,
|
||||
const UIEditorDockHostPalette& palette,
|
||||
const UIEditorDockHostMetrics& metrics) {
|
||||
AppendUIEditorDockHost(
|
||||
drawList,
|
||||
bounds,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
state,
|
||||
UIEditorDockHostForegroundOptions{},
|
||||
palette,
|
||||
metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
620
new_editor/src/Shell/UIEditorDockHostInteraction.cpp
Normal file
620
new_editor/src/Shell/UIEditorDockHostInteraction.cpp
Normal file
@@ -0,0 +1,620 @@
|
||||
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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;
|
||||
using Widgets::UIEditorDockHostTabItemLayout;
|
||||
using Widgets::UIEditorDockHostTabStackLayout;
|
||||
using Widgets::UIEditorTabStripHitTargetKind;
|
||||
using Widgets::UIEditorTabStripItem;
|
||||
|
||||
struct DockHostTabStripEventResult {
|
||||
bool consumed = false;
|
||||
bool commandRequested = false;
|
||||
UIEditorWorkspaceCommandKind commandKind = UIEditorWorkspaceCommandKind::ActivatePanel;
|
||||
std::string panelId = {};
|
||||
UIEditorDockHostHitTarget hitTarget = {};
|
||||
int priority = 0;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldDispatchTabStripEvent(
|
||||
const UIInputEvent& event,
|
||||
bool splitterActive) {
|
||||
if (splitterActive && event.type != UIInputEventType::FocusLost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case UIInputEventType::FocusLost:
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
case UIInputEventType::PointerLeave:
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
case UIInputEventType::KeyDown:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult ApplySplitRatio(
|
||||
UIEditorWorkspaceController& controller,
|
||||
std::string_view nodeId,
|
||||
float splitRatio) {
|
||||
return controller.SetSplitRatio(nodeId, splitRatio);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult DispatchPanelCommand(
|
||||
UIEditorWorkspaceController& controller,
|
||||
UIEditorWorkspaceCommandKind kind,
|
||||
std::string panelId) {
|
||||
UIEditorWorkspaceCommand command = {};
|
||||
command.kind = kind;
|
||||
command.panelId = std::move(panelId);
|
||||
return controller.Dispatch(command);
|
||||
}
|
||||
|
||||
UIEditorDockHostTabStripInteractionEntry& FindOrCreateTabStripInteractionEntry(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
std::string_view nodeId) {
|
||||
for (UIEditorDockHostTabStripInteractionEntry& entry : state.tabStripInteractions) {
|
||||
if (entry.nodeId == nodeId) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
state.tabStripInteractions.push_back({});
|
||||
UIEditorDockHostTabStripInteractionEntry& entry = state.tabStripInteractions.back();
|
||||
entry.nodeId = std::string(nodeId);
|
||||
return entry;
|
||||
}
|
||||
|
||||
void PruneTabStripInteractionEntries(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout) {
|
||||
const auto isVisibleNodeId = [&layout](std::string_view nodeId) {
|
||||
return std::find_if(
|
||||
layout.tabStacks.begin(),
|
||||
layout.tabStacks.end(),
|
||||
[nodeId](const UIEditorDockHostTabStackLayout& tabStack) {
|
||||
return tabStack.nodeId == nodeId;
|
||||
}) != layout.tabStacks.end();
|
||||
};
|
||||
|
||||
state.tabStripInteractions.erase(
|
||||
std::remove_if(
|
||||
state.tabStripInteractions.begin(),
|
||||
state.tabStripInteractions.end(),
|
||||
[&isVisibleNodeId](const UIEditorDockHostTabStripInteractionEntry& entry) {
|
||||
return !isVisibleNodeId(entry.nodeId);
|
||||
}),
|
||||
state.tabStripInteractions.end());
|
||||
|
||||
state.dockHostState.tabStripStates.erase(
|
||||
std::remove_if(
|
||||
state.dockHostState.tabStripStates.begin(),
|
||||
state.dockHostState.tabStripStates.end(),
|
||||
[&isVisibleNodeId](const Widgets::UIEditorDockHostTabStripVisualState& entry) {
|
||||
return !isVisibleNodeId(entry.nodeId);
|
||||
}),
|
||||
state.dockHostState.tabStripStates.end());
|
||||
}
|
||||
|
||||
void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state) {
|
||||
state.dockHostState.tabStripStates.clear();
|
||||
state.dockHostState.tabStripStates.reserve(state.tabStripInteractions.size());
|
||||
for (const UIEditorDockHostTabStripInteractionEntry& entry : state.tabStripInteractions) {
|
||||
Widgets::UIEditorDockHostTabStripVisualState visualState = {};
|
||||
visualState.nodeId = entry.nodeId;
|
||||
visualState.state = entry.state.tabStripState;
|
||||
state.dockHostState.tabStripStates.push_back(std::move(visualState));
|
||||
}
|
||||
}
|
||||
|
||||
bool HasFocusedTabStrip(const UIEditorDockHostInteractionState& state) {
|
||||
return std::find_if(
|
||||
state.tabStripInteractions.begin(),
|
||||
state.tabStripInteractions.end(),
|
||||
[](const UIEditorDockHostTabStripInteractionEntry& entry) {
|
||||
return entry.state.tabStripState.focused;
|
||||
}) != state.tabStripInteractions.end();
|
||||
}
|
||||
|
||||
std::vector<UIEditorTabStripItem> BuildTabStripItems(
|
||||
const UIEditorDockHostTabStackLayout& tabStack) {
|
||||
std::vector<UIEditorTabStripItem> items = {};
|
||||
items.reserve(tabStack.items.size());
|
||||
for (const UIEditorDockHostTabItemLayout& itemLayout : tabStack.items) {
|
||||
UIEditorTabStripItem item = {};
|
||||
item.tabId = itemLayout.panelId;
|
||||
item.title = itemLayout.title;
|
||||
item.closable = itemLayout.closable;
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
UIEditorDockHostHitTarget MapTabStripHitTarget(
|
||||
const UIEditorDockHostTabStackLayout& tabStack,
|
||||
const UIEditorTabStripInteractionResult& result) {
|
||||
UIEditorDockHostHitTarget target = {};
|
||||
target.nodeId = tabStack.nodeId;
|
||||
target.index = result.hitTarget.index;
|
||||
|
||||
switch (result.hitTarget.kind) {
|
||||
case UIEditorTabStripHitTargetKind::HeaderBackground:
|
||||
target.kind = UIEditorDockHostHitTargetKind::TabStripBackground;
|
||||
break;
|
||||
case UIEditorTabStripHitTargetKind::Tab:
|
||||
target.kind = UIEditorDockHostHitTargetKind::Tab;
|
||||
if (result.hitTarget.index < tabStack.items.size()) {
|
||||
target.panelId = tabStack.items[result.hitTarget.index].panelId;
|
||||
}
|
||||
break;
|
||||
case UIEditorTabStripHitTargetKind::CloseButton:
|
||||
target.kind = UIEditorDockHostHitTargetKind::TabCloseButton;
|
||||
if (result.hitTarget.index < tabStack.items.size()) {
|
||||
target.panelId = tabStack.items[result.hitTarget.index].panelId;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
int ResolveTabStripPriority(const UIEditorTabStripInteractionResult& result) {
|
||||
if (result.closeRequested) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (result.selectionChanged || result.keyboardNavigated) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (result.consumed || result.hitTarget.kind != UIEditorTabStripHitTargetKind::None) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
DockHostTabStripEventResult ProcessTabStripEvent(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
const Widgets::UIEditorDockHostLayout& layout,
|
||||
const UIInputEvent& event,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics) {
|
||||
DockHostTabStripEventResult resolved = {};
|
||||
|
||||
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
UIEditorDockHostTabStripInteractionEntry& entry =
|
||||
FindOrCreateTabStripInteractionEntry(state, tabStack.nodeId);
|
||||
std::string selectedTabId = tabStack.selectedPanelId;
|
||||
const std::vector<UIEditorTabStripItem> items = BuildTabStripItems(tabStack);
|
||||
const UIEditorTabStripInteractionFrame frame = UpdateUIEditorTabStripInteraction(
|
||||
entry.state,
|
||||
selectedTabId,
|
||||
tabStack.bounds,
|
||||
items,
|
||||
{ event },
|
||||
metrics.tabStripMetrics);
|
||||
|
||||
const int priority = ResolveTabStripPriority(frame.result);
|
||||
if (priority < resolved.priority) {
|
||||
continue;
|
||||
}
|
||||
|
||||
resolved.hitTarget = MapTabStripHitTarget(tabStack, frame.result);
|
||||
if ((frame.result.closeRequested && !frame.result.closedTabId.empty()) ||
|
||||
(event.type == UIInputEventType::PointerButtonUp &&
|
||||
frame.result.consumed &&
|
||||
resolved.hitTarget.kind == UIEditorDockHostHitTargetKind::TabCloseButton &&
|
||||
!resolved.hitTarget.panelId.empty())) {
|
||||
resolved.commandRequested = true;
|
||||
resolved.commandKind = UIEditorWorkspaceCommandKind::ClosePanel;
|
||||
resolved.panelId =
|
||||
!frame.result.closedTabId.empty()
|
||||
? frame.result.closedTabId
|
||||
: resolved.hitTarget.panelId;
|
||||
} else if ((frame.result.selectionChanged ||
|
||||
frame.result.keyboardNavigated ||
|
||||
(event.type == UIInputEventType::PointerButtonUp &&
|
||||
frame.result.consumed &&
|
||||
resolved.hitTarget.kind == UIEditorDockHostHitTargetKind::Tab)) &&
|
||||
(!frame.result.selectedTabId.empty() || !resolved.hitTarget.panelId.empty())) {
|
||||
resolved.commandRequested = true;
|
||||
resolved.commandKind = UIEditorWorkspaceCommandKind::ActivatePanel;
|
||||
resolved.panelId =
|
||||
!frame.result.selectedTabId.empty()
|
||||
? frame.result.selectedTabId
|
||||
: resolved.hitTarget.panelId;
|
||||
} else if (priority == 0) {
|
||||
continue;
|
||||
} else {
|
||||
resolved.commandRequested = false;
|
||||
resolved.panelId.clear();
|
||||
}
|
||||
|
||||
resolved.consumed = frame.result.consumed;
|
||||
resolved.priority = priority;
|
||||
}
|
||||
|
||||
SyncDockHostTabStripVisualStates(state);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
UIEditorDockHostInteractionState& state,
|
||||
UIEditorWorkspaceController& controller,
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics) {
|
||||
SyncDockHostTabStripVisualStates(state);
|
||||
Widgets::UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
state.dockHostState,
|
||||
metrics);
|
||||
PruneTabStripInteractionEntries(state, layout);
|
||||
SyncDockHostTabStripVisualStates(state);
|
||||
layout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
state.dockHostState,
|
||||
metrics);
|
||||
SyncHoverTarget(state, layout);
|
||||
|
||||
UIEditorDockHostInteractionResult interactionResult = {};
|
||||
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 = {};
|
||||
const DockHostTabStripEventResult tabStripResult =
|
||||
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
||||
? ProcessTabStripEvent(state, layout, event, metrics)
|
||||
: DockHostTabStripEventResult {};
|
||||
|
||||
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 = {};
|
||||
}
|
||||
if (!HasFocusedTabStrip(state)) {
|
||||
state.dockHostState.focused = false;
|
||||
}
|
||||
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 {
|
||||
if (tabStripResult.priority > 0) {
|
||||
state.dockHostState.focused = true;
|
||||
eventResult.consumed = tabStripResult.consumed;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
} 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;
|
||||
}
|
||||
|
||||
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
tabStripResult.commandKind,
|
||||
tabStripResult.panelId);
|
||||
eventResult.commandExecuted =
|
||||
eventResult.commandResult.status !=
|
||||
UIEditorWorkspaceCommandStatus::Rejected;
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.priority > 0) {
|
||||
eventResult.consumed = tabStripResult.consumed;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
if (eventResult.hitTarget.kind != UIEditorDockHostHitTargetKind::None) {
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
||||
switch (state.dockHostState.hoveredTarget.kind) {
|
||||
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::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::Tab:
|
||||
case UIEditorDockHostHitTargetKind::TabCloseButton:
|
||||
case UIEditorDockHostHitTargetKind::TabStripBackground:
|
||||
state.dockHostState.focused = true;
|
||||
eventResult.consumed = tabStripResult.priority > 0
|
||||
? tabStripResult.consumed
|
||||
: true;
|
||||
break;
|
||||
|
||||
case UIEditorDockHostHitTargetKind::None:
|
||||
default:
|
||||
if (!HasFocusedTabStrip(state)) {
|
||||
state.dockHostState.focused = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::KeyDown:
|
||||
if (tabStripResult.commandRequested && !tabStripResult.panelId.empty()) {
|
||||
eventResult.commandResult = DispatchPanelCommand(
|
||||
controller,
|
||||
tabStripResult.commandKind,
|
||||
tabStripResult.panelId);
|
||||
eventResult.commandExecuted =
|
||||
eventResult.commandResult.status !=
|
||||
UIEditorWorkspaceCommandStatus::Rejected;
|
||||
eventResult.consumed = true;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
state.dockHostState.focused = true;
|
||||
} else if (tabStripResult.priority > 0) {
|
||||
eventResult.consumed = tabStripResult.consumed;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
SyncDockHostTabStripVisualStates(state);
|
||||
layout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
state.dockHostState,
|
||||
metrics);
|
||||
PruneTabStripInteractionEntries(state, layout);
|
||||
SyncDockHostTabStripVisualStates(state);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
SyncDockHostTabStripVisualStates(state);
|
||||
layout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
state.dockHostState,
|
||||
metrics);
|
||||
PruneTabStripInteractionEntries(state, layout);
|
||||
SyncDockHostTabStripVisualStates(state);
|
||||
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
|
||||
226
new_editor/src/Shell/UIEditorMenuBar.cpp
Normal file
226
new_editor/src/Shell/UIEditorMenuBar.cpp
Normal file
@@ -0,0 +1,226 @@
|
||||
#include <XCEditor/Shell/UIEditorMenuBar.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
float ResolveEstimatedLabelWidth(
|
||||
const UIEditorMenuBarItem& item,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
if (item.desiredLabelWidth > 0.0f) {
|
||||
return item.desiredLabelWidth;
|
||||
}
|
||||
|
||||
return static_cast<float>(item.label.size()) * ClampNonNegative(metrics.estimatedGlyphWidth);
|
||||
}
|
||||
|
||||
float ResolveLabelTop(const UIRect& rect, const UIEditorMenuBarMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kMenuBarFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
}
|
||||
|
||||
bool IsButtonFocused(
|
||||
const UIEditorMenuBarState& state,
|
||||
std::size_t index) {
|
||||
if (!state.focused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state.openIndex == index || state.hoveredIndex == index;
|
||||
}
|
||||
|
||||
UIColor ResolveButtonFillColor(
|
||||
bool open,
|
||||
bool hovered,
|
||||
const UIEditorMenuBarPalette& palette) {
|
||||
if (open) {
|
||||
return palette.buttonOpenColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.buttonHoveredColor;
|
||||
}
|
||||
|
||||
return palette.buttonColor;
|
||||
}
|
||||
|
||||
UIColor ResolveButtonBorderColor(
|
||||
bool open,
|
||||
bool focused,
|
||||
const UIEditorMenuBarPalette& palette) {
|
||||
if (focused) {
|
||||
return palette.focusedBorderColor;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
return palette.openBorderColor;
|
||||
}
|
||||
|
||||
return palette.borderColor;
|
||||
}
|
||||
|
||||
float ResolveButtonBorderThickness(
|
||||
bool open,
|
||||
bool focused,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
if (focused) {
|
||||
return metrics.focusedBorderThickness;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
return metrics.openBorderThickness;
|
||||
}
|
||||
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
float ResolveUIEditorMenuBarDesiredButtonWidth(
|
||||
const UIEditorMenuBarItem& item,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
return ResolveEstimatedLabelWidth(item, metrics) + ClampNonNegative(metrics.buttonPaddingX) * 2.0f;
|
||||
}
|
||||
|
||||
UIEditorMenuBarLayout BuildUIEditorMenuBarLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
UIEditorMenuBarLayout layout = {};
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
layout.contentRect = UIRect(
|
||||
layout.bounds.x + ClampNonNegative(metrics.horizontalInset),
|
||||
layout.bounds.y + ClampNonNegative(metrics.verticalInset),
|
||||
(std::max)(
|
||||
layout.bounds.width - ClampNonNegative(metrics.horizontalInset) * 2.0f,
|
||||
0.0f),
|
||||
(std::max)(
|
||||
layout.bounds.height - ClampNonNegative(metrics.verticalInset) * 2.0f,
|
||||
0.0f));
|
||||
|
||||
layout.buttonRects.reserve(items.size());
|
||||
float cursorX = layout.contentRect.x;
|
||||
for (const UIEditorMenuBarItem& item : items) {
|
||||
const float width = ResolveUIEditorMenuBarDesiredButtonWidth(item, metrics);
|
||||
layout.buttonRects.emplace_back(
|
||||
cursorX,
|
||||
layout.contentRect.y,
|
||||
width,
|
||||
layout.contentRect.height);
|
||||
cursorX += width + ClampNonNegative(metrics.buttonGap);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorMenuBarHitTarget HitTestUIEditorMenuBar(
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const UIPoint& point) {
|
||||
UIEditorMenuBarHitTarget target = {};
|
||||
if (!IsPointInsideRect(layout.bounds, point)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.buttonRects.size(); ++index) {
|
||||
if (IsPointInsideRect(layout.buttonRects[index], point)) {
|
||||
target.kind = UIEditorMenuBarHitTargetKind::Button;
|
||||
target.index = index;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
target.kind = UIEditorMenuBarHitTargetKind::BarBackground;
|
||||
return target;
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuBarBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState& state,
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuBarForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState&,
|
||||
const UIEditorMenuBarPalette& palette,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
|
||||
const UIRect& rect = layout.buttonRects[index];
|
||||
const float textLeft = rect.x + ClampNonNegative(metrics.buttonPaddingX);
|
||||
const float textRight = rect.x + rect.width - ClampNonNegative(metrics.buttonPaddingX);
|
||||
if (textRight <= textLeft) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.PushClipRect(UIRect(textLeft, rect.y, textRight - textLeft, rect.height), true);
|
||||
drawList.AddText(
|
||||
UIPoint(textLeft, ResolveLabelTop(rect, metrics)),
|
||||
items[index].label,
|
||||
items[index].enabled ? palette.textPrimary : palette.textDisabled,
|
||||
kMenuBarFontSize);
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuBar(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState& state,
|
||||
const UIEditorMenuBarPalette& palette,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
const UIEditorMenuBarLayout layout = BuildUIEditorMenuBarLayout(bounds, items, metrics);
|
||||
AppendUIEditorMenuBarBackground(drawList, layout, items, state, palette, metrics);
|
||||
AppendUIEditorMenuBarForeground(drawList, layout, items, state, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
265
new_editor/src/Shell/UIEditorMenuModel.cpp
Normal file
265
new_editor/src/Shell/UIEditorMenuModel.cpp
Normal file
@@ -0,0 +1,265 @@
|
||||
#include <XCEditor/Shell/UIEditorMenuModel.h>
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
|
||||
|
||||
#include <utility>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
UIEditorMenuModelValidationResult MakeValidationError(
|
||||
UIEditorMenuModelValidationCode code,
|
||||
std::string message) {
|
||||
UIEditorMenuModelValidationResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ResolveCheckedState(
|
||||
const UIEditorMenuCheckedStateBinding& binding,
|
||||
const UIEditorWorkspaceController& controller) {
|
||||
if (binding.source == UIEditorMenuCheckedStateSource::None) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
FindUIEditorPanelSessionState(controller.GetSession(), binding.panelId);
|
||||
if (panelState == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (binding.source) {
|
||||
case UIEditorMenuCheckedStateSource::PanelOpen:
|
||||
return panelState->open;
|
||||
case UIEditorMenuCheckedStateSource::PanelVisible:
|
||||
return panelState->visible;
|
||||
case UIEditorMenuCheckedStateSource::PanelActive:
|
||||
return controller.GetWorkspace().activePanelId == binding.panelId;
|
||||
case UIEditorMenuCheckedStateSource::None:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorMenuModelValidationResult ValidateMenuItems(
|
||||
const std::vector<UIEditorMenuItemDescriptor>& items,
|
||||
const UIEditorCommandRegistry& commandRegistry,
|
||||
std::string_view parentPath) {
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
const UIEditorMenuItemDescriptor& item = items[index];
|
||||
const std::string itemPath =
|
||||
std::string(parentPath) + "[" + std::to_string(index) + "]";
|
||||
|
||||
switch (item.kind) {
|
||||
case UIEditorMenuItemKind::Command:
|
||||
if (item.commandId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::EmptyCommandId,
|
||||
"Menu item '" + itemPath + "' must define a commandId.");
|
||||
}
|
||||
if (FindUIEditorCommandDescriptor(commandRegistry, item.commandId) == nullptr) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::UnknownCommandId,
|
||||
"Menu item '" + itemPath + "' references unknown command '" +
|
||||
item.commandId + "'.");
|
||||
}
|
||||
if (item.label.empty()) {
|
||||
const UIEditorCommandDescriptor* descriptor =
|
||||
FindUIEditorCommandDescriptor(commandRegistry, item.commandId);
|
||||
if (descriptor == nullptr || descriptor->displayName.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::MissingItemLabel,
|
||||
"Menu item '" + itemPath + "' must define a label or use a command with displayName.");
|
||||
}
|
||||
}
|
||||
if (!item.children.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::CommandItemHasChildren,
|
||||
"Command menu item '" + itemPath + "' must not define children.");
|
||||
}
|
||||
if (item.checkedState.source != UIEditorMenuCheckedStateSource::None &&
|
||||
item.checkedState.panelId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::MissingCheckedStatePanelId,
|
||||
"Command menu item '" + itemPath + "' checked state requires a panelId.");
|
||||
}
|
||||
break;
|
||||
|
||||
case UIEditorMenuItemKind::Separator:
|
||||
if (!item.commandId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::SeparatorHasCommandId,
|
||||
"Separator menu item '" + itemPath + "' must not define a commandId.");
|
||||
}
|
||||
if (!item.children.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::SeparatorHasChildren,
|
||||
"Separator menu item '" + itemPath + "' must not define children.");
|
||||
}
|
||||
if (item.checkedState.source != UIEditorMenuCheckedStateSource::None) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::UnexpectedCheckedState,
|
||||
"Separator menu item '" + itemPath + "' must not define checked state.");
|
||||
}
|
||||
break;
|
||||
|
||||
case UIEditorMenuItemKind::Submenu:
|
||||
if (item.label.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::SubmenuMissingLabel,
|
||||
"Submenu item '" + itemPath + "' must define a label.");
|
||||
}
|
||||
if (item.children.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::SubmenuEmptyChildren,
|
||||
"Submenu item '" + itemPath + "' must contain at least one child.");
|
||||
}
|
||||
if (!item.commandId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::SubmenuHasCommandId,
|
||||
"Submenu item '" + itemPath + "' must not define a commandId.");
|
||||
}
|
||||
if (item.checkedState.source != UIEditorMenuCheckedStateSource::None) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::UnexpectedCheckedState,
|
||||
"Submenu item '" + itemPath + "' must not define checked state.");
|
||||
}
|
||||
{
|
||||
const auto childValidation =
|
||||
ValidateMenuItems(item.children, commandRegistry, itemPath);
|
||||
if (!childValidation.IsValid()) {
|
||||
return childValidation;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorResolvedMenuItem ResolveMenuItem(
|
||||
const UIEditorMenuItemDescriptor& item,
|
||||
const UIEditorCommandDispatcher& commandDispatcher,
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShortcutManager* shortcutManager) {
|
||||
UIEditorResolvedMenuItem resolved = {};
|
||||
resolved.kind = item.kind;
|
||||
resolved.itemId = item.itemId;
|
||||
resolved.label = item.label;
|
||||
resolved.commandId = item.commandId;
|
||||
|
||||
switch (item.kind) {
|
||||
case UIEditorMenuItemKind::Separator:
|
||||
resolved.enabled = false;
|
||||
return resolved;
|
||||
|
||||
case UIEditorMenuItemKind::Submenu:
|
||||
for (const UIEditorMenuItemDescriptor& child : item.children) {
|
||||
resolved.children.push_back(
|
||||
ResolveMenuItem(child, commandDispatcher, controller, shortcutManager));
|
||||
}
|
||||
resolved.enabled = !resolved.children.empty();
|
||||
return resolved;
|
||||
|
||||
case UIEditorMenuItemKind::Command:
|
||||
break;
|
||||
}
|
||||
|
||||
const UIEditorCommandEvaluationResult evaluation =
|
||||
commandDispatcher.Evaluate(item.commandId, controller);
|
||||
resolved.commandDisplayName = evaluation.displayName;
|
||||
if (resolved.label.empty()) {
|
||||
resolved.label = evaluation.displayName;
|
||||
}
|
||||
resolved.shortcutText =
|
||||
shortcutManager != nullptr
|
||||
? shortcutManager->GetPreferredShortcutText(item.commandId)
|
||||
: std::string();
|
||||
resolved.enabled = evaluation.IsExecutable();
|
||||
resolved.previewStatus = evaluation.previewResult.status;
|
||||
resolved.message = evaluation.message;
|
||||
resolved.checked = ResolveCheckedState(item.checkedState, controller);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorMenuItemKindName(UIEditorMenuItemKind kind) {
|
||||
switch (kind) {
|
||||
case UIEditorMenuItemKind::Command:
|
||||
return "Command";
|
||||
case UIEditorMenuItemKind::Separator:
|
||||
return "Separator";
|
||||
case UIEditorMenuItemKind::Submenu:
|
||||
return "Submenu";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
UIEditorMenuModelValidationResult ValidateUIEditorMenuModel(
|
||||
const UIEditorMenuModel& model,
|
||||
const UIEditorCommandRegistry& commandRegistry) {
|
||||
const auto commandValidation = ValidateUIEditorCommandRegistry(commandRegistry);
|
||||
if (!commandValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::InvalidCommandRegistry,
|
||||
commandValidation.message);
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> seenMenuIds = {};
|
||||
for (std::size_t index = 0; index < model.menus.size(); ++index) {
|
||||
const UIEditorMenuDescriptor& menu = model.menus[index];
|
||||
const std::string menuPath = "menus[" + std::to_string(index) + "]";
|
||||
if (menu.menuId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::EmptyMenuId,
|
||||
menuPath + " must define menuId.");
|
||||
}
|
||||
if (menu.label.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::EmptyMenuLabel,
|
||||
menuPath + " must define label.");
|
||||
}
|
||||
if (!seenMenuIds.insert(menu.menuId).second) {
|
||||
return MakeValidationError(
|
||||
UIEditorMenuModelValidationCode::DuplicateMenuId,
|
||||
"Duplicate menuId '" + menu.menuId + "'.");
|
||||
}
|
||||
|
||||
const auto itemValidation =
|
||||
ValidateMenuItems(menu.items, commandRegistry, menuPath + ".items");
|
||||
if (!itemValidation.IsValid()) {
|
||||
return itemValidation;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorResolvedMenuModel BuildUIEditorResolvedMenuModel(
|
||||
const UIEditorMenuModel& model,
|
||||
const UIEditorCommandDispatcher& commandDispatcher,
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShortcutManager* shortcutManager) {
|
||||
UIEditorResolvedMenuModel resolved = {};
|
||||
for (const UIEditorMenuDescriptor& menu : model.menus) {
|
||||
UIEditorResolvedMenuDescriptor resolvedMenu = {};
|
||||
resolvedMenu.menuId = menu.menuId;
|
||||
resolvedMenu.label = menu.label;
|
||||
for (const UIEditorMenuItemDescriptor& item : menu.items) {
|
||||
resolvedMenu.items.push_back(
|
||||
ResolveMenuItem(item, commandDispatcher, controller, shortcutManager));
|
||||
}
|
||||
resolved.menus.push_back(std::move(resolvedMenu));
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
286
new_editor/src/Shell/UIEditorMenuPopup.cpp
Normal file
286
new_editor/src/Shell/UIEditorMenuPopup.cpp
Normal file
@@ -0,0 +1,286 @@
|
||||
#include <XCEditor/Shell/UIEditorMenuPopup.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Editor::UIEditorMenuItemKind;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
float ResolveEstimatedWidth(float explicitWidth, std::string_view text, float glyphWidth) {
|
||||
if (explicitWidth > 0.0f) {
|
||||
return explicitWidth;
|
||||
}
|
||||
|
||||
return static_cast<float>(text.size()) * glyphWidth;
|
||||
}
|
||||
|
||||
bool IsInteractiveItem(const UIEditorMenuPopupItem& item) {
|
||||
return item.kind != UIEditorMenuItemKind::Separator;
|
||||
}
|
||||
|
||||
float ResolveRowTextTop(const UIRect& rect, const UIEditorMenuPopupMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - metrics.labelFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
}
|
||||
|
||||
float ResolveGlyphTop(const UIRect& rect, const UIEditorMenuPopupMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - metrics.glyphFontSize) * 0.5f) - 0.5f;
|
||||
}
|
||||
|
||||
bool IsHighlighted(const UIEditorMenuPopupState& state, std::size_t index) {
|
||||
return state.hoveredIndex == index || state.submenuOpenIndex == index;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
float ResolveUIEditorMenuPopupDesiredWidth(
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
float widestRow = 0.0f;
|
||||
for (const UIEditorMenuPopupItem& item : items) {
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float labelWidth =
|
||||
ResolveEstimatedWidth(item.desiredLabelWidth, item.label, metrics.estimatedGlyphWidth);
|
||||
const float shortcutWidth =
|
||||
item.shortcutText.empty()
|
||||
? 0.0f
|
||||
: ResolveEstimatedWidth(
|
||||
item.desiredShortcutWidth,
|
||||
item.shortcutText,
|
||||
metrics.estimatedGlyphWidth);
|
||||
const float submenuWidth =
|
||||
item.hasSubmenu ? ClampNonNegative(metrics.submenuIndicatorWidth) : 0.0f;
|
||||
const float rowWidth =
|
||||
ClampNonNegative(metrics.labelInsetX) +
|
||||
ClampNonNegative(metrics.checkColumnWidth) +
|
||||
labelWidth +
|
||||
(shortcutWidth > 0.0f ? ClampNonNegative(metrics.shortcutGap) + shortcutWidth : 0.0f) +
|
||||
submenuWidth +
|
||||
ClampNonNegative(metrics.shortcutInsetRight);
|
||||
widestRow = (std::max)(widestRow, rowWidth);
|
||||
}
|
||||
|
||||
return widestRow + ClampNonNegative(metrics.contentPaddingX) * 2.0f;
|
||||
}
|
||||
|
||||
float MeasureUIEditorMenuPopupHeight(
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
float height = ClampNonNegative(metrics.contentPaddingY) * 2.0f;
|
||||
for (const UIEditorMenuPopupItem& item : items) {
|
||||
height += item.kind == UIEditorMenuItemKind::Separator
|
||||
? ClampNonNegative(metrics.separatorHeight)
|
||||
: ClampNonNegative(metrics.itemHeight);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
UIEditorMenuPopupLayout BuildUIEditorMenuPopupLayout(
|
||||
const UIRect& popupRect,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
UIEditorMenuPopupLayout layout = {};
|
||||
layout.popupRect = UIRect(
|
||||
popupRect.x,
|
||||
popupRect.y,
|
||||
ClampNonNegative(popupRect.width),
|
||||
ClampNonNegative(popupRect.height));
|
||||
layout.contentRect = UIRect(
|
||||
layout.popupRect.x + ClampNonNegative(metrics.contentPaddingX),
|
||||
layout.popupRect.y + ClampNonNegative(metrics.contentPaddingY),
|
||||
(std::max)(
|
||||
layout.popupRect.width - ClampNonNegative(metrics.contentPaddingX) * 2.0f,
|
||||
0.0f),
|
||||
(std::max)(
|
||||
layout.popupRect.height - ClampNonNegative(metrics.contentPaddingY) * 2.0f,
|
||||
0.0f));
|
||||
|
||||
float cursorY = layout.contentRect.y;
|
||||
layout.itemRects.reserve(items.size());
|
||||
for (const UIEditorMenuPopupItem& item : items) {
|
||||
const float itemHeight = item.kind == UIEditorMenuItemKind::Separator
|
||||
? ClampNonNegative(metrics.separatorHeight)
|
||||
: ClampNonNegative(metrics.itemHeight);
|
||||
layout.itemRects.emplace_back(
|
||||
layout.contentRect.x,
|
||||
cursorY,
|
||||
layout.contentRect.width,
|
||||
itemHeight);
|
||||
cursorY += itemHeight;
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorMenuPopupHitTarget HitTestUIEditorMenuPopup(
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIPoint& point) {
|
||||
UIEditorMenuPopupHitTarget target = {};
|
||||
if (!IsPointInsideRect(layout.popupRect, point)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) {
|
||||
if (IsInteractiveItem(items[index]) && IsPointInsideRect(layout.itemRects[index], point)) {
|
||||
target.kind = UIEditorMenuPopupHitTargetKind::Item;
|
||||
target.index = index;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
target.kind = UIEditorMenuPopupHitTargetKind::PopupSurface;
|
||||
return target;
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuPopupBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState& state,
|
||||
const UIEditorMenuPopupPalette& palette,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.popupRect, palette.popupColor, metrics.popupCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.popupRect,
|
||||
palette.borderColor,
|
||||
metrics.borderThickness,
|
||||
metrics.popupCornerRounding);
|
||||
|
||||
for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) {
|
||||
const UIEditorMenuPopupItem& item = items[index];
|
||||
const UIRect& rect = layout.itemRects[index];
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
const float lineY = rect.y + rect.height * 0.5f;
|
||||
drawList.AddFilledRect(
|
||||
UIRect(rect.x + 8.0f, lineY, (std::max)(rect.width - 16.0f, 0.0f), metrics.separatorThickness),
|
||||
palette.separatorColor);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsHighlighted(state, index)) {
|
||||
drawList.AddFilledRect(
|
||||
rect,
|
||||
state.submenuOpenIndex == index ? palette.itemOpenColor : palette.itemHoverColor,
|
||||
metrics.rowCornerRounding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuPopupForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState&,
|
||||
const UIEditorMenuPopupPalette& palette,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) {
|
||||
const UIEditorMenuPopupItem& item = items[index];
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIRect& rect = layout.itemRects[index];
|
||||
const UIColor mainColor = item.enabled ? palette.textPrimary : palette.textDisabled;
|
||||
const UIColor secondaryColor = item.enabled ? palette.textMuted : palette.textDisabled;
|
||||
|
||||
const float checkLeft = rect.x + 6.0f;
|
||||
if (item.checked) {
|
||||
drawList.AddText(
|
||||
UIPoint(checkLeft, ResolveGlyphTop(rect, metrics)),
|
||||
"*",
|
||||
palette.glyphColor,
|
||||
metrics.glyphFontSize);
|
||||
}
|
||||
|
||||
const float labelLeft =
|
||||
rect.x + ClampNonNegative(metrics.labelInsetX) + ClampNonNegative(metrics.checkColumnWidth);
|
||||
const float labelRight =
|
||||
rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight);
|
||||
float labelClipWidth = (std::max)(labelRight - labelLeft, 0.0f);
|
||||
if (!item.shortcutText.empty()) {
|
||||
const float shortcutWidth =
|
||||
ResolveEstimatedWidth(
|
||||
item.desiredShortcutWidth,
|
||||
item.shortcutText,
|
||||
metrics.estimatedGlyphWidth);
|
||||
labelClipWidth = (std::max)(
|
||||
labelClipWidth - shortcutWidth - ClampNonNegative(metrics.shortcutGap),
|
||||
0.0f);
|
||||
}
|
||||
if (item.hasSubmenu) {
|
||||
labelClipWidth = (std::max)(
|
||||
labelClipWidth - ClampNonNegative(metrics.submenuIndicatorWidth),
|
||||
0.0f);
|
||||
}
|
||||
|
||||
drawList.PushClipRect(UIRect(labelLeft, rect.y, labelClipWidth, rect.height), true);
|
||||
drawList.AddText(
|
||||
UIPoint(labelLeft, ResolveRowTextTop(rect, metrics)),
|
||||
item.label,
|
||||
mainColor,
|
||||
metrics.labelFontSize);
|
||||
drawList.PopClipRect();
|
||||
|
||||
if (!item.shortcutText.empty()) {
|
||||
const float shortcutWidth =
|
||||
ResolveEstimatedWidth(
|
||||
item.desiredShortcutWidth,
|
||||
item.shortcutText,
|
||||
metrics.estimatedGlyphWidth);
|
||||
const float shortcutLeft = rect.x + rect.width -
|
||||
ClampNonNegative(metrics.shortcutInsetRight) -
|
||||
shortcutWidth -
|
||||
(item.hasSubmenu ? ClampNonNegative(metrics.submenuIndicatorWidth) : 0.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(shortcutLeft, ResolveRowTextTop(rect, metrics)),
|
||||
item.shortcutText,
|
||||
secondaryColor,
|
||||
metrics.labelFontSize);
|
||||
}
|
||||
|
||||
if (item.hasSubmenu) {
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight),
|
||||
ResolveGlyphTop(rect, metrics)),
|
||||
">",
|
||||
palette.glyphColor,
|
||||
metrics.glyphFontSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuPopup(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& popupRect,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState& state,
|
||||
const UIEditorMenuPopupPalette& palette,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
const UIEditorMenuPopupLayout layout =
|
||||
BuildUIEditorMenuPopupLayout(popupRect, items, metrics);
|
||||
AppendUIEditorMenuPopupBackground(drawList, layout, items, state, palette, metrics);
|
||||
AppendUIEditorMenuPopupForeground(drawList, layout, items, state, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
222
new_editor/src/Shell/UIEditorMenuSession.cpp
Normal file
222
new_editor/src/Shell/UIEditorMenuSession.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include <XCEditor/Shell/UIEditorMenuSession.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool AreEquivalentPopupEntries(
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayEntry& lhs,
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayEntry& rhs) {
|
||||
return lhs.popupId == rhs.popupId &&
|
||||
lhs.parentPopupId == rhs.parentPopupId &&
|
||||
lhs.anchorRect.x == rhs.anchorRect.x &&
|
||||
lhs.anchorRect.y == rhs.anchorRect.y &&
|
||||
lhs.anchorRect.width == rhs.anchorRect.width &&
|
||||
lhs.anchorRect.height == rhs.anchorRect.height &&
|
||||
lhs.anchorPath == rhs.anchorPath &&
|
||||
lhs.surfacePath == rhs.surfacePath &&
|
||||
lhs.placement == rhs.placement &&
|
||||
lhs.dismissOnPointerOutside == rhs.dismissOnPointerOutside &&
|
||||
lhs.dismissOnEscape == rhs.dismissOnEscape &&
|
||||
lhs.dismissOnFocusLoss == rhs.dismissOnFocusLoss;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool UIEditorMenuSession::IsPopupOpen(std::string_view popupId) const {
|
||||
return m_popupOverlayModel.FindPopup(popupId) != nullptr;
|
||||
}
|
||||
|
||||
const UIEditorMenuPopupState* UIEditorMenuSession::FindPopupState(
|
||||
std::string_view popupId) const {
|
||||
for (const UIEditorMenuPopupState& state : m_popupStates) {
|
||||
if (state.popupId == popupId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UIEditorMenuSession::Reset() {
|
||||
m_openRootMenuId.clear();
|
||||
m_popupOverlayModel = {};
|
||||
m_popupStates.clear();
|
||||
m_openSubmenuItemIds.clear();
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::OpenMenuBarRoot(
|
||||
std::string_view menuId,
|
||||
::XCEngine::UI::Widgets::UIPopupOverlayEntry entry) {
|
||||
return OpenRootMenu(menuId, std::move(entry));
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::OpenRootMenu(
|
||||
std::string_view menuId,
|
||||
::XCEngine::UI::Widgets::UIPopupOverlayEntry entry) {
|
||||
if (menuId.empty() || entry.popupId.empty()) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayEntry* rootPopup =
|
||||
m_popupOverlayModel.GetRootPopup();
|
||||
if (rootPopup != nullptr &&
|
||||
m_openRootMenuId == menuId &&
|
||||
AreEquivalentPopupEntries(*rootPopup, entry)) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
entry.parentPopupId.clear();
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.OpenPopup(std::move(entry));
|
||||
if (mutation.changed) {
|
||||
m_popupStates.clear();
|
||||
|
||||
UIEditorMenuPopupState rootState = {};
|
||||
rootState.popupId = mutation.openedPopupId;
|
||||
rootState.menuId = std::string(menuId);
|
||||
m_popupStates.push_back(std::move(rootState));
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::HoverMenuBarRoot(
|
||||
std::string_view menuId,
|
||||
::XCEngine::UI::Widgets::UIPopupOverlayEntry entry) {
|
||||
if (!HasOpenMenu() || IsMenuOpen(menuId)) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
return OpenRootMenu(menuId, std::move(entry));
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::HoverSubmenu(
|
||||
std::string_view itemId,
|
||||
::XCEngine::UI::Widgets::UIPopupOverlayEntry entry) {
|
||||
if (!HasOpenMenu() ||
|
||||
itemId.empty() ||
|
||||
entry.popupId.empty() ||
|
||||
entry.parentPopupId.empty() ||
|
||||
m_popupOverlayModel.FindPopup(entry.parentPopupId) == nullptr) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayEntry* existingPopup =
|
||||
m_popupOverlayModel.FindPopup(entry.popupId);
|
||||
if (existingPopup != nullptr &&
|
||||
existingPopup->parentPopupId == entry.parentPopupId) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.OpenPopup(std::move(entry));
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
|
||||
UIEditorMenuPopupState popupState = {};
|
||||
popupState.popupId = mutation.openedPopupId;
|
||||
popupState.menuId = m_openRootMenuId;
|
||||
popupState.itemId = std::string(itemId);
|
||||
m_popupStates.push_back(std::move(popupState));
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::CloseAll(
|
||||
::XCEngine::UI::Widgets::UIPopupDismissReason dismissReason) {
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.CloseAll(dismissReason);
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromEscape() {
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.DismissFromEscape();
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromPointerDown(
|
||||
const UIInputPath& hitPath) {
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.DismissFromPointerDown(hitPath);
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromFocusLoss(
|
||||
const UIInputPath& focusedPath) {
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.DismissFromFocusLoss(focusedPath);
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::BuildResult(
|
||||
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult& mutation) const {
|
||||
UIEditorMenuSessionMutationResult result = {};
|
||||
result.changed = mutation.changed;
|
||||
result.openRootMenuId = m_openRootMenuId;
|
||||
result.openedPopupId = mutation.openedPopupId;
|
||||
result.closedPopupIds = mutation.closedPopupIds;
|
||||
result.dismissReason = mutation.dismissReason;
|
||||
return result;
|
||||
}
|
||||
|
||||
void UIEditorMenuSession::RemoveClosedPopupStates(
|
||||
const std::vector<std::string>& closedPopupIds) {
|
||||
if (closedPopupIds.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::erase_if(
|
||||
m_popupStates,
|
||||
[&closedPopupIds](const UIEditorMenuPopupState& state) {
|
||||
return std::find(
|
||||
closedPopupIds.begin(),
|
||||
closedPopupIds.end(),
|
||||
state.popupId) != closedPopupIds.end();
|
||||
});
|
||||
}
|
||||
|
||||
void UIEditorMenuSession::RebuildDerivedState() {
|
||||
m_openSubmenuItemIds.clear();
|
||||
|
||||
if (m_popupStates.empty() || !m_popupOverlayModel.HasOpenPopups()) {
|
||||
m_openRootMenuId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
m_openRootMenuId = m_popupStates.front().menuId;
|
||||
for (std::size_t index = 1u; index < m_popupStates.size(); ++index) {
|
||||
if (!m_popupStates[index].itemId.empty()) {
|
||||
m_openSubmenuItemIds.push_back(m_popupStates[index].itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
241
new_editor/src/Shell/UIEditorPanelContentHost.cpp
Normal file
241
new_editor/src/Shell/UIEditorPanelContentHost.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
const ::XCEngine::UI::UIRect* FindVisiblePanelBodyRect(
|
||||
const Widgets::UIEditorDockHostLayout& layout,
|
||||
std::string_view panelId) {
|
||||
for (const Widgets::UIEditorDockHostPanelLayout& panel : layout.panels) {
|
||||
if (panel.panelId == panelId) {
|
||||
return &panel.frameLayout.bodyRect;
|
||||
}
|
||||
}
|
||||
|
||||
for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
|
||||
if (tabStack.selectedPanelId == panelId) {
|
||||
return &tabStack.contentFrameLayout.bodyRect;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool AreRectsEquivalent(
|
||||
const ::XCEngine::UI::UIRect& lhs,
|
||||
const ::XCEngine::UI::UIRect& rhs) {
|
||||
return lhs.x == rhs.x &&
|
||||
lhs.y == rhs.y &&
|
||||
lhs.width == rhs.width &&
|
||||
lhs.height == rhs.height;
|
||||
}
|
||||
|
||||
bool SupportsBinding(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorPanelContentHostBinding& binding) {
|
||||
if (!IsUIEditorPanelPresentationExternallyHosted(binding.kind)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(panelRegistry, binding.panelId);
|
||||
return descriptor != nullptr && descriptor->presentationKind == binding.kind;
|
||||
}
|
||||
|
||||
UIEditorPanelContentHostPanelState* FindMutablePanelState(
|
||||
UIEditorPanelContentHostState& state,
|
||||
std::string_view panelId) {
|
||||
for (UIEditorPanelContentHostPanelState& panelState : state.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorPanelContentHostPanelState& EnsurePanelState(
|
||||
UIEditorPanelContentHostState& state,
|
||||
std::string_view panelId,
|
||||
UIEditorPanelPresentationKind kind) {
|
||||
if (UIEditorPanelContentHostPanelState* existing =
|
||||
FindMutablePanelState(state, panelId)) {
|
||||
existing->kind = kind;
|
||||
return *existing;
|
||||
}
|
||||
|
||||
UIEditorPanelContentHostPanelState panelState = {};
|
||||
panelState.panelId = std::string(panelId);
|
||||
panelState.kind = kind;
|
||||
state.panelStates.push_back(std::move(panelState));
|
||||
return state.panelStates.back();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorPanelContentHostEventKindName(
|
||||
UIEditorPanelContentHostEventKind kind) {
|
||||
switch (kind) {
|
||||
case UIEditorPanelContentHostEventKind::Mounted:
|
||||
return "Mounted";
|
||||
case UIEditorPanelContentHostEventKind::Unmounted:
|
||||
return "Unmounted";
|
||||
case UIEditorPanelContentHostEventKind::BoundsChanged:
|
||||
return "BoundsChanged";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
bool IsUIEditorPanelPresentationExternallyHosted(UIEditorPanelPresentationKind kind) {
|
||||
return kind == UIEditorPanelPresentationKind::ViewportShell ||
|
||||
kind == UIEditorPanelPresentationKind::HostedContent;
|
||||
}
|
||||
|
||||
const UIEditorPanelContentHostMountRequest* FindUIEditorPanelContentHostMountRequest(
|
||||
const UIEditorPanelContentHostRequest& request,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorPanelContentHostMountRequest& mountRequest : request.mountRequests) {
|
||||
if (mountRequest.panelId == panelId) {
|
||||
return &mountRequest;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorPanelContentHostPanelState* FindUIEditorPanelContentHostPanelState(
|
||||
const UIEditorPanelContentHostState& state,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorPanelContentHostPanelState& panelState : state.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorPanelContentHostPanelState* FindUIEditorPanelContentHostPanelState(
|
||||
const UIEditorPanelContentHostFrame& frame,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorPanelContentHostPanelState& panelState : frame.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorPanelContentHostRequest ResolveUIEditorPanelContentHostRequest(
|
||||
const Widgets::UIEditorDockHostLayout& dockHostLayout,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const std::vector<UIEditorPanelContentHostBinding>& bindings) {
|
||||
UIEditorPanelContentHostRequest request = {};
|
||||
for (const UIEditorPanelContentHostBinding& binding : bindings) {
|
||||
if (!SupportsBinding(panelRegistry, binding)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ::XCEngine::UI::UIRect* bodyRect =
|
||||
FindVisiblePanelBodyRect(dockHostLayout, binding.panelId);
|
||||
if (bodyRect == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorPanelContentHostMountRequest mountRequest = {};
|
||||
mountRequest.panelId = binding.panelId;
|
||||
mountRequest.kind = binding.kind;
|
||||
mountRequest.bounds = *bodyRect;
|
||||
request.mountRequests.push_back(std::move(mountRequest));
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
UIEditorPanelContentHostFrame UpdateUIEditorPanelContentHost(
|
||||
UIEditorPanelContentHostState& state,
|
||||
const UIEditorPanelContentHostRequest& request,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const std::vector<UIEditorPanelContentHostBinding>& bindings) {
|
||||
UIEditorPanelContentHostFrame frame = {};
|
||||
std::unordered_set<std::string> supportedPanelIds = {};
|
||||
for (const UIEditorPanelContentHostBinding& binding : bindings) {
|
||||
if (!SupportsBinding(panelRegistry, binding)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
supportedPanelIds.insert(binding.panelId);
|
||||
EnsurePanelState(state, binding.panelId, binding.kind);
|
||||
}
|
||||
|
||||
state.panelStates.erase(
|
||||
std::remove_if(
|
||||
state.panelStates.begin(),
|
||||
state.panelStates.end(),
|
||||
[&](const UIEditorPanelContentHostPanelState& panelState) {
|
||||
return !supportedPanelIds.contains(panelState.panelId);
|
||||
}),
|
||||
state.panelStates.end());
|
||||
|
||||
for (UIEditorPanelContentHostPanelState& panelState : state.panelStates) {
|
||||
const UIEditorPanelContentHostMountRequest* mountRequest =
|
||||
FindUIEditorPanelContentHostMountRequest(request, panelState.panelId);
|
||||
|
||||
const bool wasMounted = panelState.mounted;
|
||||
const ::XCEngine::UI::UIRect previousBounds = panelState.bounds;
|
||||
|
||||
panelState.mounted = mountRequest != nullptr;
|
||||
panelState.bounds = mountRequest != nullptr ? mountRequest->bounds : ::XCEngine::UI::UIRect{};
|
||||
if (mountRequest != nullptr) {
|
||||
panelState.kind = mountRequest->kind;
|
||||
}
|
||||
|
||||
if (!wasMounted && panelState.mounted) {
|
||||
frame.events.push_back({
|
||||
UIEditorPanelContentHostEventKind::Mounted,
|
||||
panelState.panelId,
|
||||
panelState.kind,
|
||||
panelState.bounds
|
||||
});
|
||||
} else if (wasMounted && !panelState.mounted) {
|
||||
frame.events.push_back({
|
||||
UIEditorPanelContentHostEventKind::Unmounted,
|
||||
panelState.panelId,
|
||||
panelState.kind,
|
||||
previousBounds
|
||||
});
|
||||
} else if (panelState.mounted &&
|
||||
!AreRectsEquivalent(previousBounds, panelState.bounds)) {
|
||||
frame.events.push_back({
|
||||
UIEditorPanelContentHostEventKind::BoundsChanged,
|
||||
panelState.panelId,
|
||||
panelState.kind,
|
||||
panelState.bounds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
frame.panelStates = state.panelStates;
|
||||
return frame;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectMountedUIEditorPanelContentHostPanelIds(
|
||||
const UIEditorPanelContentHostFrame& frame) {
|
||||
std::vector<std::string> panelIds = {};
|
||||
for (const UIEditorPanelContentHostPanelState& panelState : frame.panelStates) {
|
||||
if (panelState.mounted) {
|
||||
panelIds.push_back(panelState.panelId);
|
||||
}
|
||||
}
|
||||
return panelIds;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
356
new_editor/src/Shell/UIEditorPanelFrame.cpp
Normal file
356
new_editor/src/Shell/UIEditorPanelFrame.cpp
Normal file
@@ -0,0 +1,356 @@
|
||||
#include <XCEditor/Shell/UIEditorPanelFrame.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
float ResolveActionButtonExtent(
|
||||
const UIEditorPanelFrameMetrics& metrics,
|
||||
float headerHeight) {
|
||||
const float inset = (std::max)(metrics.actionInsetX, 0.0f);
|
||||
return (std::min)(
|
||||
(std::max)(metrics.actionButtonExtent, 0.0f),
|
||||
(std::max)(headerHeight - inset * 2.0f, 0.0f));
|
||||
}
|
||||
|
||||
float ResolveActionCornerRounding(const UIRect& rect) {
|
||||
return (std::min)((std::max)((std::min)(rect.width, rect.height) * 0.35f, 0.0f), 6.0f);
|
||||
}
|
||||
|
||||
float ResolveActionGlyphLeft(const UIRect& rect) {
|
||||
return rect.x + (std::max)(0.0f, (rect.width - 7.0f) * 0.5f);
|
||||
}
|
||||
|
||||
float ResolveActionGlyphTop(const UIRect& rect) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - 12.0f) * 0.5f) - 0.5f;
|
||||
}
|
||||
|
||||
UIColor ResolveActionFillColor(
|
||||
bool selected,
|
||||
bool hovered,
|
||||
const UIEditorPanelFramePalette& palette) {
|
||||
if (selected) {
|
||||
return palette.actionButtonSelectedColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.actionButtonHoveredColor;
|
||||
}
|
||||
|
||||
return palette.actionButtonColor;
|
||||
}
|
||||
|
||||
void AppendActionButton(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
std::string_view glyph,
|
||||
bool selected,
|
||||
bool hovered,
|
||||
const UIEditorPanelFramePalette& palette) {
|
||||
if (rect.width <= 0.0f || rect.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float cornerRounding = ResolveActionCornerRounding(rect);
|
||||
drawList.AddFilledRect(
|
||||
rect,
|
||||
ResolveActionFillColor(selected, hovered, palette),
|
||||
cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
rect,
|
||||
palette.actionButtonBorderColor,
|
||||
1.0f,
|
||||
cornerRounding);
|
||||
drawList.AddText(
|
||||
UIPoint(ResolveActionGlyphLeft(rect), ResolveActionGlyphTop(rect)),
|
||||
std::string(glyph),
|
||||
palette.actionGlyphColor,
|
||||
12.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsUIEditorPanelFramePointInside(
|
||||
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;
|
||||
}
|
||||
|
||||
bool IsUIEditorPanelFramePinButtonVisible(const UIEditorPanelFrameState& state) {
|
||||
return state.pinnable;
|
||||
}
|
||||
|
||||
bool IsUIEditorPanelFrameCloseButtonVisible(const UIEditorPanelFrameState& state) {
|
||||
return state.closable;
|
||||
}
|
||||
|
||||
UIEditorPanelFrameLayout BuildUIEditorPanelFrameLayout(
|
||||
const UIRect& frameRect,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
UIEditorPanelFrameLayout layout = {};
|
||||
layout.frameRect = UIRect(
|
||||
frameRect.x,
|
||||
frameRect.y,
|
||||
ClampNonNegative(frameRect.width),
|
||||
ClampNonNegative(frameRect.height));
|
||||
|
||||
const float headerHeight =
|
||||
(std::min)(ClampNonNegative(metrics.headerHeight), layout.frameRect.height);
|
||||
const float footerHeight =
|
||||
state.showFooter
|
||||
? (std::min)(
|
||||
ClampNonNegative(metrics.footerHeight),
|
||||
(std::max)(layout.frameRect.height - headerHeight, 0.0f))
|
||||
: 0.0f;
|
||||
|
||||
layout.hasFooter = footerHeight > 0.0f;
|
||||
layout.showPinButton = IsUIEditorPanelFramePinButtonVisible(state);
|
||||
layout.showCloseButton = IsUIEditorPanelFrameCloseButtonVisible(state);
|
||||
|
||||
layout.headerRect = UIRect(
|
||||
layout.frameRect.x,
|
||||
layout.frameRect.y,
|
||||
layout.frameRect.width,
|
||||
headerHeight);
|
||||
|
||||
if (layout.hasFooter) {
|
||||
layout.footerRect = UIRect(
|
||||
layout.frameRect.x,
|
||||
layout.frameRect.y + layout.frameRect.height - footerHeight,
|
||||
layout.frameRect.width,
|
||||
footerHeight);
|
||||
}
|
||||
|
||||
const float bodyBandTop = layout.headerRect.y + layout.headerRect.height;
|
||||
const float bodyBandBottom = layout.hasFooter
|
||||
? layout.footerRect.y
|
||||
: layout.frameRect.y + layout.frameRect.height;
|
||||
const float contentPadding = ClampNonNegative(metrics.contentPadding);
|
||||
|
||||
layout.bodyRect = UIRect(
|
||||
layout.frameRect.x + contentPadding,
|
||||
bodyBandTop + contentPadding,
|
||||
(std::max)(layout.frameRect.width - contentPadding * 2.0f, 0.0f),
|
||||
(std::max)(bodyBandBottom - bodyBandTop - contentPadding * 2.0f, 0.0f));
|
||||
|
||||
if (layout.headerRect.height <= 0.0f) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const float buttonExtent = ResolveActionButtonExtent(metrics, layout.headerRect.height);
|
||||
if (buttonExtent <= 0.0f) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const float buttonY = layout.headerRect.y + (layout.headerRect.height - buttonExtent) * 0.5f;
|
||||
float buttonRight = layout.headerRect.x + layout.headerRect.width - ClampNonNegative(metrics.actionInsetX);
|
||||
|
||||
if (layout.showCloseButton) {
|
||||
layout.closeButtonRect = UIRect(
|
||||
buttonRight - buttonExtent,
|
||||
buttonY,
|
||||
buttonExtent,
|
||||
buttonExtent);
|
||||
buttonRight = layout.closeButtonRect.x - ClampNonNegative(metrics.actionGap);
|
||||
}
|
||||
|
||||
if (layout.showPinButton) {
|
||||
layout.pinButtonRect = UIRect(
|
||||
buttonRight - buttonExtent,
|
||||
buttonY,
|
||||
buttonExtent,
|
||||
buttonExtent);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIColor ResolveUIEditorPanelFrameBorderColor(
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFramePalette& palette) {
|
||||
if (state.focused) {
|
||||
return palette.focusedBorderColor;
|
||||
}
|
||||
|
||||
if (state.active) {
|
||||
return palette.activeBorderColor;
|
||||
}
|
||||
|
||||
if (state.hovered) {
|
||||
return palette.hoveredBorderColor;
|
||||
}
|
||||
|
||||
return palette.borderColor;
|
||||
}
|
||||
|
||||
float ResolveUIEditorPanelFrameBorderThickness(
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
if (state.focused) {
|
||||
return metrics.focusedBorderThickness;
|
||||
}
|
||||
|
||||
if (state.active) {
|
||||
return metrics.activeBorderThickness;
|
||||
}
|
||||
|
||||
if (state.hovered) {
|
||||
return metrics.hoveredBorderThickness;
|
||||
}
|
||||
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
UIEditorPanelFrameAction HitTestUIEditorPanelFrameAction(
|
||||
const UIEditorPanelFrameLayout& layout,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIPoint& point) {
|
||||
if (layout.showPinButton &&
|
||||
IsUIEditorPanelFramePinButtonVisible(state) &&
|
||||
IsUIEditorPanelFramePointInside(layout.pinButtonRect, point)) {
|
||||
return UIEditorPanelFrameAction::Pin;
|
||||
}
|
||||
|
||||
if (layout.showCloseButton &&
|
||||
IsUIEditorPanelFrameCloseButtonVisible(state) &&
|
||||
IsUIEditorPanelFramePointInside(layout.closeButtonRect, point)) {
|
||||
return UIEditorPanelFrameAction::Close;
|
||||
}
|
||||
|
||||
return UIEditorPanelFrameAction::None;
|
||||
}
|
||||
|
||||
UIEditorPanelFrameHitTarget HitTestUIEditorPanelFrame(
|
||||
const UIEditorPanelFrameLayout& layout,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIPoint& point) {
|
||||
if (!IsUIEditorPanelFramePointInside(layout.frameRect, point)) {
|
||||
return UIEditorPanelFrameHitTarget::None;
|
||||
}
|
||||
|
||||
switch (HitTestUIEditorPanelFrameAction(layout, state, point)) {
|
||||
case UIEditorPanelFrameAction::Pin:
|
||||
return UIEditorPanelFrameHitTarget::PinButton;
|
||||
case UIEditorPanelFrameAction::Close:
|
||||
return UIEditorPanelFrameHitTarget::CloseButton;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsUIEditorPanelFramePointInside(layout.headerRect, point)) {
|
||||
return UIEditorPanelFrameHitTarget::Header;
|
||||
}
|
||||
|
||||
if (layout.hasFooter && IsUIEditorPanelFramePointInside(layout.footerRect, point)) {
|
||||
return UIEditorPanelFrameHitTarget::Footer;
|
||||
}
|
||||
|
||||
if (IsUIEditorPanelFramePointInside(layout.bodyRect, point)) {
|
||||
return UIEditorPanelFrameHitTarget::Body;
|
||||
}
|
||||
|
||||
return UIEditorPanelFrameHitTarget::Body;
|
||||
}
|
||||
|
||||
void AppendUIEditorPanelFrameBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorPanelFrameLayout& layout,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFramePalette& palette,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.frameRect, palette.surfaceColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.frameRect,
|
||||
ResolveUIEditorPanelFrameBorderColor(state, palette),
|
||||
ResolveUIEditorPanelFrameBorderThickness(state, metrics),
|
||||
metrics.cornerRounding);
|
||||
if (layout.headerRect.height > 0.0f) {
|
||||
drawList.AddFilledRect(layout.headerRect, palette.headerColor, metrics.cornerRounding);
|
||||
}
|
||||
if (layout.hasFooter && layout.footerRect.height > 0.0f) {
|
||||
drawList.AddFilledRect(layout.footerRect, palette.footerColor, metrics.cornerRounding);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorPanelFrameForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorPanelFrameLayout& layout,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFrameText& text,
|
||||
const UIEditorPanelFramePalette& palette,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
if (!text.title.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(layout.frameRect.x + metrics.titleInsetX, layout.headerRect.y + metrics.titleInsetY),
|
||||
std::string(text.title),
|
||||
palette.textPrimary,
|
||||
16.0f);
|
||||
}
|
||||
|
||||
if (!text.subtitle.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(layout.frameRect.x + metrics.titleInsetX, layout.headerRect.y + metrics.subtitleInsetY),
|
||||
std::string(text.subtitle),
|
||||
palette.textSecondary,
|
||||
12.0f);
|
||||
}
|
||||
|
||||
if (layout.hasFooter && !text.footer.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(layout.footerRect.x + metrics.footerInsetX, layout.footerRect.y + metrics.footerInsetY),
|
||||
std::string(text.footer),
|
||||
palette.textMuted,
|
||||
11.0f);
|
||||
}
|
||||
|
||||
if (layout.showPinButton) {
|
||||
AppendActionButton(
|
||||
drawList,
|
||||
layout.pinButtonRect,
|
||||
"P",
|
||||
state.pinned,
|
||||
state.pinHovered,
|
||||
palette);
|
||||
}
|
||||
|
||||
if (layout.showCloseButton) {
|
||||
AppendActionButton(
|
||||
drawList,
|
||||
layout.closeButtonRect,
|
||||
"X",
|
||||
false,
|
||||
state.closeHovered,
|
||||
palette);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorPanelFrame(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& frameRect,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFrameText& text,
|
||||
const UIEditorPanelFramePalette& palette,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
const UIEditorPanelFrameLayout layout =
|
||||
BuildUIEditorPanelFrameLayout(frameRect, state, metrics);
|
||||
AppendUIEditorPanelFrameBackground(drawList, layout, state, palette, metrics);
|
||||
AppendUIEditorPanelFrameForeground(drawList, layout, state, text, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
207
new_editor/src/Shell/UIEditorPanelHostLifecycle.cpp
Normal file
207
new_editor/src/Shell/UIEditorPanelHostLifecycle.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
#include <XCEditor/Shell/UIEditorPanelHostLifecycle.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using HostStateMap = std::unordered_map<std::string_view, const UIEditorPanelHostState*>;
|
||||
|
||||
HostStateMap BuildHostStateMap(const std::vector<UIEditorPanelHostState>& states) {
|
||||
HostStateMap map = {};
|
||||
map.reserve(states.size());
|
||||
for (const UIEditorPanelHostState& state : states) {
|
||||
map.emplace(state.panelId, &state);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const UIEditorPanelHostState* FindHostState(
|
||||
const std::vector<UIEditorPanelHostState>& states,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorPanelHostState& state : states) {
|
||||
if (state.panelId == panelId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AppendLeaveEvents(
|
||||
std::vector<UIEditorPanelHostLifecycleEvent>& events,
|
||||
const std::vector<UIEditorPanelHostState>& previousStates,
|
||||
const HostStateMap& currentStateMap) {
|
||||
auto appendCategory = [&](auto predicate, UIEditorPanelHostLifecycleEventKind kind) {
|
||||
for (const UIEditorPanelHostState& previous : previousStates) {
|
||||
const UIEditorPanelHostState emptyState = {};
|
||||
const UIEditorPanelHostState& current =
|
||||
currentStateMap.contains(previous.panelId)
|
||||
? *currentStateMap.at(previous.panelId)
|
||||
: emptyState;
|
||||
if (predicate(previous, current)) {
|
||||
events.push_back({ kind, previous.panelId });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
appendCategory(
|
||||
[](const UIEditorPanelHostState& previous, const UIEditorPanelHostState& current) {
|
||||
return previous.focused && !current.focused;
|
||||
},
|
||||
UIEditorPanelHostLifecycleEventKind::FocusLost);
|
||||
appendCategory(
|
||||
[](const UIEditorPanelHostState& previous, const UIEditorPanelHostState& current) {
|
||||
return previous.active && !current.active;
|
||||
},
|
||||
UIEditorPanelHostLifecycleEventKind::Deactivated);
|
||||
appendCategory(
|
||||
[](const UIEditorPanelHostState& previous, const UIEditorPanelHostState& current) {
|
||||
return previous.visible && !current.visible;
|
||||
},
|
||||
UIEditorPanelHostLifecycleEventKind::Hidden);
|
||||
appendCategory(
|
||||
[](const UIEditorPanelHostState& previous, const UIEditorPanelHostState& current) {
|
||||
return previous.attached && !current.attached;
|
||||
},
|
||||
UIEditorPanelHostLifecycleEventKind::Detached);
|
||||
}
|
||||
|
||||
void AppendEnterEvents(
|
||||
std::vector<UIEditorPanelHostLifecycleEvent>& events,
|
||||
const std::vector<UIEditorPanelHostState>& currentStates,
|
||||
const HostStateMap& previousStateMap) {
|
||||
auto appendCategory = [&](auto predicate, UIEditorPanelHostLifecycleEventKind kind) {
|
||||
for (const UIEditorPanelHostState& current : currentStates) {
|
||||
const UIEditorPanelHostState emptyState = {};
|
||||
const UIEditorPanelHostState& previous =
|
||||
previousStateMap.contains(current.panelId)
|
||||
? *previousStateMap.at(current.panelId)
|
||||
: emptyState;
|
||||
if (predicate(previous, current)) {
|
||||
events.push_back({ kind, current.panelId });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
appendCategory(
|
||||
[](const UIEditorPanelHostState& previous, const UIEditorPanelHostState& current) {
|
||||
return !previous.attached && current.attached;
|
||||
},
|
||||
UIEditorPanelHostLifecycleEventKind::Attached);
|
||||
appendCategory(
|
||||
[](const UIEditorPanelHostState& previous, const UIEditorPanelHostState& current) {
|
||||
return !previous.visible && current.visible;
|
||||
},
|
||||
UIEditorPanelHostLifecycleEventKind::Shown);
|
||||
appendCategory(
|
||||
[](const UIEditorPanelHostState& previous, const UIEditorPanelHostState& current) {
|
||||
return !previous.active && current.active;
|
||||
},
|
||||
UIEditorPanelHostLifecycleEventKind::Activated);
|
||||
appendCategory(
|
||||
[](const UIEditorPanelHostState& previous, const UIEditorPanelHostState& current) {
|
||||
return !previous.focused && current.focused;
|
||||
},
|
||||
UIEditorPanelHostLifecycleEventKind::FocusGained);
|
||||
}
|
||||
|
||||
std::vector<UIEditorPanelHostState> BuildResolvedPanelHostStates(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorPanelHostLifecycleRequest& request) {
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
std::unordered_set<std::string_view> visiblePanelIds = {};
|
||||
visiblePanelIds.reserve(visiblePanels.size());
|
||||
for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) {
|
||||
visiblePanelIds.insert(panel.panelId);
|
||||
}
|
||||
|
||||
std::vector<UIEditorPanelHostState> states = {};
|
||||
states.reserve(panelRegistry.panels.size());
|
||||
for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) {
|
||||
if (!ContainsUIEditorWorkspacePanel(workspace, descriptor.panelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorPanelSessionState* sessionState =
|
||||
FindUIEditorPanelSessionState(session, descriptor.panelId);
|
||||
if (sessionState == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorPanelHostState state = {};
|
||||
state.panelId = descriptor.panelId;
|
||||
state.attached = sessionState->open;
|
||||
state.visible = state.attached && visiblePanelIds.contains(state.panelId);
|
||||
state.active = state.visible && workspace.activePanelId == state.panelId;
|
||||
state.focused = state.active && request.focusedPanelId == state.panelId;
|
||||
states.push_back(std::move(state));
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorPanelHostLifecycleEventKindName(
|
||||
UIEditorPanelHostLifecycleEventKind kind) {
|
||||
switch (kind) {
|
||||
case UIEditorPanelHostLifecycleEventKind::Attached:
|
||||
return "Attached";
|
||||
case UIEditorPanelHostLifecycleEventKind::Detached:
|
||||
return "Detached";
|
||||
case UIEditorPanelHostLifecycleEventKind::Shown:
|
||||
return "Shown";
|
||||
case UIEditorPanelHostLifecycleEventKind::Hidden:
|
||||
return "Hidden";
|
||||
case UIEditorPanelHostLifecycleEventKind::Activated:
|
||||
return "Activated";
|
||||
case UIEditorPanelHostLifecycleEventKind::Deactivated:
|
||||
return "Deactivated";
|
||||
case UIEditorPanelHostLifecycleEventKind::FocusGained:
|
||||
return "FocusGained";
|
||||
case UIEditorPanelHostLifecycleEventKind::FocusLost:
|
||||
return "FocusLost";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
const UIEditorPanelHostState* FindUIEditorPanelHostState(
|
||||
const UIEditorPanelHostLifecycleState& state,
|
||||
std::string_view panelId) {
|
||||
return FindHostState(state.panelStates, panelId);
|
||||
}
|
||||
|
||||
const UIEditorPanelHostState* FindUIEditorPanelHostState(
|
||||
const UIEditorPanelHostLifecycleFrame& frame,
|
||||
std::string_view panelId) {
|
||||
return FindHostState(frame.panelStates, panelId);
|
||||
}
|
||||
|
||||
UIEditorPanelHostLifecycleFrame UpdateUIEditorPanelHostLifecycle(
|
||||
UIEditorPanelHostLifecycleState& state,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorPanelHostLifecycleRequest& request) {
|
||||
UIEditorPanelHostLifecycleFrame frame = {};
|
||||
frame.panelStates =
|
||||
BuildResolvedPanelHostStates(panelRegistry, workspace, session, request);
|
||||
|
||||
const HostStateMap previousStateMap = BuildHostStateMap(state.panelStates);
|
||||
const HostStateMap currentStateMap = BuildHostStateMap(frame.panelStates);
|
||||
AppendLeaveEvents(frame.events, state.panelStates, currentStateMap);
|
||||
AppendEnterEvents(frame.events, frame.panelStates, previousStateMap);
|
||||
|
||||
state.panelStates = frame.panelStates;
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
74
new_editor/src/Shell/UIEditorPanelRegistry.cpp
Normal file
74
new_editor/src/Shell/UIEditorPanelRegistry.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
UIEditorPanelRegistryValidationResult MakeValidationError(
|
||||
UIEditorPanelRegistryValidationCode code,
|
||||
std::string message) {
|
||||
UIEditorPanelRegistryValidationResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorPanelRegistry BuildDefaultEditorShellPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{
|
||||
"editor-foundation-root",
|
||||
"Root Surface",
|
||||
UIEditorPanelPresentationKind::Placeholder,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* FindUIEditorPanelDescriptor(
|
||||
const UIEditorPanelRegistry& registry,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorPanelDescriptor& descriptor : registry.panels) {
|
||||
if (descriptor.panelId == panelId) {
|
||||
return &descriptor;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorPanelRegistryValidationResult ValidateUIEditorPanelRegistry(
|
||||
const UIEditorPanelRegistry& registry) {
|
||||
std::unordered_set<std::string> panelIds = {};
|
||||
for (const UIEditorPanelDescriptor& descriptor : registry.panels) {
|
||||
if (descriptor.panelId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorPanelRegistryValidationCode::EmptyPanelId,
|
||||
"Panel registry entry must define a panelId.");
|
||||
}
|
||||
|
||||
if (descriptor.defaultTitle.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorPanelRegistryValidationCode::EmptyDefaultTitle,
|
||||
"Panel descriptor '" + descriptor.panelId + "' must define a defaultTitle.");
|
||||
}
|
||||
|
||||
if (!panelIds.insert(descriptor.panelId).second) {
|
||||
return MakeValidationError(
|
||||
UIEditorPanelRegistryValidationCode::DuplicatePanelId,
|
||||
"Panel descriptor '" + descriptor.panelId + "' is duplicated in the registry.");
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
218
new_editor/src/Shell/UIEditorShellCompose.cpp
Normal file
218
new_editor/src/Shell/UIEditorShellCompose.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using Widgets::AppendUIEditorMenuBarBackground;
|
||||
using Widgets::AppendUIEditorMenuBarForeground;
|
||||
using Widgets::AppendUIEditorStatusBarBackground;
|
||||
using Widgets::AppendUIEditorStatusBarForeground;
|
||||
using Widgets::BuildUIEditorMenuBarLayout;
|
||||
using Widgets::BuildUIEditorStatusBarLayout;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorShellComposeLayout BuildUIEditorShellComposeLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems,
|
||||
const std::vector<Widgets::UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorShellComposeMetrics& metrics) {
|
||||
UIEditorShellComposeLayout layout = {};
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
layout.contentRect = InsetRect(layout.bounds, metrics.outerPadding);
|
||||
|
||||
const float menuBarHeight = ClampNonNegative(metrics.menuBarMetrics.barHeight);
|
||||
const float statusBarHeight = ClampNonNegative(metrics.statusBarMetrics.barHeight);
|
||||
const float sectionGap = ClampNonNegative(metrics.sectionGap);
|
||||
const float availableHeight = layout.contentRect.height;
|
||||
const float combinedBars = menuBarHeight + statusBarHeight;
|
||||
const float gapBudget = combinedBars > 0.0f ? sectionGap * 2.0f : 0.0f;
|
||||
const float clampedGapBudget = (std::min)(gapBudget, availableHeight);
|
||||
const float workspaceHeight =
|
||||
(std::max)(0.0f, availableHeight - combinedBars - clampedGapBudget);
|
||||
|
||||
float cursorY = layout.contentRect.y;
|
||||
if (menuBarHeight > 0.0f) {
|
||||
layout.menuBarRect = UIRect(
|
||||
layout.contentRect.x,
|
||||
cursorY,
|
||||
layout.contentRect.width,
|
||||
menuBarHeight);
|
||||
cursorY += menuBarHeight + sectionGap;
|
||||
}
|
||||
|
||||
layout.workspaceRect = UIRect(
|
||||
layout.contentRect.x,
|
||||
cursorY,
|
||||
layout.contentRect.width,
|
||||
workspaceHeight);
|
||||
|
||||
if (statusBarHeight > 0.0f) {
|
||||
layout.statusBarRect = UIRect(
|
||||
layout.contentRect.x,
|
||||
layout.contentRect.y + layout.contentRect.height - statusBarHeight,
|
||||
layout.contentRect.width,
|
||||
statusBarHeight);
|
||||
}
|
||||
|
||||
layout.menuBarLayout =
|
||||
BuildUIEditorMenuBarLayout(layout.menuBarRect, menuBarItems, metrics.menuBarMetrics);
|
||||
layout.statusBarLayout =
|
||||
BuildUIEditorStatusBarLayout(layout.statusBarRect, statusSegments, metrics.statusBarMetrics);
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorShellComposeRequest ResolveUIEditorShellComposeRequest(
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorShellComposeModel& model,
|
||||
const Widgets::UIEditorDockHostState& dockHostState,
|
||||
const UIEditorShellComposeState& state,
|
||||
const UIEditorShellComposeMetrics& metrics) {
|
||||
UIEditorShellComposeRequest request = {};
|
||||
request.layout = BuildUIEditorShellComposeLayout(
|
||||
bounds,
|
||||
model.menuBarItems,
|
||||
model.statusSegments,
|
||||
metrics);
|
||||
request.workspaceRequest = ResolveUIEditorWorkspaceComposeRequest(
|
||||
request.layout.workspaceRect,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
model.workspacePresentations,
|
||||
dockHostState,
|
||||
metrics.dockHostMetrics,
|
||||
metrics.viewportMetrics);
|
||||
request.layout.menuBarLayout = BuildUIEditorMenuBarLayout(
|
||||
request.layout.menuBarRect,
|
||||
model.menuBarItems,
|
||||
metrics.menuBarMetrics);
|
||||
request.layout.statusBarLayout = BuildUIEditorStatusBarLayout(
|
||||
request.layout.statusBarRect,
|
||||
model.statusSegments,
|
||||
metrics.statusBarMetrics);
|
||||
(void)state;
|
||||
return request;
|
||||
}
|
||||
|
||||
UIEditorShellComposeFrame UpdateUIEditorShellCompose(
|
||||
UIEditorShellComposeState& state,
|
||||
const UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const UIEditorShellComposeModel& model,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorDockHostState& dockHostState,
|
||||
const UIEditorShellComposeMetrics& metrics) {
|
||||
UIEditorShellComposeFrame frame = {};
|
||||
frame.layout = BuildUIEditorShellComposeLayout(
|
||||
bounds,
|
||||
model.menuBarItems,
|
||||
model.statusSegments,
|
||||
metrics);
|
||||
frame.workspaceFrame = UpdateUIEditorWorkspaceCompose(
|
||||
state.workspaceState,
|
||||
frame.layout.workspaceRect,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
model.workspacePresentations,
|
||||
inputEvents,
|
||||
dockHostState,
|
||||
metrics.dockHostMetrics,
|
||||
metrics.viewportMetrics);
|
||||
frame.layout.menuBarLayout = BuildUIEditorMenuBarLayout(
|
||||
frame.layout.menuBarRect,
|
||||
model.menuBarItems,
|
||||
metrics.menuBarMetrics);
|
||||
frame.layout.statusBarLayout = BuildUIEditorStatusBarLayout(
|
||||
frame.layout.statusBarRect,
|
||||
model.statusSegments,
|
||||
metrics.statusBarMetrics);
|
||||
return frame;
|
||||
}
|
||||
|
||||
void AppendUIEditorShellCompose(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorShellComposeFrame& frame,
|
||||
const UIEditorShellComposeModel& model,
|
||||
const UIEditorShellComposeState& state,
|
||||
const UIEditorShellComposePalette& palette,
|
||||
const UIEditorShellComposeMetrics& metrics) {
|
||||
drawList.AddFilledRect(
|
||||
frame.layout.bounds,
|
||||
palette.surfaceColor,
|
||||
metrics.surfaceCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
frame.layout.bounds,
|
||||
palette.surfaceBorderColor,
|
||||
1.0f,
|
||||
metrics.surfaceCornerRounding);
|
||||
|
||||
AppendUIEditorMenuBarBackground(
|
||||
drawList,
|
||||
frame.layout.menuBarLayout,
|
||||
model.menuBarItems,
|
||||
state.menuBarState,
|
||||
palette.menuBarPalette,
|
||||
metrics.menuBarMetrics);
|
||||
AppendUIEditorMenuBarForeground(
|
||||
drawList,
|
||||
frame.layout.menuBarLayout,
|
||||
model.menuBarItems,
|
||||
state.menuBarState,
|
||||
palette.menuBarPalette,
|
||||
metrics.menuBarMetrics);
|
||||
|
||||
AppendUIEditorWorkspaceCompose(
|
||||
drawList,
|
||||
frame.workspaceFrame,
|
||||
palette.dockHostPalette,
|
||||
metrics.dockHostMetrics,
|
||||
palette.viewportPalette,
|
||||
metrics.viewportMetrics);
|
||||
|
||||
AppendUIEditorStatusBarBackground(
|
||||
drawList,
|
||||
frame.layout.statusBarLayout,
|
||||
model.statusSegments,
|
||||
state.statusBarState,
|
||||
palette.statusBarPalette,
|
||||
metrics.statusBarMetrics);
|
||||
AppendUIEditorStatusBarForeground(
|
||||
drawList,
|
||||
frame.layout.statusBarLayout,
|
||||
model.statusSegments,
|
||||
state.statusBarState,
|
||||
palette.statusBarPalette,
|
||||
metrics.statusBarMetrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
871
new_editor/src/Shell/UIEditorShellInteraction.cpp
Normal file
871
new_editor/src/Shell/UIEditorShellInteraction.cpp
Normal file
@@ -0,0 +1,871 @@
|
||||
#include <XCEditor/Shell/UIEditorShellInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIElementId;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIInputPath;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::UISize;
|
||||
using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect;
|
||||
using ::XCEngine::UI::Widgets::UIPopupOverlayEntry;
|
||||
using ::XCEngine::UI::Widgets::UIPopupPlacement;
|
||||
using Widgets::AppendUIEditorMenuPopupBackground;
|
||||
using Widgets::AppendUIEditorMenuPopupForeground;
|
||||
using Widgets::BuildUIEditorMenuBarLayout;
|
||||
using Widgets::BuildUIEditorMenuPopupLayout;
|
||||
using Widgets::HitTestUIEditorMenuBar;
|
||||
using Widgets::HitTestUIEditorMenuPopup;
|
||||
using Widgets::MeasureUIEditorMenuPopupHeight;
|
||||
using Widgets::ResolveUIEditorMenuPopupDesiredWidth;
|
||||
using Widgets::UIEditorMenuBarHitTargetKind;
|
||||
using Widgets::UIEditorMenuBarInvalidIndex;
|
||||
using Widgets::UIEditorMenuPopupHitTargetKind;
|
||||
using Widgets::UIEditorMenuPopupInvalidIndex;
|
||||
|
||||
constexpr UIElementId kShellPathRoot = 0x1E1001ull;
|
||||
constexpr UIElementId kMenuBarPathRoot = 0x1E1002ull;
|
||||
constexpr UIElementId kPopupPathRoot = 0x1E1003ull;
|
||||
constexpr UIElementId kMenuItemPathRoot = 0x1E1004ull;
|
||||
constexpr UIElementId kOutsidePointerPath = 0x1E10FFull;
|
||||
|
||||
struct RequestHit {
|
||||
const UIEditorShellInteractionMenuButtonRequest* menuButton = nullptr;
|
||||
const UIEditorShellInteractionPopupRequest* popupRequest = nullptr;
|
||||
const UIEditorShellInteractionPopupItemRequest* popupItem = nullptr;
|
||||
};
|
||||
|
||||
struct BuildRequestOutput {
|
||||
UIEditorShellInteractionRequest request = {};
|
||||
bool hadInvalidPopupState = false;
|
||||
};
|
||||
|
||||
UIElementId HashText(std::string_view text) {
|
||||
std::uint64_t hash = 1469598103934665603ull;
|
||||
for (const unsigned char value : text) {
|
||||
hash ^= value;
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
|
||||
hash &= 0x7FFFFFFFFFFFFFFFull;
|
||||
return hash == 0u ? 1u : hash;
|
||||
}
|
||||
|
||||
std::string BuildRootPopupId(std::string_view menuId) {
|
||||
return "editor.menu.root." + std::string(menuId);
|
||||
}
|
||||
|
||||
std::string BuildSubmenuPopupId(std::string_view popupId, std::string_view itemId) {
|
||||
return std::string(popupId) + ".child." + std::string(itemId);
|
||||
}
|
||||
|
||||
UIInputPath BuildMenuButtonPath(std::string_view menuId) {
|
||||
return UIInputPath { kShellPathRoot, kMenuBarPathRoot, HashText(menuId) };
|
||||
}
|
||||
|
||||
UIInputPath BuildPopupSurfacePath(std::string_view popupId) {
|
||||
return UIInputPath { kShellPathRoot, kPopupPathRoot, HashText(popupId) };
|
||||
}
|
||||
|
||||
UIInputPath BuildMenuItemPath(std::string_view popupId, std::string_view itemId) {
|
||||
return UIInputPath {
|
||||
kShellPathRoot,
|
||||
kPopupPathRoot,
|
||||
HashText(popupId),
|
||||
kMenuItemPathRoot,
|
||||
HashText(itemId)
|
||||
};
|
||||
}
|
||||
|
||||
const UIEditorResolvedMenuDescriptor* FindResolvedMenu(
|
||||
const UIEditorResolvedMenuModel& model,
|
||||
std::string_view menuId) {
|
||||
for (const UIEditorResolvedMenuDescriptor& menu : model.menus) {
|
||||
if (menu.menuId == menuId) {
|
||||
return &menu;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorResolvedMenuItem* FindResolvedMenuItemRecursive(
|
||||
const std::vector<UIEditorResolvedMenuItem>& items,
|
||||
std::string_view itemId) {
|
||||
for (const UIEditorResolvedMenuItem& item : items) {
|
||||
if (item.itemId == itemId) {
|
||||
return &item;
|
||||
}
|
||||
|
||||
if (!item.children.empty()) {
|
||||
if (const UIEditorResolvedMenuItem* child =
|
||||
FindResolvedMenuItemRecursive(item.children, itemId);
|
||||
child != nullptr) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(
|
||||
const UIEditorResolvedMenuModel& model,
|
||||
const UIEditorMenuPopupState& popupState) {
|
||||
const UIEditorResolvedMenuDescriptor* menu =
|
||||
FindResolvedMenu(model, popupState.menuId);
|
||||
if (menu == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (popupState.itemId.empty()) {
|
||||
return &menu->items;
|
||||
}
|
||||
|
||||
const UIEditorResolvedMenuItem* item =
|
||||
FindResolvedMenuItemRecursive(menu->items, popupState.itemId);
|
||||
if (item == nullptr ||
|
||||
item->kind != UIEditorMenuItemKind::Submenu ||
|
||||
!item->enabled ||
|
||||
item->children.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &item->children;
|
||||
}
|
||||
|
||||
std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
|
||||
const UIEditorResolvedMenuModel& model) {
|
||||
std::vector<Widgets::UIEditorMenuBarItem> items = {};
|
||||
items.reserve(model.menus.size());
|
||||
|
||||
for (const UIEditorResolvedMenuDescriptor& menu : model.menus) {
|
||||
Widgets::UIEditorMenuBarItem item = {};
|
||||
item.menuId = menu.menuId;
|
||||
item.label = menu.label;
|
||||
item.enabled = !menu.items.empty();
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
UIEditorShellComposeModel BuildShellComposeModel(
|
||||
const UIEditorShellInteractionModel& model,
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems) {
|
||||
UIEditorShellComposeModel shellModel = {};
|
||||
shellModel.menuBarItems = menuBarItems;
|
||||
shellModel.statusSegments = model.statusSegments;
|
||||
shellModel.workspacePresentations = model.workspacePresentations;
|
||||
return shellModel;
|
||||
}
|
||||
|
||||
std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
|
||||
const std::vector<UIEditorResolvedMenuItem>& items) {
|
||||
std::vector<Widgets::UIEditorMenuPopupItem> widgetItems = {};
|
||||
widgetItems.reserve(items.size());
|
||||
|
||||
for (const UIEditorResolvedMenuItem& item : items) {
|
||||
Widgets::UIEditorMenuPopupItem widgetItem = {};
|
||||
widgetItem.itemId = item.itemId;
|
||||
widgetItem.kind = item.kind;
|
||||
widgetItem.label = item.label;
|
||||
widgetItem.shortcutText = item.shortcutText;
|
||||
widgetItem.enabled = item.enabled;
|
||||
widgetItem.checked = item.checked;
|
||||
widgetItem.hasSubmenu = item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty();
|
||||
widgetItems.push_back(std::move(widgetItem));
|
||||
}
|
||||
|
||||
return widgetItems;
|
||||
}
|
||||
|
||||
std::size_t FindMenuBarIndex(
|
||||
const std::vector<Widgets::UIEditorMenuBarItem>& items,
|
||||
std::string_view menuId) {
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
if (items[index].menuId == menuId) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return UIEditorMenuBarInvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t FindPopupItemIndex(
|
||||
const std::vector<UIEditorShellInteractionPopupItemRequest>& items,
|
||||
std::string_view itemId) {
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
if (items[index].itemId == itemId) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return UIEditorMenuPopupInvalidIndex;
|
||||
}
|
||||
|
||||
bool HasMeaningfulInteractionResult(
|
||||
const UIEditorShellInteractionResult& result) {
|
||||
return result.consumed ||
|
||||
result.requestPointerCapture ||
|
||||
result.releasePointerCapture ||
|
||||
result.commandTriggered ||
|
||||
result.commandDispatched ||
|
||||
result.menuMutation.changed ||
|
||||
result.workspaceResult.consumed ||
|
||||
!result.menuId.empty() ||
|
||||
!result.popupId.empty() ||
|
||||
!result.itemId.empty() ||
|
||||
!result.commandId.empty();
|
||||
}
|
||||
|
||||
bool ShouldRefreshResolvedShellModel(
|
||||
const UIEditorShellInteractionResult& result) {
|
||||
return result.commandDispatched ||
|
||||
result.workspaceResult.dockHostResult.commandExecuted;
|
||||
}
|
||||
|
||||
BuildRequestOutput BuildRequest(
|
||||
const UIRect& bounds,
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShellInteractionModel& model,
|
||||
const UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionMetrics& metrics) {
|
||||
BuildRequestOutput output = {};
|
||||
UIEditorShellInteractionRequest& request = output.request;
|
||||
request.menuBarItems = BuildMenuBarItems(model.resolvedMenuModel);
|
||||
|
||||
const UIEditorShellComposeModel shellModel =
|
||||
BuildShellComposeModel(model, request.menuBarItems);
|
||||
request.shellRequest = ResolveUIEditorShellComposeRequest(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
shellModel,
|
||||
state.workspaceInteractionState.dockHostInteractionState.dockHostState,
|
||||
state.composeState,
|
||||
metrics.shellMetrics);
|
||||
|
||||
request.menuButtons.reserve(request.menuBarItems.size());
|
||||
for (std::size_t index = 0; index < request.menuBarItems.size(); ++index) {
|
||||
UIEditorShellInteractionMenuButtonRequest button = {};
|
||||
button.menuId = request.menuBarItems[index].menuId;
|
||||
button.label = request.menuBarItems[index].label;
|
||||
button.popupId = BuildRootPopupId(button.menuId);
|
||||
button.enabled = request.menuBarItems[index].enabled;
|
||||
if (index < request.shellRequest.layout.menuBarLayout.buttonRects.size()) {
|
||||
button.rect = request.shellRequest.layout.menuBarLayout.buttonRects[index];
|
||||
}
|
||||
button.path = BuildMenuButtonPath(button.menuId);
|
||||
request.menuButtons.push_back(std::move(button));
|
||||
}
|
||||
|
||||
const auto& popupStates = state.menuSession.GetPopupStates();
|
||||
request.popupRequests.reserve(popupStates.size());
|
||||
for (const UIEditorMenuPopupState& popupState : popupStates) {
|
||||
const UIPopupOverlayEntry* overlayEntry =
|
||||
state.menuSession.GetPopupOverlayModel().FindPopup(popupState.popupId);
|
||||
const std::vector<UIEditorResolvedMenuItem>* resolvedItems =
|
||||
ResolvePopupItems(model.resolvedMenuModel, popupState);
|
||||
if (overlayEntry == nullptr || resolvedItems == nullptr) {
|
||||
output.hadInvalidPopupState = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionPopupRequest popupRequest = {};
|
||||
popupRequest.popupId = popupState.popupId;
|
||||
popupRequest.menuId = popupState.menuId;
|
||||
popupRequest.sourceItemId = popupState.itemId;
|
||||
popupRequest.overlayEntry = *overlayEntry;
|
||||
popupRequest.resolvedItems = *resolvedItems;
|
||||
popupRequest.widgetItems = BuildPopupWidgetItems(popupRequest.resolvedItems);
|
||||
|
||||
const float popupWidth =
|
||||
ResolveUIEditorMenuPopupDesiredWidth(popupRequest.widgetItems, metrics.popupMetrics);
|
||||
const float popupHeight =
|
||||
MeasureUIEditorMenuPopupHeight(popupRequest.widgetItems, metrics.popupMetrics);
|
||||
popupRequest.placement = ResolvePopupPlacementRect(
|
||||
overlayEntry->anchorRect,
|
||||
UISize(popupWidth, popupHeight),
|
||||
request.shellRequest.layout.bounds,
|
||||
overlayEntry->placement);
|
||||
popupRequest.layout =
|
||||
BuildUIEditorMenuPopupLayout(popupRequest.placement.rect, popupRequest.widgetItems, metrics.popupMetrics);
|
||||
|
||||
popupRequest.itemRequests.reserve(popupRequest.resolvedItems.size());
|
||||
for (std::size_t index = 0; index < popupRequest.resolvedItems.size(); ++index) {
|
||||
const UIEditorResolvedMenuItem& resolvedItem = popupRequest.resolvedItems[index];
|
||||
UIEditorShellInteractionPopupItemRequest itemRequest = {};
|
||||
itemRequest.popupId = popupRequest.popupId;
|
||||
itemRequest.menuId = popupRequest.menuId;
|
||||
itemRequest.itemId = resolvedItem.itemId;
|
||||
itemRequest.label = resolvedItem.label;
|
||||
itemRequest.commandId = resolvedItem.commandId;
|
||||
itemRequest.kind = resolvedItem.kind;
|
||||
itemRequest.enabled = resolvedItem.enabled;
|
||||
itemRequest.checked = resolvedItem.checked;
|
||||
itemRequest.hasSubmenu =
|
||||
resolvedItem.kind == UIEditorMenuItemKind::Submenu &&
|
||||
!resolvedItem.children.empty();
|
||||
if (itemRequest.hasSubmenu) {
|
||||
itemRequest.childPopupId =
|
||||
BuildSubmenuPopupId(popupRequest.popupId, itemRequest.itemId);
|
||||
}
|
||||
if (index < popupRequest.layout.itemRects.size()) {
|
||||
itemRequest.rect = popupRequest.layout.itemRects[index];
|
||||
}
|
||||
itemRequest.path = BuildMenuItemPath(popupRequest.popupId, itemRequest.itemId);
|
||||
popupRequest.itemRequests.push_back(std::move(itemRequest));
|
||||
}
|
||||
|
||||
request.popupRequests.push_back(std::move(popupRequest));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
RequestHit HitTestRequest(
|
||||
const UIEditorShellInteractionRequest& request,
|
||||
const UIPoint& point,
|
||||
bool hasPointerPosition) {
|
||||
RequestHit hit = {};
|
||||
if (!hasPointerPosition) {
|
||||
return hit;
|
||||
}
|
||||
|
||||
for (std::size_t index = request.popupRequests.size(); index > 0u; --index) {
|
||||
const UIEditorShellInteractionPopupRequest& popupRequest =
|
||||
request.popupRequests[index - 1u];
|
||||
const auto popupHit =
|
||||
HitTestUIEditorMenuPopup(popupRequest.layout, popupRequest.widgetItems, point);
|
||||
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item &&
|
||||
popupHit.index < popupRequest.itemRequests.size()) {
|
||||
hit.popupRequest = &popupRequest;
|
||||
hit.popupItem = &popupRequest.itemRequests[popupHit.index];
|
||||
return hit;
|
||||
}
|
||||
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) {
|
||||
hit.popupRequest = &popupRequest;
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
|
||||
const auto menuHit =
|
||||
HitTestUIEditorMenuBar(request.shellRequest.layout.menuBarLayout, point);
|
||||
if (menuHit.kind == UIEditorMenuBarHitTargetKind::Button &&
|
||||
menuHit.index < request.menuButtons.size()) {
|
||||
hit.menuButton = &request.menuButtons[menuHit.index];
|
||||
}
|
||||
|
||||
return hit;
|
||||
}
|
||||
|
||||
UIPopupOverlayEntry BuildRootPopupEntry(
|
||||
const UIEditorShellInteractionMenuButtonRequest& button) {
|
||||
UIPopupOverlayEntry entry = {};
|
||||
entry.popupId = button.popupId;
|
||||
entry.anchorRect = button.rect;
|
||||
entry.anchorPath = button.path;
|
||||
entry.surfacePath = BuildPopupSurfacePath(button.popupId);
|
||||
entry.placement = UIPopupPlacement::BottomStart;
|
||||
return entry;
|
||||
}
|
||||
|
||||
UIPopupOverlayEntry BuildSubmenuPopupEntry(
|
||||
const UIEditorShellInteractionPopupItemRequest& item) {
|
||||
UIPopupOverlayEntry entry = {};
|
||||
entry.popupId = item.childPopupId;
|
||||
entry.parentPopupId = item.popupId;
|
||||
entry.anchorRect = item.rect;
|
||||
entry.anchorPath = item.path;
|
||||
entry.surfacePath = BuildPopupSurfacePath(item.childPopupId);
|
||||
entry.placement = UIPopupPlacement::RightStart;
|
||||
return entry;
|
||||
}
|
||||
|
||||
void UpdateMenuBarVisualState(
|
||||
UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionRequest& request,
|
||||
const RequestHit& hit) {
|
||||
state.composeState.menuBarState.openIndex = FindMenuBarIndex(
|
||||
request.menuBarItems,
|
||||
state.menuSession.GetOpenRootMenuId());
|
||||
state.composeState.menuBarState.hoveredIndex =
|
||||
hit.menuButton != nullptr
|
||||
? FindMenuBarIndex(request.menuBarItems, hit.menuButton->menuId)
|
||||
: UIEditorMenuBarInvalidIndex;
|
||||
state.composeState.menuBarState.focused =
|
||||
state.focused || state.menuSession.HasOpenMenu();
|
||||
}
|
||||
|
||||
std::vector<UIEditorShellInteractionPopupFrame> BuildPopupFrames(
|
||||
const UIEditorShellInteractionRequest& request,
|
||||
const UIEditorShellInteractionState& state,
|
||||
std::string_view hoveredPopupId,
|
||||
std::string_view hoveredItemId) {
|
||||
std::vector<UIEditorShellInteractionPopupFrame> popupFrames = {};
|
||||
popupFrames.reserve(request.popupRequests.size());
|
||||
|
||||
for (std::size_t index = 0; index < request.popupRequests.size(); ++index) {
|
||||
const UIEditorShellInteractionPopupRequest& popupRequest =
|
||||
request.popupRequests[index];
|
||||
UIEditorShellInteractionPopupFrame popupFrame = {};
|
||||
popupFrame.popupId = popupRequest.popupId;
|
||||
popupFrame.popupState.focused = state.focused || state.menuSession.HasOpenMenu();
|
||||
popupFrame.popupState.hoveredIndex =
|
||||
popupRequest.popupId == hoveredPopupId
|
||||
? FindPopupItemIndex(popupRequest.itemRequests, hoveredItemId)
|
||||
: UIEditorMenuPopupInvalidIndex;
|
||||
if (index + 1u < request.popupRequests.size()) {
|
||||
popupFrame.popupState.submenuOpenIndex = FindPopupItemIndex(
|
||||
popupRequest.itemRequests,
|
||||
request.popupRequests[index + 1u].sourceItemId);
|
||||
}
|
||||
popupFrames.push_back(std::move(popupFrame));
|
||||
}
|
||||
|
||||
return popupFrames;
|
||||
}
|
||||
|
||||
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
case UIInputEventType::PointerWheel:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<UIInputEvent> FilterWorkspaceInputEvents(
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
bool menuModalDuringFrame) {
|
||||
if (!menuModalDuringFrame) {
|
||||
return inputEvents;
|
||||
}
|
||||
|
||||
std::vector<UIInputEvent> filtered = {};
|
||||
for (const UIInputEvent& event : inputEvents) {
|
||||
if (event.type == UIInputEventType::FocusGained ||
|
||||
event.type == UIInputEventType::FocusLost) {
|
||||
filtered.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorShellInteractionModel ResolveUIEditorShellInteractionModel(
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShellInteractionDefinition& definition,
|
||||
const UIEditorShellInteractionServices& services) {
|
||||
UIEditorShellInteractionModel model = {};
|
||||
if (services.commandDispatcher != nullptr) {
|
||||
model.resolvedMenuModel = BuildUIEditorResolvedMenuModel(
|
||||
definition.menuModel,
|
||||
*services.commandDispatcher,
|
||||
controller,
|
||||
services.shortcutManager);
|
||||
}
|
||||
model.statusSegments = definition.statusSegments;
|
||||
model.workspacePresentations = definition.workspacePresentations;
|
||||
return model;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest(
|
||||
const UIRect& bounds,
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShellInteractionModel& model,
|
||||
const UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionMetrics& metrics,
|
||||
const UIEditorShellInteractionServices& services) {
|
||||
(void)services;
|
||||
return BuildRequest(
|
||||
bounds,
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
UIEditorShellInteractionState& state,
|
||||
UIEditorWorkspaceController& controller,
|
||||
const UIRect& bounds,
|
||||
const UIEditorShellInteractionModel& model,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const UIEditorShellInteractionServices& services,
|
||||
const UIEditorShellInteractionMetrics& metrics) {
|
||||
UIEditorShellInteractionResult interactionResult = {};
|
||||
bool menuModalDuringFrame = state.menuSession.HasOpenMenu();
|
||||
|
||||
BuildRequestOutput requestBuild = BuildRequest(
|
||||
bounds,
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics);
|
||||
UIEditorShellInteractionRequest request = std::move(requestBuild.request);
|
||||
|
||||
if (requestBuild.hadInvalidPopupState && state.menuSession.HasOpenMenu()) {
|
||||
interactionResult.menuMutation = state.menuSession.CloseAll();
|
||||
interactionResult.consumed = interactionResult.menuMutation.changed;
|
||||
menuModalDuringFrame =
|
||||
interactionResult.menuMutation.changed || state.menuSession.HasOpenMenu();
|
||||
|
||||
requestBuild = BuildRequest(
|
||||
bounds,
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics);
|
||||
request = std::move(requestBuild.request);
|
||||
}
|
||||
|
||||
for (const UIInputEvent& event : inputEvents) {
|
||||
UIEditorShellInteractionResult eventResult = {};
|
||||
|
||||
if (ShouldUsePointerPosition(event)) {
|
||||
state.pointerPosition = event.position;
|
||||
state.hasPointerPosition = true;
|
||||
} else if (event.type == UIInputEventType::PointerLeave) {
|
||||
state.hasPointerPosition = false;
|
||||
}
|
||||
|
||||
const RequestHit hit =
|
||||
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
|
||||
|
||||
switch (event.type) {
|
||||
case UIInputEventType::FocusGained:
|
||||
state.focused = true;
|
||||
break;
|
||||
|
||||
case UIInputEventType::FocusLost:
|
||||
state.focused = false;
|
||||
if (state.menuSession.HasOpenMenu()) {
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.DismissFromFocusLoss({});
|
||||
eventResult.consumed = eventResult.menuMutation.changed;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
if (state.menuSession.HasOpenMenu()) {
|
||||
if (hit.menuButton != nullptr && hit.menuButton->enabled) {
|
||||
eventResult.menuId = hit.menuButton->menuId;
|
||||
if (!state.menuSession.IsMenuOpen(hit.menuButton->menuId)) {
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.HoverMenuBarRoot(
|
||||
hit.menuButton->menuId,
|
||||
BuildRootPopupEntry(*hit.menuButton));
|
||||
} else {
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.DismissFromFocusLoss(hit.menuButton->path);
|
||||
}
|
||||
} else if (hit.popupItem != nullptr) {
|
||||
eventResult.menuId = hit.popupItem->menuId;
|
||||
eventResult.popupId = hit.popupItem->popupId;
|
||||
eventResult.itemId = hit.popupItem->itemId;
|
||||
if (hit.popupItem->hasSubmenu && hit.popupItem->enabled) {
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.HoverSubmenu(
|
||||
hit.popupItem->itemId,
|
||||
BuildSubmenuPopupEntry(*hit.popupItem));
|
||||
} else {
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.DismissFromFocusLoss(hit.popupItem->path);
|
||||
}
|
||||
} else if (hit.popupRequest != nullptr) {
|
||||
eventResult.menuId = hit.popupRequest->menuId;
|
||||
eventResult.popupId = hit.popupRequest->popupId;
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.DismissFromFocusLoss(
|
||||
hit.popupRequest->overlayEntry.surfacePath);
|
||||
}
|
||||
|
||||
if (eventResult.menuMutation.changed) {
|
||||
eventResult.consumed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
if (event.pointerButton != ::XCEngine::UI::UIPointerButton::Left) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (hit.menuButton != nullptr && hit.menuButton->enabled) {
|
||||
state.focused = true;
|
||||
eventResult.consumed = true;
|
||||
eventResult.menuId = hit.menuButton->menuId;
|
||||
if (state.menuSession.IsMenuOpen(hit.menuButton->menuId)) {
|
||||
eventResult.menuMutation = state.menuSession.CloseAll();
|
||||
} else {
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.OpenMenuBarRoot(
|
||||
hit.menuButton->menuId,
|
||||
BuildRootPopupEntry(*hit.menuButton));
|
||||
}
|
||||
} else if (hit.popupItem != nullptr) {
|
||||
state.focused = true;
|
||||
eventResult.consumed = true;
|
||||
eventResult.menuId = hit.popupItem->menuId;
|
||||
eventResult.popupId = hit.popupItem->popupId;
|
||||
eventResult.itemId = hit.popupItem->itemId;
|
||||
if (hit.popupItem->hasSubmenu && hit.popupItem->enabled) {
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.HoverSubmenu(
|
||||
hit.popupItem->itemId,
|
||||
BuildSubmenuPopupEntry(*hit.popupItem));
|
||||
} else if (hit.popupItem->enabled) {
|
||||
eventResult.commandTriggered = true;
|
||||
eventResult.commandId = hit.popupItem->commandId;
|
||||
if (services.commandDispatcher != nullptr) {
|
||||
eventResult.commandDispatchResult =
|
||||
services.commandDispatcher->Dispatch(
|
||||
eventResult.commandId,
|
||||
controller);
|
||||
eventResult.commandDispatched = true;
|
||||
}
|
||||
eventResult.menuMutation = state.menuSession.CloseAll();
|
||||
} else {
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.DismissFromPointerDown(hit.popupItem->path);
|
||||
}
|
||||
} else if (hit.popupRequest != nullptr) {
|
||||
eventResult.consumed = true;
|
||||
eventResult.menuId = hit.popupRequest->menuId;
|
||||
eventResult.popupId = hit.popupRequest->popupId;
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.DismissFromPointerDown(
|
||||
hit.popupRequest->overlayEntry.surfacePath);
|
||||
} else if (state.menuSession.HasOpenMenu()) {
|
||||
eventResult.consumed = true;
|
||||
eventResult.menuMutation =
|
||||
state.menuSession.DismissFromPointerDown(UIInputPath { kOutsidePointerPath });
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::KeyDown:
|
||||
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Escape) &&
|
||||
state.menuSession.HasOpenMenu()) {
|
||||
eventResult.consumed = true;
|
||||
eventResult.menuMutation = state.menuSession.DismissFromEscape();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (HasMeaningfulInteractionResult(eventResult)) {
|
||||
interactionResult = std::move(eventResult);
|
||||
}
|
||||
|
||||
if (interactionResult.menuMutation.changed || state.menuSession.HasOpenMenu()) {
|
||||
menuModalDuringFrame = true;
|
||||
request = BuildRequest(
|
||||
bounds,
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<UIInputEvent> workspaceInputEvents =
|
||||
FilterWorkspaceInputEvents(inputEvents, menuModalDuringFrame);
|
||||
UIEditorWorkspaceInteractionModel workspaceModel = {};
|
||||
workspaceModel.workspacePresentations = model.workspacePresentations;
|
||||
UIEditorWorkspaceInteractionFrame workspaceInteractionFrame =
|
||||
UpdateUIEditorWorkspaceInteraction(
|
||||
state.workspaceInteractionState,
|
||||
controller,
|
||||
request.shellRequest.layout.workspaceRect,
|
||||
workspaceModel,
|
||||
workspaceInputEvents,
|
||||
metrics.shellMetrics.dockHostMetrics);
|
||||
state.composeState.workspaceState = state.workspaceInteractionState.composeState;
|
||||
|
||||
request = BuildRequest(
|
||||
bounds,
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics).request;
|
||||
|
||||
const RequestHit finalHit =
|
||||
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
|
||||
UpdateMenuBarVisualState(state, request, finalHit);
|
||||
|
||||
UIEditorShellInteractionFrame frame = {};
|
||||
frame.request = request;
|
||||
frame.shellFrame.layout = request.shellRequest.layout;
|
||||
frame.shellFrame.workspaceFrame = workspaceInteractionFrame.composeFrame;
|
||||
frame.workspaceInteractionFrame = std::move(workspaceInteractionFrame);
|
||||
frame.popupFrames = BuildPopupFrames(
|
||||
frame.request,
|
||||
state,
|
||||
finalHit.popupRequest != nullptr ? finalHit.popupRequest->popupId : std::string_view(),
|
||||
finalHit.popupItem != nullptr ? finalHit.popupItem->itemId : std::string_view());
|
||||
frame.openRootMenuId = std::string(state.menuSession.GetOpenRootMenuId());
|
||||
frame.hoveredMenuId =
|
||||
finalHit.menuButton != nullptr ? finalHit.menuButton->menuId : std::string();
|
||||
frame.hoveredPopupId =
|
||||
finalHit.popupRequest != nullptr ? finalHit.popupRequest->popupId : std::string();
|
||||
frame.hoveredItemId =
|
||||
finalHit.popupItem != nullptr ? finalHit.popupItem->itemId : std::string();
|
||||
frame.focused = state.focused || state.menuSession.HasOpenMenu();
|
||||
interactionResult.workspaceResult = frame.workspaceInteractionFrame.result;
|
||||
interactionResult.menuModal = state.menuSession.HasOpenMenu();
|
||||
interactionResult.workspaceInputSuppressed = menuModalDuringFrame;
|
||||
interactionResult.requestPointerCapture =
|
||||
interactionResult.workspaceResult.requestPointerCapture;
|
||||
interactionResult.releasePointerCapture =
|
||||
interactionResult.workspaceResult.releasePointerCapture;
|
||||
interactionResult.viewportInteractionChanged =
|
||||
interactionResult.workspaceResult.viewportInteractionChanged;
|
||||
interactionResult.viewportPanelId =
|
||||
interactionResult.workspaceResult.viewportPanelId;
|
||||
interactionResult.viewportInputFrame =
|
||||
interactionResult.workspaceResult.viewportInputFrame;
|
||||
interactionResult.consumed =
|
||||
interactionResult.consumed || interactionResult.workspaceResult.consumed;
|
||||
frame.model = model;
|
||||
frame.result = std::move(interactionResult);
|
||||
return frame;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest(
|
||||
const UIRect& bounds,
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const UIEditorShellInteractionDefinition& definition,
|
||||
const UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionMetrics& metrics,
|
||||
const UIEditorShellInteractionServices& services) {
|
||||
const UIEditorShellInteractionModel model =
|
||||
ResolveUIEditorShellInteractionModel(controller, definition, services);
|
||||
return ResolveUIEditorShellInteractionRequest(
|
||||
bounds,
|
||||
controller,
|
||||
model,
|
||||
state,
|
||||
metrics,
|
||||
services);
|
||||
}
|
||||
|
||||
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
|
||||
UIEditorShellInteractionState& state,
|
||||
UIEditorWorkspaceController& controller,
|
||||
const UIRect& bounds,
|
||||
const UIEditorShellInteractionDefinition& definition,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const UIEditorShellInteractionServices& services,
|
||||
const UIEditorShellInteractionMetrics& metrics) {
|
||||
UIEditorShellInteractionModel model =
|
||||
ResolveUIEditorShellInteractionModel(controller, definition, services);
|
||||
UIEditorShellInteractionFrame frame =
|
||||
UpdateUIEditorShellInteraction(
|
||||
state,
|
||||
controller,
|
||||
bounds,
|
||||
model,
|
||||
inputEvents,
|
||||
services,
|
||||
metrics);
|
||||
if (!ShouldRefreshResolvedShellModel(frame.result)) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
UIEditorShellInteractionModel refreshedModel =
|
||||
ResolveUIEditorShellInteractionModel(controller, definition, services);
|
||||
UIEditorShellInteractionFrame refreshedFrame =
|
||||
UpdateUIEditorShellInteraction(
|
||||
state,
|
||||
controller,
|
||||
bounds,
|
||||
refreshedModel,
|
||||
{},
|
||||
services,
|
||||
metrics);
|
||||
refreshedFrame.result = frame.result;
|
||||
return refreshedFrame;
|
||||
}
|
||||
|
||||
void AppendUIEditorShellInteraction(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorShellInteractionFrame& frame,
|
||||
const UIEditorShellInteractionModel& model,
|
||||
const UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionPalette& palette,
|
||||
const UIEditorShellInteractionMetrics& metrics) {
|
||||
const UIEditorShellComposeModel shellModel =
|
||||
BuildShellComposeModel(model, frame.request.menuBarItems);
|
||||
AppendUIEditorShellCompose(
|
||||
drawList,
|
||||
frame.shellFrame,
|
||||
shellModel,
|
||||
state.composeState,
|
||||
palette.shellPalette,
|
||||
metrics.shellMetrics);
|
||||
|
||||
const std::size_t popupCount =
|
||||
(std::min)(frame.request.popupRequests.size(), frame.popupFrames.size());
|
||||
for (std::size_t index = 0; index < popupCount; ++index) {
|
||||
const UIEditorShellInteractionPopupRequest& popupRequest =
|
||||
frame.request.popupRequests[index];
|
||||
const UIEditorShellInteractionPopupFrame& popupFrame =
|
||||
frame.popupFrames[index];
|
||||
AppendUIEditorMenuPopupBackground(
|
||||
drawList,
|
||||
popupRequest.layout,
|
||||
popupRequest.widgetItems,
|
||||
popupFrame.popupState,
|
||||
palette.popupPalette,
|
||||
metrics.popupMetrics);
|
||||
AppendUIEditorMenuPopupForeground(
|
||||
drawList,
|
||||
popupRequest.layout,
|
||||
popupRequest.widgetItems,
|
||||
popupFrame.popupState,
|
||||
palette.popupPalette,
|
||||
metrics.popupMetrics);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorShellInteraction(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorShellInteractionFrame& frame,
|
||||
const UIEditorShellInteractionState& state,
|
||||
const UIEditorShellInteractionPalette& palette,
|
||||
const UIEditorShellInteractionMetrics& metrics) {
|
||||
AppendUIEditorShellInteraction(
|
||||
drawList,
|
||||
frame,
|
||||
frame.model,
|
||||
state,
|
||||
palette,
|
||||
metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
283
new_editor/src/Shell/UIEditorStatusBar.cpp
Normal file
283
new_editor/src/Shell/UIEditorStatusBar.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include <XCEditor/Shell/UIEditorStatusBar.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
bool ContainsPoint(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;
|
||||
}
|
||||
|
||||
bool HasArea(const UIRect& rect) {
|
||||
return rect.width > 0.0f && rect.height > 0.0f;
|
||||
}
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
UIColor ResolveSegmentFillColor(
|
||||
bool hovered,
|
||||
bool active,
|
||||
const UIEditorStatusBarPalette& palette) {
|
||||
if (active) {
|
||||
return palette.segmentActiveColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.segmentHoveredColor;
|
||||
}
|
||||
|
||||
return palette.segmentColor;
|
||||
}
|
||||
|
||||
float ResolveSegmentCornerRounding(const UIEditorStatusBarMetrics& metrics) {
|
||||
return (std::max)(metrics.cornerRounding - 2.0f, 0.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
float ResolveUIEditorStatusBarDesiredSegmentWidth(
|
||||
const UIEditorStatusBarSegment& segment,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
if (segment.desiredWidth > 0.0f) {
|
||||
return segment.desiredWidth;
|
||||
}
|
||||
|
||||
return segment.label.empty()
|
||||
? metrics.segmentPaddingX * 2.0f
|
||||
: metrics.segmentPaddingX * 2.0f +
|
||||
static_cast<float>(segment.label.size()) * metrics.estimatedGlyphWidth;
|
||||
}
|
||||
|
||||
UIEditorStatusBarLayout BuildUIEditorStatusBarLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
UIEditorStatusBarLayout layout = {};
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
layout.segmentRects.resize(segments.size(), UIRect{});
|
||||
layout.separatorRects.resize(segments.size(), UIRect{});
|
||||
|
||||
const float contentTop = layout.bounds.y;
|
||||
const float contentHeight = layout.bounds.height;
|
||||
const float leftStart = layout.bounds.x + metrics.outerPaddingX;
|
||||
const float rightLimit = layout.bounds.x + layout.bounds.width - metrics.outerPaddingX;
|
||||
|
||||
float leadingCursor = leftStart;
|
||||
float leadingRight = leftStart;
|
||||
for (std::size_t index = 0u; index < segments.size(); ++index) {
|
||||
const auto& segment = segments[index];
|
||||
if (segment.slot != UIEditorStatusBarSlot::Leading) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float segmentWidth = ResolveUIEditorStatusBarDesiredSegmentWidth(segment, metrics);
|
||||
layout.segmentRects[index] = UIRect(
|
||||
leadingCursor,
|
||||
contentTop,
|
||||
segmentWidth,
|
||||
contentHeight);
|
||||
leadingCursor += segmentWidth;
|
||||
leadingRight = leadingCursor;
|
||||
|
||||
if (segment.showSeparator) {
|
||||
layout.separatorRects[index] = UIRect(
|
||||
leadingCursor,
|
||||
contentTop + metrics.separatorInsetY,
|
||||
metrics.separatorWidth,
|
||||
(std::max)(contentHeight - metrics.separatorInsetY * 2.0f, 0.0f));
|
||||
leadingCursor += metrics.separatorWidth;
|
||||
}
|
||||
|
||||
leadingCursor += metrics.segmentGap;
|
||||
leadingRight = (std::max)(leadingRight, leadingCursor - metrics.segmentGap);
|
||||
}
|
||||
|
||||
float trailingCursor = rightLimit;
|
||||
float trailingLeft = rightLimit;
|
||||
for (std::size_t reverseIndex = segments.size(); reverseIndex > 0u; --reverseIndex) {
|
||||
const std::size_t index = reverseIndex - 1u;
|
||||
const auto& segment = segments[index];
|
||||
if (segment.slot != UIEditorStatusBarSlot::Trailing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float segmentWidth = ResolveUIEditorStatusBarDesiredSegmentWidth(segment, metrics);
|
||||
const bool hasSeparator = segment.showSeparator;
|
||||
|
||||
if (hasSeparator) {
|
||||
trailingCursor -= metrics.separatorWidth;
|
||||
layout.separatorRects[index] = UIRect(
|
||||
trailingCursor,
|
||||
contentTop + metrics.separatorInsetY,
|
||||
metrics.separatorWidth,
|
||||
(std::max)(contentHeight - metrics.separatorInsetY * 2.0f, 0.0f));
|
||||
trailingCursor -= metrics.segmentGap;
|
||||
}
|
||||
|
||||
trailingCursor -= segmentWidth;
|
||||
layout.segmentRects[index] = UIRect(
|
||||
trailingCursor,
|
||||
contentTop,
|
||||
segmentWidth,
|
||||
contentHeight);
|
||||
trailingCursor -= metrics.segmentGap;
|
||||
trailingLeft = (std::min)(trailingLeft, layout.segmentRects[index].x);
|
||||
}
|
||||
|
||||
const float leadingWidth =
|
||||
leadingRight > leftStart ? leadingRight - leftStart : 0.0f;
|
||||
layout.leadingSlotRect = UIRect(leftStart, contentTop, leadingWidth, contentHeight);
|
||||
|
||||
const float trailingWidth =
|
||||
trailingLeft < rightLimit ? rightLimit - trailingLeft : 0.0f;
|
||||
layout.trailingSlotRect = UIRect(trailingLeft, contentTop, trailingWidth, contentHeight);
|
||||
|
||||
if (HasArea(layout.leadingSlotRect) &&
|
||||
HasArea(layout.trailingSlotRect) &&
|
||||
layout.leadingSlotRect.x + layout.leadingSlotRect.width + metrics.slotGapMin >
|
||||
layout.trailingSlotRect.x) {
|
||||
const float overlap =
|
||||
layout.leadingSlotRect.x + layout.leadingSlotRect.width + metrics.slotGapMin -
|
||||
layout.trailingSlotRect.x;
|
||||
layout.trailingSlotRect.x += overlap;
|
||||
layout.trailingSlotRect.width = (std::max)(layout.trailingSlotRect.width - overlap, 0.0f);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorStatusBarHitTarget HitTestUIEditorStatusBar(
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const UIPoint& point) {
|
||||
if (!ContainsPoint(layout.bounds, point)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (std::size_t index = 0u; index < layout.separatorRects.size(); ++index) {
|
||||
if (HasArea(layout.separatorRects[index]) &&
|
||||
ContainsPoint(layout.separatorRects[index], point)) {
|
||||
return { UIEditorStatusBarHitTargetKind::Separator, index };
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0u; index < layout.segmentRects.size(); ++index) {
|
||||
if (HasArea(layout.segmentRects[index]) &&
|
||||
ContainsPoint(layout.segmentRects[index], point)) {
|
||||
return { UIEditorStatusBarHitTargetKind::Segment, index };
|
||||
}
|
||||
}
|
||||
|
||||
return { UIEditorStatusBarHitTargetKind::Background, UIEditorStatusBarInvalidIndex };
|
||||
}
|
||||
|
||||
UIColor ResolveUIEditorStatusBarTextColor(
|
||||
UIEditorStatusBarTextTone tone,
|
||||
const UIEditorStatusBarPalette& palette) {
|
||||
switch (tone) {
|
||||
case UIEditorStatusBarTextTone::Muted:
|
||||
return palette.textMuted;
|
||||
case UIEditorStatusBarTextTone::Accent:
|
||||
return palette.textAccent;
|
||||
case UIEditorStatusBarTextTone::Primary:
|
||||
default:
|
||||
return palette.textPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorStatusBarBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState& state,
|
||||
const UIEditorStatusBarPalette& palette,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.surfaceColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.borderThickness,
|
||||
metrics.cornerRounding);
|
||||
|
||||
for (std::size_t index = 0u; index < segments.size(); ++index) {
|
||||
if (!HasArea(layout.segmentRects[index])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
const bool active = state.activeIndex == index;
|
||||
if ((!hovered && !active) || !segments[index].interactive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.AddFilledRect(
|
||||
layout.segmentRects[index],
|
||||
ResolveSegmentFillColor(hovered, active, palette),
|
||||
ResolveSegmentCornerRounding(metrics));
|
||||
drawList.AddRectOutline(
|
||||
layout.segmentRects[index],
|
||||
palette.segmentBorderColor,
|
||||
1.0f,
|
||||
ResolveSegmentCornerRounding(metrics));
|
||||
}
|
||||
|
||||
for (const UIRect& separatorRect : layout.separatorRects) {
|
||||
if (!HasArea(separatorRect)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.AddFilledRect(separatorRect, palette.separatorColor);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorStatusBarForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState&,
|
||||
const UIEditorStatusBarPalette& palette,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
for (std::size_t index = 0u; index < segments.size(); ++index) {
|
||||
if (!HasArea(layout.segmentRects[index]) || segments[index].label.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
layout.segmentRects[index].x + metrics.segmentPaddingX,
|
||||
layout.segmentRects[index].y + metrics.segmentPaddingY),
|
||||
segments[index].label,
|
||||
ResolveUIEditorStatusBarTextColor(segments[index].tone, palette),
|
||||
12.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorStatusBar(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState& state,
|
||||
const UIEditorStatusBarPalette& palette,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
const UIEditorStatusBarLayout layout =
|
||||
BuildUIEditorStatusBarLayout(bounds, segments, metrics);
|
||||
AppendUIEditorStatusBarBackground(drawList, layout, segments, state, palette, metrics);
|
||||
AppendUIEditorStatusBarForeground(drawList, layout, segments, state, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
221
new_editor/src/Shell/UIEditorViewportInputBridge.cpp
Normal file
221
new_editor/src/Shell/UIEditorViewportInputBridge.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
#include <XCEditor/Shell/UIEditorViewportInputBridge.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
bool ContainsPoint(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;
|
||||
}
|
||||
|
||||
UIPoint ToLocalPoint(const UIRect& rect, const UIPoint& point) {
|
||||
return UIPoint(point.x - rect.x, point.y - rect.y);
|
||||
}
|
||||
|
||||
std::uint8_t ButtonMask(UIPointerButton button) {
|
||||
switch (button) {
|
||||
case UIPointerButton::Left: return 1u << 0u;
|
||||
case UIPointerButton::Right: return 1u << 1u;
|
||||
case UIPointerButton::Middle: return 1u << 2u;
|
||||
case UIPointerButton::X1: return 1u << 3u;
|
||||
case UIPointerButton::X2: return 1u << 4u;
|
||||
case UIPointerButton::None:
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
bool AnyPointerButtonsDown(const UIEditorViewportInputBridgeState& state) {
|
||||
return state.pointerButtonsDownMask != 0u;
|
||||
}
|
||||
|
||||
void ClearCapture(UIEditorViewportInputBridgeState& state) {
|
||||
state.captured = false;
|
||||
state.captureButton = UIPointerButton::None;
|
||||
}
|
||||
|
||||
void ClearInputState(UIEditorViewportInputBridgeState& state) {
|
||||
state.pressedKeys.clear();
|
||||
state.pointerButtonsDownMask = 0u;
|
||||
ClearCapture(state);
|
||||
state.hovered = false;
|
||||
}
|
||||
|
||||
void UpdatePointerPosition(
|
||||
UIEditorViewportInputBridgeState& state,
|
||||
UIEditorViewportInputBridgeFrame& frame,
|
||||
const UIRect& inputRect,
|
||||
const UIPoint& screenPosition,
|
||||
const ::XCEngine::UI::UIInputModifiers& modifiers) {
|
||||
if (state.hasPointerPosition) {
|
||||
frame.pointerDelta.x += screenPosition.x - state.lastScreenPointerPosition.x;
|
||||
frame.pointerDelta.y += screenPosition.y - state.lastScreenPointerPosition.y;
|
||||
frame.pointerMoved =
|
||||
frame.pointerMoved ||
|
||||
screenPosition.x != state.lastScreenPointerPosition.x ||
|
||||
screenPosition.y != state.lastScreenPointerPosition.y;
|
||||
}
|
||||
|
||||
state.lastScreenPointerPosition = screenPosition;
|
||||
state.lastLocalPointerPosition = ToLocalPoint(inputRect, screenPosition);
|
||||
state.hasPointerPosition = true;
|
||||
state.modifiers = modifiers;
|
||||
|
||||
frame.screenPointerPosition = state.lastScreenPointerPosition;
|
||||
frame.localPointerPosition = state.lastLocalPointerPosition;
|
||||
frame.modifiers = state.modifiers;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsUIEditorViewportInputBridgeKeyDown(
|
||||
const UIEditorViewportInputBridgeState& state,
|
||||
std::int32_t keyCode) {
|
||||
return state.pressedKeys.contains(keyCode);
|
||||
}
|
||||
|
||||
bool IsUIEditorViewportInputBridgePointerButtonDown(
|
||||
const UIEditorViewportInputBridgeState& state,
|
||||
UIPointerButton button) {
|
||||
const std::uint8_t mask = ButtonMask(button);
|
||||
return mask != 0u && (state.pointerButtonsDownMask & mask) != 0u;
|
||||
}
|
||||
|
||||
UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge(
|
||||
UIEditorViewportInputBridgeState& state,
|
||||
const UIRect& inputRect,
|
||||
const std::vector<UIInputEvent>& events,
|
||||
const UIEditorViewportInputBridgeConfig& config) {
|
||||
UIEditorViewportInputBridgeFrame frame = {};
|
||||
frame.screenPointerPosition = state.lastScreenPointerPosition;
|
||||
frame.localPointerPosition = state.lastLocalPointerPosition;
|
||||
frame.modifiers = state.modifiers;
|
||||
|
||||
for (const UIInputEvent& event : events) {
|
||||
const bool inside = ContainsPoint(inputRect, event.position);
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerEnter:
|
||||
case UIInputEventType::PointerMove:
|
||||
UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers);
|
||||
state.hovered = inside;
|
||||
break;
|
||||
case UIInputEventType::PointerLeave:
|
||||
state.hovered = false;
|
||||
state.lastScreenPointerPosition = event.position;
|
||||
state.lastLocalPointerPosition = ToLocalPoint(inputRect, event.position);
|
||||
frame.screenPointerPosition = state.lastScreenPointerPosition;
|
||||
frame.localPointerPosition = state.lastLocalPointerPosition;
|
||||
frame.modifiers = state.modifiers;
|
||||
break;
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers);
|
||||
state.hovered = inside;
|
||||
state.pointerButtonsDownMask |= ButtonMask(event.pointerButton);
|
||||
frame.changedPointerButton = event.pointerButton;
|
||||
if (inside) {
|
||||
frame.pointerPressedInside = true;
|
||||
if (config.focusOnPointerDownInside && !state.focused) {
|
||||
state.focused = true;
|
||||
frame.focusGained = true;
|
||||
}
|
||||
if (config.capturePointerOnPointerDownInside && !state.captured) {
|
||||
state.captured = true;
|
||||
state.captureButton = event.pointerButton;
|
||||
frame.captureStarted = true;
|
||||
}
|
||||
} else if (config.clearFocusOnPointerDownOutside) {
|
||||
if (state.focused) {
|
||||
state.focused = false;
|
||||
frame.focusLost = true;
|
||||
}
|
||||
if (state.captured) {
|
||||
ClearCapture(state);
|
||||
frame.captureEnded = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers);
|
||||
state.hovered = inside;
|
||||
state.pointerButtonsDownMask &= static_cast<std::uint8_t>(~ButtonMask(event.pointerButton));
|
||||
frame.changedPointerButton = event.pointerButton;
|
||||
if (inside) {
|
||||
frame.pointerReleasedInside = true;
|
||||
}
|
||||
if (state.captured &&
|
||||
state.captureButton == event.pointerButton &&
|
||||
!AnyPointerButtonsDown(state)) {
|
||||
ClearCapture(state);
|
||||
frame.captureEnded = true;
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::PointerWheel:
|
||||
UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers);
|
||||
state.hovered = inside;
|
||||
if (inside || state.captured) {
|
||||
frame.wheelDelta += event.wheelDelta;
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::KeyDown:
|
||||
state.modifiers = event.modifiers;
|
||||
frame.modifiers = state.modifiers;
|
||||
if (state.focused) {
|
||||
state.pressedKeys.insert(event.keyCode);
|
||||
frame.pressedKeyCodes.push_back(event.keyCode);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::KeyUp:
|
||||
state.modifiers = event.modifiers;
|
||||
frame.modifiers = state.modifiers;
|
||||
if (state.focused) {
|
||||
state.pressedKeys.erase(event.keyCode);
|
||||
frame.releasedKeyCodes.push_back(event.keyCode);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::Character:
|
||||
if (state.focused) {
|
||||
frame.characters.push_back(event.character);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::FocusLost:
|
||||
if (state.focused) {
|
||||
frame.focusLost = true;
|
||||
}
|
||||
if (state.captured) {
|
||||
frame.captureEnded = true;
|
||||
}
|
||||
state.focused = false;
|
||||
ClearInputState(state);
|
||||
break;
|
||||
case UIInputEventType::FocusGained:
|
||||
state.modifiers = event.modifiers;
|
||||
frame.modifiers = state.modifiers;
|
||||
break;
|
||||
case UIInputEventType::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
frame.pointerInside = ContainsPoint(inputRect, state.lastScreenPointerPosition);
|
||||
frame.hovered = state.hovered;
|
||||
frame.focused = state.focused;
|
||||
frame.captured = state.captured;
|
||||
frame.screenPointerPosition = state.lastScreenPointerPosition;
|
||||
frame.localPointerPosition = state.lastLocalPointerPosition;
|
||||
frame.modifiers = state.modifiers;
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
72
new_editor/src/Shell/UIEditorViewportShell.cpp
Normal file
72
new_editor/src/Shell/UIEditorViewportShell.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include <XCEditor/Shell/UIEditorViewportShell.h>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using Widgets::BuildUIEditorViewportSlotLayout;
|
||||
using Widgets::UIEditorViewportSlotFrame;
|
||||
using Widgets::UIEditorViewportSlotLayout;
|
||||
using Widgets::UIEditorViewportSlotState;
|
||||
|
||||
UIEditorViewportSlotLayout BuildViewportShellLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorViewportShellSpec& spec,
|
||||
const UIEditorViewportSlotFrame& frame,
|
||||
const Widgets::UIEditorViewportSlotMetrics& metrics) {
|
||||
return BuildUIEditorViewportSlotLayout(
|
||||
bounds,
|
||||
spec.chrome,
|
||||
frame,
|
||||
spec.toolItems,
|
||||
spec.statusSegments,
|
||||
metrics);
|
||||
}
|
||||
|
||||
UIEditorViewportSlotState BuildViewportShellSlotState(
|
||||
const UIEditorViewportShellVisualState& visualState,
|
||||
const UIEditorViewportInputBridgeFrame& inputFrame) {
|
||||
UIEditorViewportSlotState slotState = {};
|
||||
slotState.focused = inputFrame.focused;
|
||||
slotState.surfaceHovered = inputFrame.hovered;
|
||||
slotState.surfaceActive = inputFrame.focused || inputFrame.captured;
|
||||
slotState.inputCaptured = inputFrame.captured;
|
||||
slotState.hoveredToolIndex = visualState.hoveredToolIndex;
|
||||
slotState.activeToolIndex = visualState.activeToolIndex;
|
||||
slotState.statusBarState = visualState.statusBarState;
|
||||
slotState.statusBarState.focused =
|
||||
slotState.statusBarState.focused || inputFrame.focused;
|
||||
return slotState;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorViewportShellRequest ResolveUIEditorViewportShellRequest(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorViewportShellSpec& spec,
|
||||
const Widgets::UIEditorViewportSlotMetrics& metrics) {
|
||||
UIEditorViewportShellRequest request = {};
|
||||
request.slotLayout = BuildViewportShellLayout(bounds, spec, {}, metrics);
|
||||
request.requestedViewportSize = request.slotLayout.requestedSurfaceSize;
|
||||
return request;
|
||||
}
|
||||
|
||||
UIEditorViewportShellFrame UpdateUIEditorViewportShell(
|
||||
UIEditorViewportShellState& state,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorViewportShellModel& model,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorViewportSlotMetrics& metrics) {
|
||||
UIEditorViewportShellFrame frame = {};
|
||||
frame.slotLayout = BuildViewportShellLayout(bounds, model.spec, model.frame, metrics);
|
||||
frame.requestedViewportSize = frame.slotLayout.requestedSurfaceSize;
|
||||
frame.inputFrame = UpdateUIEditorViewportInputBridge(
|
||||
state.inputBridgeState,
|
||||
frame.slotLayout.inputRect,
|
||||
inputEvents,
|
||||
model.spec.inputBridgeConfig);
|
||||
frame.slotState = BuildViewportShellSlotState(model.spec.visualState, frame.inputFrame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
532
new_editor/src/Shell/UIEditorViewportSlot.cpp
Normal file
532
new_editor/src/Shell/UIEditorViewportSlot.cpp
Normal file
@@ -0,0 +1,532 @@
|
||||
#include <XCEditor/Shell/UIEditorViewportSlot.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::UISize;
|
||||
|
||||
bool ContainsPoint(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;
|
||||
}
|
||||
|
||||
bool HasArea(const UIRect& rect) {
|
||||
return rect.width > 0.0f && rect.height > 0.0f;
|
||||
}
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
UIRect InsetRect(const UIRect& rect, float inset) {
|
||||
const float clampedInset = ClampNonNegative(inset);
|
||||
return UIRect(
|
||||
rect.x + clampedInset,
|
||||
rect.y + clampedInset,
|
||||
(std::max)(rect.width - clampedInset * 2.0f, 0.0f),
|
||||
(std::max)(rect.height - clampedInset * 2.0f, 0.0f));
|
||||
}
|
||||
|
||||
UISize ResolveFrameAspectSize(const UIEditorViewportSlotFrame& frame, const UISize& fallback) {
|
||||
if (frame.presentedSize.width > 0.0f && frame.presentedSize.height > 0.0f) {
|
||||
return frame.presentedSize;
|
||||
}
|
||||
|
||||
if (frame.texture.IsValid()) {
|
||||
return UISize(
|
||||
static_cast<float>(frame.texture.width),
|
||||
static_cast<float>(frame.texture.height));
|
||||
}
|
||||
|
||||
if (frame.requestedSize.width > 0.0f && frame.requestedSize.height > 0.0f) {
|
||||
return frame.requestedSize;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
UIRect FitRectToAspect(const UIRect& container, const UISize& size) {
|
||||
if (!HasArea(container) || size.width <= 0.0f || size.height <= 0.0f) {
|
||||
return container;
|
||||
}
|
||||
|
||||
const float containerAspect = container.width / container.height;
|
||||
const float frameAspect = size.width / size.height;
|
||||
if (frameAspect <= 0.0f) {
|
||||
return container;
|
||||
}
|
||||
|
||||
if (frameAspect >= containerAspect) {
|
||||
const float fittedHeight = container.width / frameAspect;
|
||||
return UIRect(
|
||||
container.x,
|
||||
container.y + (container.height - fittedHeight) * 0.5f,
|
||||
container.width,
|
||||
fittedHeight);
|
||||
}
|
||||
|
||||
const float fittedWidth = container.height * frameAspect;
|
||||
return UIRect(
|
||||
container.x + (container.width - fittedWidth) * 0.5f,
|
||||
container.y,
|
||||
fittedWidth,
|
||||
container.height);
|
||||
}
|
||||
|
||||
UIColor ResolveToolFillColor(
|
||||
const UIEditorViewportSlotToolItem& item,
|
||||
bool hovered,
|
||||
bool active,
|
||||
const UIEditorViewportSlotPalette& palette) {
|
||||
if (!item.enabled) {
|
||||
return palette.toolDisabledColor;
|
||||
}
|
||||
|
||||
if (active || item.selected) {
|
||||
return palette.toolSelectedColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.toolHoveredColor;
|
||||
}
|
||||
|
||||
return palette.toolColor;
|
||||
}
|
||||
|
||||
UIColor ResolveSurfaceBorderColor(
|
||||
const UIEditorViewportSlotState& state,
|
||||
const UIEditorViewportSlotPalette& palette) {
|
||||
if (state.inputCaptured) {
|
||||
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) {
|
||||
return metrics.focusedSurfaceBorderThickness;
|
||||
}
|
||||
|
||||
return metrics.surfaceBorderThickness;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
float ResolveUIEditorViewportSlotDesiredToolWidth(
|
||||
const UIEditorViewportSlotToolItem& item,
|
||||
const UIEditorViewportSlotMetrics& metrics) {
|
||||
if (item.desiredWidth > 0.0f) {
|
||||
return item.desiredWidth;
|
||||
}
|
||||
|
||||
return item.label.empty()
|
||||
? metrics.toolPaddingX * 2.0f
|
||||
: metrics.toolPaddingX * 2.0f +
|
||||
static_cast<float>(item.label.size()) * metrics.estimatedGlyphWidth;
|
||||
}
|
||||
|
||||
UIEditorViewportSlotLayout BuildUIEditorViewportSlotLayout(
|
||||
const UIRect& bounds,
|
||||
const UIEditorViewportSlotChrome& chrome,
|
||||
const UIEditorViewportSlotFrame& frame,
|
||||
const std::vector<UIEditorViewportSlotToolItem>& toolItems,
|
||||
const std::vector<UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorViewportSlotMetrics& metrics) {
|
||||
UIEditorViewportSlotLayout layout = {};
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
layout.toolItemRects.resize(toolItems.size(), UIRect{});
|
||||
|
||||
float remainingHeight = layout.bounds.height;
|
||||
const float topBarHeight =
|
||||
chrome.showTopBar
|
||||
? (std::min)(ClampNonNegative(chrome.topBarHeight), remainingHeight)
|
||||
: 0.0f;
|
||||
remainingHeight = (std::max)(remainingHeight - topBarHeight, 0.0f);
|
||||
const float bottomBarHeight =
|
||||
chrome.showBottomBar
|
||||
? (std::min)(ClampNonNegative(chrome.bottomBarHeight), remainingHeight)
|
||||
: 0.0f;
|
||||
|
||||
layout.hasTopBar = topBarHeight > 0.0f;
|
||||
layout.hasBottomBar = bottomBarHeight > 0.0f;
|
||||
|
||||
if (layout.hasTopBar) {
|
||||
layout.topBarRect = UIRect(
|
||||
layout.bounds.x,
|
||||
layout.bounds.y,
|
||||
layout.bounds.width,
|
||||
topBarHeight);
|
||||
}
|
||||
|
||||
if (layout.hasBottomBar) {
|
||||
layout.bottomBarRect = UIRect(
|
||||
layout.bounds.x,
|
||||
layout.bounds.y + layout.bounds.height - bottomBarHeight,
|
||||
layout.bounds.width,
|
||||
bottomBarHeight);
|
||||
layout.statusBarLayout =
|
||||
BuildUIEditorStatusBarLayout(layout.bottomBarRect, statusSegments);
|
||||
}
|
||||
|
||||
const float surfaceTop = layout.hasTopBar
|
||||
? layout.topBarRect.y + layout.topBarRect.height
|
||||
: layout.bounds.y;
|
||||
const float surfaceBottom = layout.hasBottomBar
|
||||
? layout.bottomBarRect.y
|
||||
: layout.bounds.y + layout.bounds.height;
|
||||
layout.surfaceRect = UIRect(
|
||||
layout.bounds.x,
|
||||
surfaceTop,
|
||||
layout.bounds.width,
|
||||
(std::max)(surfaceBottom - surfaceTop, 0.0f));
|
||||
layout.inputRect = InsetRect(layout.surfaceRect, metrics.surfaceInset);
|
||||
layout.requestedSurfaceSize =
|
||||
UISize(layout.inputRect.width, layout.inputRect.height);
|
||||
layout.textureRect = frame.hasTexture
|
||||
? FitRectToAspect(
|
||||
layout.inputRect,
|
||||
ResolveFrameAspectSize(frame, layout.requestedSurfaceSize))
|
||||
: layout.inputRect;
|
||||
|
||||
if (!layout.hasTopBar) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const float toolbarTop = layout.topBarRect.y + metrics.topBarPaddingY;
|
||||
const float toolbarHeight =
|
||||
(std::max)(layout.topBarRect.height - metrics.topBarPaddingY * 2.0f, 0.0f);
|
||||
const float innerLeft = layout.topBarRect.x + metrics.topBarPaddingX;
|
||||
const float innerRight =
|
||||
layout.topBarRect.x + layout.topBarRect.width - metrics.topBarPaddingX;
|
||||
|
||||
float leadingCursor = innerLeft;
|
||||
float trailingCursor = innerRight;
|
||||
float leadingRight = innerLeft;
|
||||
float trailingLeft = innerRight;
|
||||
|
||||
for (std::size_t index = 0u; index < toolItems.size(); ++index) {
|
||||
const auto& item = toolItems[index];
|
||||
const float itemWidth = ResolveUIEditorViewportSlotDesiredToolWidth(item, metrics);
|
||||
if (item.slot != UIEditorViewportSlotToolSlot::Leading) {
|
||||
continue;
|
||||
}
|
||||
|
||||
layout.toolItemRects[index] = UIRect(
|
||||
leadingCursor,
|
||||
toolbarTop,
|
||||
itemWidth,
|
||||
toolbarHeight);
|
||||
leadingCursor += itemWidth + metrics.toolGap;
|
||||
leadingRight = layout.toolItemRects[index].x + layout.toolItemRects[index].width;
|
||||
}
|
||||
|
||||
for (std::size_t reverseIndex = toolItems.size(); reverseIndex > 0u; --reverseIndex) {
|
||||
const std::size_t index = reverseIndex - 1u;
|
||||
const auto& item = toolItems[index];
|
||||
const float itemWidth = ResolveUIEditorViewportSlotDesiredToolWidth(item, metrics);
|
||||
if (item.slot != UIEditorViewportSlotToolSlot::Trailing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
trailingCursor -= itemWidth;
|
||||
layout.toolItemRects[index] = UIRect(
|
||||
trailingCursor,
|
||||
toolbarTop,
|
||||
itemWidth,
|
||||
toolbarHeight);
|
||||
trailingLeft = trailingCursor;
|
||||
trailingCursor -= metrics.toolGap;
|
||||
}
|
||||
|
||||
if (leadingRight > innerLeft) {
|
||||
layout.toolbarLeadingRect = UIRect(
|
||||
innerLeft,
|
||||
layout.topBarRect.y,
|
||||
leadingRight - innerLeft,
|
||||
layout.topBarRect.height);
|
||||
}
|
||||
|
||||
if (trailingLeft < innerRight) {
|
||||
layout.toolbarTrailingRect = UIRect(
|
||||
trailingLeft,
|
||||
layout.topBarRect.y,
|
||||
innerRight - trailingLeft,
|
||||
layout.topBarRect.height);
|
||||
}
|
||||
|
||||
float titleLeft = innerLeft;
|
||||
if (HasArea(layout.toolbarLeadingRect)) {
|
||||
titleLeft = layout.toolbarLeadingRect.x + layout.toolbarLeadingRect.width + metrics.titleGap;
|
||||
}
|
||||
|
||||
float titleRight = innerRight;
|
||||
if (HasArea(layout.toolbarTrailingRect)) {
|
||||
titleRight = layout.toolbarTrailingRect.x - metrics.titleGap;
|
||||
}
|
||||
|
||||
layout.titleRect = UIRect(
|
||||
titleLeft,
|
||||
layout.topBarRect.y,
|
||||
(std::max)(titleRight - titleLeft, 0.0f),
|
||||
layout.topBarRect.height);
|
||||
layout.subtitleRect = layout.titleRect;
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorViewportSlotHitTarget HitTestUIEditorViewportSlot(
|
||||
const UIEditorViewportSlotLayout& layout,
|
||||
const UIPoint& point) {
|
||||
if (!ContainsPoint(layout.bounds, point)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (layout.hasTopBar) {
|
||||
for (std::size_t index = 0u; index < layout.toolItemRects.size(); ++index) {
|
||||
if (HasArea(layout.toolItemRects[index]) &&
|
||||
ContainsPoint(layout.toolItemRects[index], point)) {
|
||||
return { UIEditorViewportSlotHitTargetKind::ToolItem, index };
|
||||
}
|
||||
}
|
||||
|
||||
if (HasArea(layout.titleRect) && ContainsPoint(layout.titleRect, point)) {
|
||||
return { UIEditorViewportSlotHitTargetKind::Title, UIEditorViewportSlotInvalidIndex };
|
||||
}
|
||||
|
||||
if (ContainsPoint(layout.topBarRect, point)) {
|
||||
return { UIEditorViewportSlotHitTargetKind::TopBar, UIEditorViewportSlotInvalidIndex };
|
||||
}
|
||||
}
|
||||
|
||||
if (layout.hasBottomBar && ContainsPoint(layout.bottomBarRect, point)) {
|
||||
const UIEditorStatusBarHitTarget hit =
|
||||
HitTestUIEditorStatusBar(layout.statusBarLayout, point);
|
||||
switch (hit.kind) {
|
||||
case UIEditorStatusBarHitTargetKind::Segment:
|
||||
return { UIEditorViewportSlotHitTargetKind::StatusSegment, hit.index };
|
||||
case UIEditorStatusBarHitTargetKind::Separator:
|
||||
return { UIEditorViewportSlotHitTargetKind::StatusSeparator, hit.index };
|
||||
case UIEditorStatusBarHitTargetKind::Background:
|
||||
return { UIEditorViewportSlotHitTargetKind::BottomBar, UIEditorViewportSlotInvalidIndex };
|
||||
case UIEditorStatusBarHitTargetKind::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ContainsPoint(layout.inputRect, point) || ContainsPoint(layout.surfaceRect, point)) {
|
||||
return { UIEditorViewportSlotHitTargetKind::Surface, UIEditorViewportSlotInvalidIndex };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void AppendUIEditorViewportSlotBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorViewportSlotLayout& layout,
|
||||
const std::vector<UIEditorViewportSlotToolItem>& toolItems,
|
||||
const std::vector<UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorViewportSlotState& state,
|
||||
const UIEditorViewportSlotPalette& palette,
|
||||
const UIEditorViewportSlotMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.frameColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.outerBorderThickness,
|
||||
metrics.cornerRounding);
|
||||
|
||||
if (layout.hasTopBar) {
|
||||
drawList.AddFilledRect(layout.topBarRect, palette.topBarColor, metrics.cornerRounding);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
drawList.AddRectOutline(
|
||||
layout.inputRect,
|
||||
ResolveSurfaceBorderColor(state, palette),
|
||||
ResolveSurfaceBorderThickness(state, metrics));
|
||||
|
||||
for (std::size_t index = 0u; index < toolItems.size(); ++index) {
|
||||
if (!HasArea(layout.toolItemRects[index])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool hovered = state.hoveredToolIndex == index;
|
||||
const bool active = state.activeToolIndex == index;
|
||||
drawList.AddFilledRect(
|
||||
layout.toolItemRects[index],
|
||||
ResolveToolFillColor(toolItems[index], hovered, active, palette),
|
||||
metrics.toolCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.toolItemRects[index],
|
||||
palette.toolBorderColor,
|
||||
1.0f,
|
||||
metrics.toolCornerRounding);
|
||||
}
|
||||
|
||||
if (layout.hasBottomBar) {
|
||||
AppendUIEditorStatusBarBackground(
|
||||
drawList,
|
||||
layout.statusBarLayout,
|
||||
statusSegments,
|
||||
state.statusBarState,
|
||||
palette.statusBarPalette);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorViewportSlotForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorViewportSlotLayout& layout,
|
||||
const UIEditorViewportSlotChrome& chrome,
|
||||
const UIEditorViewportSlotFrame& frame,
|
||||
const std::vector<UIEditorViewportSlotToolItem>& toolItems,
|
||||
const std::vector<UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorViewportSlotState& state,
|
||||
const UIEditorViewportSlotPalette& palette,
|
||||
const UIEditorViewportSlotMetrics& metrics) {
|
||||
if (layout.hasTopBar) {
|
||||
if (!chrome.title.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(layout.titleRect.x, layout.topBarRect.y + metrics.titleInsetY),
|
||||
std::string(chrome.title),
|
||||
palette.textPrimary,
|
||||
15.0f);
|
||||
}
|
||||
|
||||
if (!chrome.subtitle.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(layout.subtitleRect.x, layout.topBarRect.y + metrics.subtitleInsetY),
|
||||
std::string(chrome.subtitle),
|
||||
palette.textSecondary,
|
||||
11.0f);
|
||||
}
|
||||
|
||||
for (std::size_t index = 0u; index < toolItems.size(); ++index) {
|
||||
if (!HasArea(layout.toolItemRects[index]) || toolItems[index].label.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
layout.toolItemRects[index].x + metrics.toolPaddingX,
|
||||
layout.toolItemRects[index].y + 5.0f),
|
||||
toolItems[index].label,
|
||||
toolItems[index].enabled ? palette.textPrimary : palette.textMuted,
|
||||
12.0f);
|
||||
}
|
||||
}
|
||||
|
||||
const UISize frameSize = ResolveFrameAspectSize(frame, layout.requestedSurfaceSize);
|
||||
if (frame.hasTexture && frame.texture.IsValid()) {
|
||||
drawList.AddImage(layout.textureRect, frame.texture, palette.imageTint);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.inputRect.x + 14.0f, layout.inputRect.y + 14.0f),
|
||||
"Texture " +
|
||||
std::to_string(static_cast<int>(frameSize.width)) +
|
||||
"x" +
|
||||
std::to_string(static_cast<int>(frameSize.height)),
|
||||
palette.textMuted,
|
||||
12.0f);
|
||||
} else {
|
||||
const std::string statusText = frame.statusText.empty()
|
||||
? std::string("Viewport is waiting for frame")
|
||||
: frame.statusText;
|
||||
drawList.AddText(
|
||||
UIPoint(layout.inputRect.x + 16.0f, layout.inputRect.y + 18.0f),
|
||||
statusText,
|
||||
palette.textPrimary,
|
||||
14.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.inputRect.x + 16.0f, layout.inputRect.y + 42.0f),
|
||||
"Requested surface: " +
|
||||
std::to_string(static_cast<int>(layout.requestedSurfaceSize.width)) +
|
||||
"x" +
|
||||
std::to_string(static_cast<int>(layout.requestedSurfaceSize.height)),
|
||||
palette.textMuted,
|
||||
12.0f);
|
||||
}
|
||||
|
||||
if (layout.hasBottomBar) {
|
||||
AppendUIEditorStatusBarForeground(
|
||||
drawList,
|
||||
layout.statusBarLayout,
|
||||
statusSegments,
|
||||
state.statusBarState,
|
||||
palette.statusBarPalette);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorViewportSlot(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bounds,
|
||||
const UIEditorViewportSlotChrome& chrome,
|
||||
const UIEditorViewportSlotFrame& frame,
|
||||
const std::vector<UIEditorViewportSlotToolItem>& toolItems,
|
||||
const std::vector<UIEditorStatusBarSegment>& statusSegments,
|
||||
const UIEditorViewportSlotState& state,
|
||||
const UIEditorViewportSlotPalette& palette,
|
||||
const UIEditorViewportSlotMetrics& metrics) {
|
||||
const UIEditorViewportSlotLayout layout =
|
||||
BuildUIEditorViewportSlotLayout(
|
||||
bounds,
|
||||
chrome,
|
||||
frame,
|
||||
toolItems,
|
||||
statusSegments,
|
||||
metrics);
|
||||
AppendUIEditorViewportSlotBackground(
|
||||
drawList,
|
||||
layout,
|
||||
toolItems,
|
||||
statusSegments,
|
||||
state,
|
||||
palette,
|
||||
metrics);
|
||||
AppendUIEditorViewportSlotForeground(
|
||||
drawList,
|
||||
layout,
|
||||
chrome,
|
||||
frame,
|
||||
toolItems,
|
||||
statusSegments,
|
||||
state,
|
||||
palette,
|
||||
metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
332
new_editor/src/Shell/UIEditorWorkspaceCompose.cpp
Normal file
332
new_editor/src/Shell/UIEditorWorkspaceCompose.cpp
Normal file
@@ -0,0 +1,332 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceCompose.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using Widgets::AppendUIEditorDockHostBackground;
|
||||
using Widgets::AppendUIEditorDockHostForeground;
|
||||
using Widgets::AppendUIEditorViewportSlotBackground;
|
||||
using Widgets::AppendUIEditorViewportSlotForeground;
|
||||
using Widgets::BuildUIEditorDockHostLayout;
|
||||
using Widgets::UIEditorDockHostForegroundOptions;
|
||||
|
||||
const UIEditorWorkspacePanelPresentationState* FindPanelStateImpl(
|
||||
const UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationState* FindMutablePanelStateImpl(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
for (UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool SupportsExternalViewportPresentation(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspacePanelPresentationModel& presentation) {
|
||||
if (presentation.kind != UIEditorPanelPresentationKind::ViewportShell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId);
|
||||
return descriptor != nullptr &&
|
||||
descriptor->presentationKind == UIEditorPanelPresentationKind::ViewportShell;
|
||||
}
|
||||
|
||||
std::vector<UIEditorPanelContentHostBinding> BuildContentHostBindings(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations) {
|
||||
std::vector<UIEditorPanelContentHostBinding> bindings = {};
|
||||
bindings.reserve(presentations.size());
|
||||
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId);
|
||||
if (descriptor == nullptr ||
|
||||
descriptor->presentationKind != presentation.kind ||
|
||||
!IsUIEditorPanelPresentationExternallyHosted(presentation.kind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorPanelContentHostBinding binding = {};
|
||||
binding.panelId = presentation.panelId;
|
||||
binding.kind = presentation.kind;
|
||||
bindings.push_back(std::move(binding));
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationState& EnsurePanelState(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
for (UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return panelState;
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationState panelState = {};
|
||||
panelState.panelId = std::string(panelId);
|
||||
state.panelStates.push_back(std::move(panelState));
|
||||
return state.panelStates.back();
|
||||
}
|
||||
|
||||
void ResetHiddenViewportPresentationState(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
UIEditorWorkspacePanelPresentationState* panelState =
|
||||
FindMutablePanelStateImpl(state, panelId);
|
||||
if (panelState == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelState->viewportShellState = {};
|
||||
}
|
||||
|
||||
void TrimObsoleteViewportPresentationStates(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations) {
|
||||
state.panelStates.erase(
|
||||
std::remove_if(
|
||||
state.panelStates.begin(),
|
||||
state.panelStates.end(),
|
||||
[&](const UIEditorWorkspacePanelPresentationState& panelState) {
|
||||
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
|
||||
if (presentation.panelId == panelState.panelId &&
|
||||
SupportsExternalViewportPresentation(panelRegistry, presentation)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
state.panelStates.end());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const UIEditorWorkspacePanelPresentationModel* FindUIEditorWorkspacePanelPresentationModel(
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
|
||||
if (presentation.panelId == panelId) {
|
||||
return &presentation;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelPresentationState* FindUIEditorWorkspacePanelPresentationState(
|
||||
const UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
return FindPanelStateImpl(state, panelId);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceViewportComposeRequest* FindUIEditorWorkspaceViewportPresentationRequest(
|
||||
const UIEditorWorkspaceComposeRequest& request,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorWorkspaceViewportComposeRequest& viewportRequest : request.viewportRequests) {
|
||||
if (viewportRequest.panelId == panelId) {
|
||||
return &viewportRequest;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceViewportComposeFrame* FindUIEditorWorkspaceViewportPresentationFrame(
|
||||
const UIEditorWorkspaceComposeFrame& frame,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) {
|
||||
if (viewportFrame.panelId == panelId) {
|
||||
return &viewportFrame;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations,
|
||||
const Widgets::UIEditorDockHostState& dockHostState,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics) {
|
||||
UIEditorWorkspaceComposeRequest request = {};
|
||||
request.dockHostLayout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
dockHostState,
|
||||
dockHostMetrics);
|
||||
const std::vector<UIEditorPanelContentHostBinding> contentHostBindings =
|
||||
BuildContentHostBindings(panelRegistry, presentations);
|
||||
request.contentHostRequest = ResolveUIEditorPanelContentHostRequest(
|
||||
request.dockHostLayout,
|
||||
panelRegistry,
|
||||
contentHostBindings);
|
||||
|
||||
for (const UIEditorPanelContentHostMountRequest& mountRequest :
|
||||
request.contentHostRequest.mountRequests) {
|
||||
if (mountRequest.kind != UIEditorPanelPresentationKind::ViewportShell) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelPresentationModel* presentation =
|
||||
FindUIEditorWorkspacePanelPresentationModel(presentations, mountRequest.panelId);
|
||||
if (presentation == nullptr ||
|
||||
!SupportsExternalViewportPresentation(panelRegistry, *presentation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceViewportComposeRequest viewportRequest = {};
|
||||
viewportRequest.panelId = mountRequest.panelId;
|
||||
viewportRequest.bounds = mountRequest.bounds;
|
||||
viewportRequest.viewportShellRequest = ResolveUIEditorViewportShellRequest(
|
||||
mountRequest.bounds,
|
||||
presentation->viewportShellModel.spec,
|
||||
viewportMetrics);
|
||||
request.viewportRequests.push_back(std::move(viewportRequest));
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorDockHostState& dockHostState,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics) {
|
||||
UIEditorWorkspaceComposeFrame frame = {};
|
||||
frame.dockHostLayout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
dockHostState,
|
||||
dockHostMetrics);
|
||||
const std::vector<UIEditorPanelContentHostBinding> contentHostBindings =
|
||||
BuildContentHostBindings(panelRegistry, presentations);
|
||||
const UIEditorPanelContentHostRequest contentHostRequest =
|
||||
ResolveUIEditorPanelContentHostRequest(
|
||||
frame.dockHostLayout,
|
||||
panelRegistry,
|
||||
contentHostBindings);
|
||||
frame.contentHostFrame = UpdateUIEditorPanelContentHost(
|
||||
state.contentHostState,
|
||||
contentHostRequest,
|
||||
panelRegistry,
|
||||
contentHostBindings);
|
||||
TrimObsoleteViewportPresentationStates(state, panelRegistry, presentations);
|
||||
|
||||
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
|
||||
if (!SupportsExternalViewportPresentation(panelRegistry, presentation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorPanelContentHostPanelState* contentHostPanelState =
|
||||
FindUIEditorPanelContentHostPanelState(frame.contentHostFrame, presentation.panelId);
|
||||
if (contentHostPanelState == nullptr || !contentHostPanelState->mounted) {
|
||||
ResetHiddenViewportPresentationState(state, presentation.panelId);
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationState& panelState =
|
||||
EnsurePanelState(state, presentation.panelId);
|
||||
|
||||
UIEditorWorkspaceViewportComposeFrame viewportFrame = {};
|
||||
viewportFrame.panelId = presentation.panelId;
|
||||
viewportFrame.bounds = contentHostPanelState->bounds;
|
||||
viewportFrame.viewportShellModel = presentation.viewportShellModel;
|
||||
viewportFrame.viewportShellFrame = UpdateUIEditorViewportShell(
|
||||
panelState.viewportShellState,
|
||||
contentHostPanelState->bounds,
|
||||
presentation.viewportShellModel,
|
||||
inputEvents,
|
||||
viewportMetrics);
|
||||
frame.viewportFrames.push_back(std::move(viewportFrame));
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectUIEditorWorkspaceComposeExternalBodyPanelIds(
|
||||
const UIEditorWorkspaceComposeFrame& frame) {
|
||||
return CollectMountedUIEditorPanelContentHostPanelIds(frame.contentHostFrame);
|
||||
}
|
||||
|
||||
void AppendUIEditorWorkspaceCompose(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorWorkspaceComposeFrame& frame,
|
||||
const Widgets::UIEditorDockHostPalette& dockHostPalette,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics,
|
||||
const Widgets::UIEditorViewportSlotPalette& viewportPalette,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics) {
|
||||
AppendUIEditorDockHostBackground(
|
||||
drawList,
|
||||
frame.dockHostLayout,
|
||||
dockHostPalette,
|
||||
dockHostMetrics);
|
||||
|
||||
for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) {
|
||||
AppendUIEditorViewportSlotBackground(
|
||||
drawList,
|
||||
viewportFrame.viewportShellFrame.slotLayout,
|
||||
viewportFrame.viewportShellModel.spec.toolItems,
|
||||
viewportFrame.viewportShellModel.spec.statusSegments,
|
||||
viewportFrame.viewportShellFrame.slotState,
|
||||
viewportPalette,
|
||||
viewportMetrics);
|
||||
AppendUIEditorViewportSlotForeground(
|
||||
drawList,
|
||||
viewportFrame.viewportShellFrame.slotLayout,
|
||||
viewportFrame.viewportShellModel.spec.chrome,
|
||||
viewportFrame.viewportShellModel.frame,
|
||||
viewportFrame.viewportShellModel.spec.toolItems,
|
||||
viewportFrame.viewportShellModel.spec.statusSegments,
|
||||
viewportFrame.viewportShellFrame.slotState,
|
||||
viewportPalette,
|
||||
viewportMetrics);
|
||||
}
|
||||
|
||||
UIEditorDockHostForegroundOptions foregroundOptions = {};
|
||||
foregroundOptions.externalBodyPanelIds =
|
||||
CollectUIEditorWorkspaceComposeExternalBodyPanelIds(frame);
|
||||
AppendUIEditorDockHostForeground(
|
||||
drawList,
|
||||
frame.dockHostLayout,
|
||||
foregroundOptions,
|
||||
dockHostPalette,
|
||||
dockHostMetrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
450
new_editor/src/Shell/UIEditorWorkspaceController.cpp
Normal file
450
new_editor/src/Shell/UIEditorWorkspaceController.cpp
Normal file
@@ -0,0 +1,450 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> panels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
|
||||
std::vector<std::string> ids = {};
|
||||
ids.reserve(panels.size());
|
||||
for (const UIEditorWorkspaceVisiblePanel& panel : panels) {
|
||||
ids.push_back(panel.panelId);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind) {
|
||||
switch (kind) {
|
||||
case UIEditorWorkspaceCommandKind::OpenPanel:
|
||||
return "OpenPanel";
|
||||
case UIEditorWorkspaceCommandKind::ClosePanel:
|
||||
return "ClosePanel";
|
||||
case UIEditorWorkspaceCommandKind::ShowPanel:
|
||||
return "ShowPanel";
|
||||
case UIEditorWorkspaceCommandKind::HidePanel:
|
||||
return "HidePanel";
|
||||
case UIEditorWorkspaceCommandKind::ActivatePanel:
|
||||
return "ActivatePanel";
|
||||
case UIEditorWorkspaceCommandKind::ResetWorkspace:
|
||||
return "ResetWorkspace";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
std::string_view GetUIEditorWorkspaceCommandStatusName(UIEditorWorkspaceCommandStatus status) {
|
||||
switch (status) {
|
||||
case UIEditorWorkspaceCommandStatus::Changed:
|
||||
return "Changed";
|
||||
case UIEditorWorkspaceCommandStatus::NoOp:
|
||||
return "NoOp";
|
||||
case UIEditorWorkspaceCommandStatus::Rejected:
|
||||
return "Rejected";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
std::string_view GetUIEditorWorkspaceLayoutOperationStatusName(
|
||||
UIEditorWorkspaceLayoutOperationStatus status) {
|
||||
switch (status) {
|
||||
case UIEditorWorkspaceLayoutOperationStatus::Changed:
|
||||
return "Changed";
|
||||
case UIEditorWorkspaceLayoutOperationStatus::NoOp:
|
||||
return "NoOp";
|
||||
case UIEditorWorkspaceLayoutOperationStatus::Rejected:
|
||||
return "Rejected";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
UIEditorWorkspaceController::UIEditorWorkspaceController(
|
||||
UIEditorPanelRegistry panelRegistry,
|
||||
UIEditorWorkspaceModel workspace,
|
||||
UIEditorWorkspaceSession session)
|
||||
: m_panelRegistry(std::move(panelRegistry))
|
||||
, m_baselineWorkspace(workspace)
|
||||
, m_baselineSession(session)
|
||||
, m_workspace(std::move(workspace))
|
||||
, m_session(std::move(session)) {
|
||||
}
|
||||
|
||||
UIEditorWorkspaceControllerValidationResult UIEditorWorkspaceController::ValidateState() const {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(m_panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
UIEditorWorkspaceControllerValidationResult result = {};
|
||||
result.code = UIEditorWorkspaceControllerValidationCode::InvalidPanelRegistry;
|
||||
result.message = registryValidation.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(m_workspace);
|
||||
if (!workspaceValidation.IsValid()) {
|
||||
UIEditorWorkspaceControllerValidationResult result = {};
|
||||
result.code = UIEditorWorkspaceControllerValidationCode::InvalidWorkspace;
|
||||
result.message = workspaceValidation.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(m_panelRegistry, m_workspace, m_session);
|
||||
if (!sessionValidation.IsValid()) {
|
||||
UIEditorWorkspaceControllerValidationResult result = {};
|
||||
result.code = UIEditorWorkspaceControllerValidationCode::InvalidWorkspaceSession;
|
||||
result.message = sessionValidation.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot UIEditorWorkspaceController::CaptureLayoutSnapshot() const {
|
||||
return BuildUIEditorWorkspaceLayoutSnapshot(m_workspace, m_session);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::BuildResult(
|
||||
const UIEditorWorkspaceCommand& command,
|
||||
UIEditorWorkspaceCommandStatus status,
|
||||
std::string message) const {
|
||||
UIEditorWorkspaceCommandResult result = {};
|
||||
result.kind = command.kind;
|
||||
result.status = status;
|
||||
result.panelId = command.panelId;
|
||||
result.message = std::move(message);
|
||||
result.activePanelId = m_workspace.activePanelId;
|
||||
result.visiblePanelIds = CollectVisiblePanelIds(m_workspace, m_session);
|
||||
return result;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus status,
|
||||
std::string message) const {
|
||||
UIEditorWorkspaceLayoutOperationResult result = {};
|
||||
result.status = status;
|
||||
result.message = std::move(message);
|
||||
result.activePanelId = m_workspace.activePanelId;
|
||||
result.visiblePanelIds = CollectVisiblePanelIds(m_workspace, m_session);
|
||||
return result;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::FinalizeMutation(
|
||||
const UIEditorWorkspaceCommand& command,
|
||||
bool changed,
|
||||
std::string changedMessage,
|
||||
std::string unexpectedFailureMessage,
|
||||
const UIEditorWorkspaceModel& previousWorkspace,
|
||||
const UIEditorWorkspaceSession& previousSession) {
|
||||
if (!changed) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
std::move(unexpectedFailureMessage));
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
m_session = previousSession;
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Command produced invalid workspace state: " + validation.message);
|
||||
}
|
||||
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Changed,
|
||||
std::move(changedMessage));
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* UIEditorWorkspaceController::FindPanelDescriptor(
|
||||
std::string_view panelId) const {
|
||||
return FindUIEditorPanelDescriptor(m_panelRegistry, panelId);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayoutSnapshot(
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot) {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(m_panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Panel registry invalid: " + registryValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(snapshot.workspace);
|
||||
if (!workspaceValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Layout workspace invalid: " + workspaceValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(m_panelRegistry, snapshot.workspace, snapshot.session);
|
||||
if (!sessionValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Layout session invalid: " + sessionValidation.message);
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, snapshot.workspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(m_session, snapshot.session)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Current state already matches the requested layout snapshot.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
const UIEditorWorkspaceSession previousSession = m_session;
|
||||
m_workspace = snapshot.workspace;
|
||||
m_session = snapshot.session;
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
m_session = previousSession;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Restored layout produced invalid controller state: " + validation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Layout restored.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreSerializedLayout(
|
||||
std::string_view serializedLayout) {
|
||||
const UIEditorWorkspaceLayoutLoadResult loadResult =
|
||||
DeserializeUIEditorWorkspaceLayoutSnapshot(m_panelRegistry, serializedLayout);
|
||||
if (!loadResult.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Serialized layout rejected: " + loadResult.message);
|
||||
}
|
||||
|
||||
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();
|
||||
if (command.kind != UIEditorWorkspaceCommandKind::ResetWorkspace &&
|
||||
!validation.IsValid()) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
const UIEditorWorkspaceSession previousSession = m_session;
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace
|
||||
? nullptr
|
||||
: FindUIEditorPanelSessionState(m_session, command.panelId);
|
||||
const UIEditorPanelDescriptor* panelDescriptor =
|
||||
command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace
|
||||
? nullptr
|
||||
: FindPanelDescriptor(command.panelId);
|
||||
|
||||
switch (command.kind) {
|
||||
case UIEditorWorkspaceCommandKind::OpenPanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "OpenPanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "OpenPanel target panel is missing.");
|
||||
}
|
||||
if (panelState->open && panelState->visible) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already open and visible.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryOpenUIEditorWorkspacePanel(m_panelRegistry, m_workspace, m_session, command.panelId),
|
||||
"Panel opened and activated.",
|
||||
"OpenPanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ClosePanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "ClosePanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "ClosePanel target panel is missing.");
|
||||
}
|
||||
if (!panelDescriptor->canClose) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "Panel cannot be closed.");
|
||||
}
|
||||
if (!panelState->open) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already closed.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryCloseUIEditorWorkspacePanel(m_panelRegistry, m_workspace, m_session, command.panelId),
|
||||
"Panel closed.",
|
||||
"ClosePanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ShowPanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "ShowPanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "ShowPanel target panel is missing.");
|
||||
}
|
||||
if (!panelState->open) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "Closed panel must be opened before it can be shown.");
|
||||
}
|
||||
if (panelState->visible) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already visible.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryShowUIEditorWorkspacePanel(m_panelRegistry, m_workspace, m_session, command.panelId),
|
||||
"Panel shown and activated.",
|
||||
"ShowPanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::HidePanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "HidePanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "HidePanel target panel is missing.");
|
||||
}
|
||||
if (!panelDescriptor->canHide) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "Panel cannot be hidden.");
|
||||
}
|
||||
if (!panelState->open) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "Closed panel cannot be hidden.");
|
||||
}
|
||||
if (!panelState->visible) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already hidden.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryHideUIEditorWorkspacePanel(m_panelRegistry, m_workspace, m_session, command.panelId),
|
||||
"Panel hidden and active panel re-resolved.",
|
||||
"HidePanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ActivatePanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "ActivatePanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "ActivatePanel target panel is missing.");
|
||||
}
|
||||
if (!panelState->open || !panelState->visible) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "Only open and visible panels can be activated.");
|
||||
}
|
||||
if (m_workspace.activePanelId == command.panelId) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already active.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryActivateUIEditorWorkspacePanel(m_panelRegistry, m_workspace, m_session, command.panelId),
|
||||
"Panel activated.",
|
||||
"ActivatePanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ResetWorkspace:
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, m_baselineWorkspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(m_session, m_baselineSession)) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::NoOp, "Workspace already matches the baseline state.");
|
||||
}
|
||||
|
||||
m_workspace = m_baselineWorkspace;
|
||||
m_session = m_baselineSession;
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
true,
|
||||
"Workspace reset to baseline.",
|
||||
"ResetWorkspace failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
}
|
||||
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::Rejected, "Unknown command kind.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
return UIEditorWorkspaceController(
|
||||
panelRegistry,
|
||||
workspace,
|
||||
BuildDefaultUIEditorWorkspaceSession(panelRegistry, workspace));
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
88
new_editor/src/Shell/UIEditorWorkspaceInteraction.cpp
Normal file
88
new_editor/src/Shell/UIEditorWorkspaceInteraction.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceInteraction.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool HasMeaningfulViewportInputFrame(const UIEditorViewportInputBridgeFrame& frame) {
|
||||
return frame.pointerMoved ||
|
||||
frame.pointerPressedInside ||
|
||||
frame.pointerReleasedInside ||
|
||||
frame.focusGained ||
|
||||
frame.focusLost ||
|
||||
frame.captureStarted ||
|
||||
frame.captureEnded ||
|
||||
frame.wheelDelta != 0.0f ||
|
||||
!frame.pressedKeyCodes.empty() ||
|
||||
!frame.releasedKeyCodes.empty() ||
|
||||
!frame.characters.empty();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWorkspaceInteractionFrame UpdateUIEditorWorkspaceInteraction(
|
||||
UIEditorWorkspaceInteractionState& state,
|
||||
UIEditorWorkspaceController& controller,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorWorkspaceInteractionModel& model,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics) {
|
||||
UIEditorWorkspaceInteractionFrame frame = {};
|
||||
frame.dockHostFrame = UpdateUIEditorDockHostInteraction(
|
||||
state.dockHostInteractionState,
|
||||
controller,
|
||||
bounds,
|
||||
inputEvents,
|
||||
dockHostMetrics);
|
||||
frame.composeFrame = UpdateUIEditorWorkspaceCompose(
|
||||
state.composeState,
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
model.workspacePresentations,
|
||||
inputEvents,
|
||||
state.dockHostInteractionState.dockHostState,
|
||||
dockHostMetrics,
|
||||
viewportMetrics);
|
||||
|
||||
frame.result.dockHostResult = frame.dockHostFrame.result;
|
||||
frame.result.consumed = frame.dockHostFrame.result.consumed;
|
||||
frame.result.requestPointerCapture = frame.dockHostFrame.result.requestPointerCapture;
|
||||
frame.result.releasePointerCapture = frame.dockHostFrame.result.releasePointerCapture;
|
||||
|
||||
for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.composeFrame.viewportFrames) {
|
||||
const UIEditorViewportInputBridgeFrame& inputFrame =
|
||||
viewportFrame.viewportShellFrame.inputFrame;
|
||||
const bool meaningfulInput = HasMeaningfulViewportInputFrame(inputFrame);
|
||||
|
||||
if (!meaningfulInput &&
|
||||
!inputFrame.captureStarted &&
|
||||
!inputFrame.captureEnded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frame.result.viewportPanelId.empty()) {
|
||||
frame.result.viewportPanelId = viewportFrame.panelId;
|
||||
frame.result.viewportInputFrame = inputFrame;
|
||||
}
|
||||
frame.result.viewportInteractionChanged =
|
||||
frame.result.viewportInteractionChanged || meaningfulInput;
|
||||
frame.result.requestPointerCapture =
|
||||
frame.result.requestPointerCapture || inputFrame.captureStarted;
|
||||
frame.result.releasePointerCapture =
|
||||
frame.result.releasePointerCapture || inputFrame.captureEnded;
|
||||
}
|
||||
|
||||
frame.result.consumed =
|
||||
frame.result.consumed ||
|
||||
frame.result.viewportInteractionChanged ||
|
||||
frame.result.requestPointerCapture ||
|
||||
frame.result.releasePointerCapture;
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
497
new_editor/src/Shell/UIEditorWorkspaceLayoutPersistence.cpp
Normal file
497
new_editor/src/Shell/UIEditorWorkspaceLayoutPersistence.cpp
Normal file
@@ -0,0 +1,497 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceLayoutPersistence.h>
|
||||
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kLayoutHeader = "XCUI_EDITOR_WORKSPACE_LAYOUT";
|
||||
constexpr int kLayoutVersion = 1;
|
||||
|
||||
struct LayoutLine {
|
||||
std::size_t number = 0u;
|
||||
std::string text = {};
|
||||
};
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode code,
|
||||
std::string message) {
|
||||
UIEditorWorkspaceLayoutLoadResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool HasTrailingTokens(std::istringstream& stream) {
|
||||
stream >> std::ws;
|
||||
return !stream.eof();
|
||||
}
|
||||
|
||||
std::string MakeLinePrefix(const LayoutLine& line) {
|
||||
return "Line " + std::to_string(line.number) + ": ";
|
||||
}
|
||||
|
||||
bool ParseBinaryFlag(
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
int& outValue,
|
||||
UIEditorWorkspaceLayoutLoadResult& outError,
|
||||
UIEditorWorkspaceLayoutLoadCode code,
|
||||
std::string_view fieldName) {
|
||||
if (!(stream >> outValue) || (outValue != 0 && outValue != 1)) {
|
||||
outError = MakeLoadError(
|
||||
code,
|
||||
MakeLinePrefix(line) + std::string(fieldName) + " must be encoded as 0 or 1.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseNodeLine(
|
||||
const LayoutLine& line,
|
||||
std::string& outTag,
|
||||
std::istringstream& outStream) {
|
||||
outStream = std::istringstream(line.text);
|
||||
return static_cast<bool>(outStream >> outTag);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseNodeRecursive(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
UIEditorWorkspaceNode& outNode);
|
||||
|
||||
std::string SerializeAxis(UIEditorWorkspaceSplitAxis axis) {
|
||||
return axis == UIEditorWorkspaceSplitAxis::Vertical ? "vertical" : "horizontal";
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseAxis(
|
||||
std::string_view text,
|
||||
UIEditorWorkspaceSplitAxis& outAxis,
|
||||
const LayoutLine& line) {
|
||||
if (text == "horizontal") {
|
||||
outAxis = UIEditorWorkspaceSplitAxis::Horizontal;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (text == "vertical") {
|
||||
outAxis = UIEditorWorkspaceSplitAxis::Vertical;
|
||||
return {};
|
||||
}
|
||||
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "split axis must be \"horizontal\" or \"vertical\".");
|
||||
}
|
||||
|
||||
void SerializeNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::ostringstream& stream) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel:
|
||||
stream << "node_panel "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< std::quoted(node.panel.panelId) << ' '
|
||||
<< std::quoted(node.panel.title) << ' '
|
||||
<< (node.panel.placeholder ? 1 : 0) << '\n';
|
||||
return;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::TabStack:
|
||||
stream << "node_tabstack "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< node.selectedTabIndex << ' '
|
||||
<< node.children.size() << '\n';
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
SerializeNodeRecursive(child, stream);
|
||||
}
|
||||
return;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
stream << "node_split "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< std::quoted(SerializeAxis(node.splitAxis)) << ' '
|
||||
<< std::setprecision(std::numeric_limits<float>::max_digits10)
|
||||
<< node.splitRatio << '\n';
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
SerializeNodeRecursive(child, stream);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<LayoutLine> CollectNonEmptyLines(std::string_view serializedLayout) {
|
||||
std::vector<LayoutLine> lines = {};
|
||||
std::istringstream stream{ std::string(serializedLayout) };
|
||||
std::string text = {};
|
||||
std::size_t lineNumber = 0u;
|
||||
while (std::getline(stream, text)) {
|
||||
++lineNumber;
|
||||
if (!text.empty() && text.back() == '\r') {
|
||||
text.pop_back();
|
||||
}
|
||||
|
||||
const std::size_t first = text.find_first_not_of(" \t");
|
||||
if (first == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push_back({ lineNumber, text });
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseHeader(const LayoutLine& line) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string header = {};
|
||||
int version = 0;
|
||||
if (!(stream >> header >> version) || HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidHeader,
|
||||
MakeLinePrefix(line) + "invalid layout header.");
|
||||
}
|
||||
|
||||
if (header != kLayoutHeader) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidHeader,
|
||||
MakeLinePrefix(line) + "layout header magic is invalid.");
|
||||
}
|
||||
|
||||
if (version != kLayoutVersion) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::UnsupportedVersion,
|
||||
MakeLinePrefix(line) + "unsupported layout version " + std::to_string(version) + ".");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseActiveRecord(
|
||||
const LayoutLine& line,
|
||||
std::string& outActivePanelId) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string tag = {};
|
||||
if (!(stream >> tag) || tag != "active" ||
|
||||
!(stream >> std::quoted(outActivePanelId)) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::MissingActiveRecord,
|
||||
MakeLinePrefix(line) + "active record must be `active \"panel-id\"`.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParsePanelNode(
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::string panelId = {};
|
||||
std::string title = {};
|
||||
int placeholderValue = 0;
|
||||
if (!(stream >> std::quoted(nodeId) >> std::quoted(panelId) >> std::quoted(title))) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "panel node record is malformed.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult boolParse = {};
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
placeholderValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
"placeholder")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "panel node record contains trailing tokens.");
|
||||
}
|
||||
|
||||
outNode = BuildUIEditorWorkspacePanel(
|
||||
std::move(nodeId),
|
||||
std::move(panelId),
|
||||
std::move(title),
|
||||
placeholderValue != 0);
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseTabStackNode(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::size_t selectedTabIndex = 0u;
|
||||
std::size_t childCount = 0u;
|
||||
if (!(stream >> std::quoted(nodeId) >> selectedTabIndex >> childCount) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "tab stack node record is malformed.");
|
||||
}
|
||||
|
||||
outNode = {};
|
||||
outNode.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
outNode.nodeId = std::move(nodeId);
|
||||
outNode.selectedTabIndex = selectedTabIndex;
|
||||
outNode.children.reserve(childCount);
|
||||
for (std::size_t childIndex = 0; childIndex < childCount; ++childIndex) {
|
||||
UIEditorWorkspaceNode child = {};
|
||||
UIEditorWorkspaceLayoutLoadResult childResult = ParseNodeRecursive(lines, index, child);
|
||||
if (!childResult.IsValid()) {
|
||||
return childResult;
|
||||
}
|
||||
outNode.children.push_back(std::move(child));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseSplitNode(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::string axisText = {};
|
||||
float splitRatio = 0.0f;
|
||||
if (!(stream >> std::quoted(nodeId) >> std::quoted(axisText) >> splitRatio) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "split node record is malformed.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSplitAxis axis = UIEditorWorkspaceSplitAxis::Horizontal;
|
||||
if (UIEditorWorkspaceLayoutLoadResult axisResult = ParseAxis(axisText, axis, line);
|
||||
!axisResult.IsValid()) {
|
||||
return axisResult;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode primary = {};
|
||||
UIEditorWorkspaceNode secondary = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult primaryResult = ParseNodeRecursive(lines, index, primary);
|
||||
!primaryResult.IsValid()) {
|
||||
return primaryResult;
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceLayoutLoadResult secondaryResult = ParseNodeRecursive(lines, index, secondary);
|
||||
!secondaryResult.IsValid()) {
|
||||
return secondaryResult;
|
||||
}
|
||||
|
||||
outNode = BuildUIEditorWorkspaceSplit(
|
||||
std::move(nodeId),
|
||||
axis,
|
||||
splitRatio,
|
||||
std::move(primary),
|
||||
std::move(secondary));
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseNodeRecursive(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
if (index >= lines.size()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::UnexpectedEndOfInput,
|
||||
"Unexpected end of input while parsing workspace nodes.");
|
||||
}
|
||||
|
||||
const LayoutLine& line = lines[index++];
|
||||
std::istringstream stream = {};
|
||||
std::string tag = {};
|
||||
if (!ParseNodeLine(line, tag, stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "workspace node record is empty.");
|
||||
}
|
||||
|
||||
if (tag == "node_panel") {
|
||||
return ParsePanelNode(stream, line, outNode);
|
||||
}
|
||||
|
||||
if (tag == "node_tabstack") {
|
||||
return ParseTabStackNode(lines, index, stream, line, outNode);
|
||||
}
|
||||
|
||||
if (tag == "node_split") {
|
||||
return ParseSplitNode(lines, index, stream, line, outNode);
|
||||
}
|
||||
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "unknown workspace node tag '" + tag + "'.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseSessionRecord(
|
||||
const LayoutLine& line,
|
||||
UIEditorPanelSessionState& outState) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string tag = {};
|
||||
int openValue = 0;
|
||||
int visibleValue = 0;
|
||||
if (!(stream >> tag) || tag != "session" ||
|
||||
!(stream >> std::quoted(outState.panelId))) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
MakeLinePrefix(line) + "session record must start with `session`.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult boolParse = {};
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
openValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
"open")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
visibleValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
"visible")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
MakeLinePrefix(line) + "session record contains trailing tokens.");
|
||||
}
|
||||
|
||||
outState.open = openValue != 0;
|
||||
outState.visible = visibleValue != 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot BuildUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
UIEditorWorkspaceLayoutSnapshot snapshot = {};
|
||||
snapshot.workspace = workspace;
|
||||
snapshot.session = session;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceLayoutSnapshotsEquivalent(
|
||||
const UIEditorWorkspaceLayoutSnapshot& lhs,
|
||||
const UIEditorWorkspaceLayoutSnapshot& rhs) {
|
||||
return AreUIEditorWorkspaceModelsEquivalent(lhs.workspace, rhs.workspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(lhs.session, rhs.session);
|
||||
}
|
||||
|
||||
std::string SerializeUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot) {
|
||||
std::ostringstream stream = {};
|
||||
stream << kLayoutHeader << ' ' << kLayoutVersion << '\n';
|
||||
stream << "active " << std::quoted(snapshot.workspace.activePanelId) << '\n';
|
||||
SerializeNodeRecursive(snapshot.workspace.root, stream);
|
||||
for (const UIEditorPanelSessionState& state : snapshot.session.panelStates) {
|
||||
stream << "session "
|
||||
<< std::quoted(state.panelId) << ' '
|
||||
<< (state.open ? 1 : 0) << ' '
|
||||
<< (state.visible ? 1 : 0) << '\n';
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult DeserializeUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
std::string_view serializedLayout) {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidPanelRegistry,
|
||||
"Panel registry invalid: " + registryValidation.message);
|
||||
}
|
||||
|
||||
const std::vector<LayoutLine> lines = CollectNonEmptyLines(serializedLayout);
|
||||
if (lines.empty()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::EmptyInput,
|
||||
"Serialized layout input is empty.");
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceLayoutLoadResult headerResult = ParseHeader(lines.front());
|
||||
!headerResult.IsValid()) {
|
||||
return headerResult;
|
||||
}
|
||||
|
||||
if (lines.size() < 2u) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::MissingActiveRecord,
|
||||
"Serialized layout is missing the active panel record.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot snapshot = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult activeResult =
|
||||
ParseActiveRecord(lines[1], snapshot.workspace.activePanelId);
|
||||
!activeResult.IsValid()) {
|
||||
return activeResult;
|
||||
}
|
||||
|
||||
std::size_t index = 2u;
|
||||
if (UIEditorWorkspaceLayoutLoadResult rootResult =
|
||||
ParseNodeRecursive(lines, index, snapshot.workspace.root);
|
||||
!rootResult.IsValid()) {
|
||||
return rootResult;
|
||||
}
|
||||
|
||||
snapshot.session.panelStates.clear();
|
||||
while (index < lines.size()) {
|
||||
UIEditorPanelSessionState state = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult stateResult =
|
||||
ParseSessionRecord(lines[index], state);
|
||||
!stateResult.IsValid()) {
|
||||
return stateResult;
|
||||
}
|
||||
snapshot.session.panelStates.push_back(std::move(state));
|
||||
++index;
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(snapshot.workspace);
|
||||
!workspaceValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidWorkspace,
|
||||
workspaceValidation.message);
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(panelRegistry, snapshot.workspace, snapshot.session);
|
||||
!sessionValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidWorkspaceSession,
|
||||
sessionValidation.message);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult result = {};
|
||||
result.snapshot = std::move(snapshot);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
416
new_editor/src/Shell/UIEditorWorkspaceModel.cpp
Normal file
416
new_editor/src/Shell/UIEditorWorkspaceModel.cpp
Normal file
@@ -0,0 +1,416 @@
|
||||
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
UIEditorWorkspaceValidationResult MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode code,
|
||||
std::string message) {
|
||||
UIEditorWorkspaceValidationResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsValidSplitRatio(float value) {
|
||||
return std::isfinite(value) && value > 0.0f && value < 1.0f;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor& RequirePanelDescriptor(
|
||||
const UIEditorPanelRegistry& registry,
|
||||
std::string_view panelId) {
|
||||
if (const UIEditorPanelDescriptor* descriptor = FindUIEditorPanelDescriptor(registry, panelId);
|
||||
descriptor != nullptr) {
|
||||
return *descriptor;
|
||||
}
|
||||
|
||||
static const UIEditorPanelDescriptor fallbackDescriptor = {};
|
||||
return fallbackDescriptor;
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelState* FindPanelRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
||||
return node.panel.panelId == panelId ? &node.panel : nullptr;
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (const UIEditorWorkspacePanelState* found = FindPanelRecursive(child, panelId)) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* FindNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId) {
|
||||
if (node.nodeId == nodeId) {
|
||||
return &node;
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (const UIEditorWorkspaceNode* found = FindNodeRecursive(child, nodeId)) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* FindMutableNodeRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId) {
|
||||
if (node.nodeId == nodeId) {
|
||||
return &node;
|
||||
}
|
||||
|
||||
for (UIEditorWorkspaceNode& child : node.children) {
|
||||
if (UIEditorWorkspaceNode* found = FindMutableNodeRecursive(child, nodeId)) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool TryActivateRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel:
|
||||
return node.panel.panelId == panelId;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::TabStack:
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
UIEditorWorkspaceNode& child = node.children[index];
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
child.panel.panelId == panelId) {
|
||||
node.selectedTabIndex = index;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
for (UIEditorWorkspaceNode& child : node.children) {
|
||||
if (TryActivateRecursive(child, panelId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void CollectVisiblePanelsRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view activePanelId,
|
||||
std::vector<UIEditorWorkspaceVisiblePanel>& outPanels) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel: {
|
||||
UIEditorWorkspaceVisiblePanel panel = {};
|
||||
panel.panelId = node.panel.panelId;
|
||||
panel.title = node.panel.title;
|
||||
panel.active = node.panel.panelId == activePanelId;
|
||||
panel.placeholder = node.panel.placeholder;
|
||||
outPanels.push_back(std::move(panel));
|
||||
return;
|
||||
}
|
||||
|
||||
case UIEditorWorkspaceNodeKind::TabStack:
|
||||
if (node.selectedTabIndex < node.children.size()) {
|
||||
CollectVisiblePanelsRecursive(
|
||||
node.children[node.selectedTabIndex],
|
||||
activePanelId,
|
||||
outPanels);
|
||||
}
|
||||
return;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
CollectVisiblePanelsRecursive(child, activePanelId, outPanels);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorWorkspaceValidationResult ValidateNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::unordered_set<std::string>& panelIds) {
|
||||
if (node.nodeId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::EmptyNodeId,
|
||||
"Workspace node id must not be empty.");
|
||||
}
|
||||
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel:
|
||||
if (!node.children.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::NonPanelTabChild,
|
||||
"Panel node '" + node.nodeId + "' must not contain child nodes.");
|
||||
}
|
||||
|
||||
if (node.panel.panelId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::EmptyPanelId,
|
||||
"Panel node '" + node.nodeId + "' must define a panelId.");
|
||||
}
|
||||
|
||||
if (node.panel.title.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::EmptyPanelTitle,
|
||||
"Panel node '" + node.nodeId + "' must define a title.");
|
||||
}
|
||||
|
||||
if (!panelIds.insert(node.panel.panelId).second) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::DuplicatePanelId,
|
||||
"Panel id '" + node.panel.panelId + "' is duplicated in the workspace tree.");
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
case UIEditorWorkspaceNodeKind::TabStack:
|
||||
if (node.children.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::EmptyTabStack,
|
||||
"Tab stack '" + node.nodeId + "' must contain at least one panel.");
|
||||
}
|
||||
|
||||
if (node.selectedTabIndex >= node.children.size()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::InvalidSelectedTabIndex,
|
||||
"Tab stack '" + node.nodeId + "' selectedTabIndex is out of range.");
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::NonPanelTabChild,
|
||||
"Tab stack '" + node.nodeId + "' may only contain panel leaf nodes.");
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds);
|
||||
!result.IsValid()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
if (node.children.size() != 2u) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::InvalidSplitChildCount,
|
||||
"Split node '" + node.nodeId + "' must contain exactly two child nodes.");
|
||||
}
|
||||
|
||||
if (!IsValidSplitRatio(node.splitRatio)) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::InvalidSplitRatio,
|
||||
"Split node '" + node.nodeId + "' must define a ratio in the open interval (0, 1).");
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds);
|
||||
!result.IsValid()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool AreUIEditorWorkspaceNodesEquivalent(
|
||||
const UIEditorWorkspaceNode& lhs,
|
||||
const UIEditorWorkspaceNode& rhs) {
|
||||
if (lhs.kind != rhs.kind ||
|
||||
lhs.nodeId != rhs.nodeId ||
|
||||
lhs.splitAxis != rhs.splitAxis ||
|
||||
lhs.splitRatio != rhs.splitRatio ||
|
||||
lhs.selectedTabIndex != rhs.selectedTabIndex ||
|
||||
lhs.panel.panelId != rhs.panel.panelId ||
|
||||
lhs.panel.title != rhs.panel.title ||
|
||||
lhs.panel.placeholder != rhs.panel.placeholder ||
|
||||
lhs.children.size() != rhs.children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.children.size(); ++index) {
|
||||
if (!AreUIEditorWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceModelsEquivalent(
|
||||
const UIEditorWorkspaceModel& lhs,
|
||||
const UIEditorWorkspaceModel& rhs) {
|
||||
return lhs.activePanelId == rhs.activePanelId &&
|
||||
AreUIEditorWorkspaceNodesEquivalent(lhs.root, rhs.root);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel() {
|
||||
const UIEditorPanelRegistry registry = BuildDefaultEditorShellPanelRegistry();
|
||||
const UIEditorPanelDescriptor& rootPanel =
|
||||
RequirePanelDescriptor(registry, "editor-foundation-root");
|
||||
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspacePanel(
|
||||
"editor-foundation-root-node",
|
||||
rootPanel.panelId,
|
||||
rootPanel.defaultTitle,
|
||||
rootPanel.placeholder);
|
||||
workspace.activePanelId = rootPanel.panelId;
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode BuildUIEditorWorkspacePanel(
|
||||
std::string nodeId,
|
||||
std::string panelId,
|
||||
std::string title,
|
||||
bool placeholder) {
|
||||
UIEditorWorkspaceNode node = {};
|
||||
node.kind = UIEditorWorkspaceNodeKind::Panel;
|
||||
node.nodeId = std::move(nodeId);
|
||||
node.panel.panelId = std::move(panelId);
|
||||
node.panel.title = std::move(title);
|
||||
node.panel.placeholder = placeholder;
|
||||
return node;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack(
|
||||
std::string nodeId,
|
||||
std::vector<UIEditorWorkspaceNode> panels,
|
||||
std::size_t selectedTabIndex) {
|
||||
UIEditorWorkspaceNode node = {};
|
||||
node.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
node.nodeId = std::move(nodeId);
|
||||
node.selectedTabIndex = selectedTabIndex;
|
||||
node.children = std::move(panels);
|
||||
return node;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode BuildUIEditorWorkspaceSplit(
|
||||
std::string nodeId,
|
||||
UIEditorWorkspaceSplitAxis axis,
|
||||
float splitRatio,
|
||||
UIEditorWorkspaceNode primary,
|
||||
UIEditorWorkspaceNode secondary) {
|
||||
UIEditorWorkspaceNode node = {};
|
||||
node.kind = UIEditorWorkspaceNodeKind::Split;
|
||||
node.nodeId = std::move(nodeId);
|
||||
node.splitAxis = axis;
|
||||
node.splitRatio = splitRatio;
|
||||
node.children.push_back(std::move(primary));
|
||||
node.children.push_back(std::move(secondary));
|
||||
return node;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace(
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
std::unordered_set<std::string> panelIds = {};
|
||||
UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(workspace.root, panelIds);
|
||||
if (!result.IsValid()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!workspace.activePanelId.empty()) {
|
||||
const UIEditorWorkspacePanelState* activePanel = FindUIEditorWorkspaceActivePanel(workspace);
|
||||
if (activePanel == nullptr) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode::InvalidActivePanelId,
|
||||
"Active panel id '" + workspace.activePanelId + "' is missing or hidden by the current tab selection.");
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels = {};
|
||||
CollectVisiblePanelsRecursive(workspace.root, workspace.activePanelId, visiblePanels);
|
||||
return visiblePanels;
|
||||
}
|
||||
|
||||
bool ContainsUIEditorWorkspacePanel(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string_view panelId) {
|
||||
return FindPanelRecursive(workspace.root, panelId) != nullptr;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* FindUIEditorWorkspaceNode(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string_view nodeId) {
|
||||
return FindNodeRecursive(workspace.root, nodeId);
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel(
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
if (workspace.activePanelId.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace);
|
||||
for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) {
|
||||
if (panel.panelId == workspace.activePanelId) {
|
||||
return FindPanelRecursive(workspace.root, workspace.activePanelId);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool TryActivateUIEditorWorkspacePanel(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
std::string_view panelId) {
|
||||
if (!TryActivateRecursive(workspace.root, panelId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TrySetUIEditorWorkspaceSplitRatio(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
std::string_view nodeId,
|
||||
float splitRatio) {
|
||||
if (!IsValidSplitRatio(splitRatio)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* node = FindMutableNodeRecursive(workspace.root, nodeId);
|
||||
if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (std::fabs(node->splitRatio - splitRatio) <= 0.0001f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
node->splitRatio = splitRatio;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
445
new_editor/src/Shell/UIEditorWorkspaceSession.cpp
Normal file
445
new_editor/src/Shell/UIEditorWorkspaceSession.cpp
Normal file
@@ -0,0 +1,445 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
UIEditorWorkspaceSessionValidationResult MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode code,
|
||||
std::string message) {
|
||||
UIEditorWorkspaceSessionValidationResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
UIEditorPanelSessionState* FindMutablePanelSessionState(
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
for (UIEditorPanelSessionState& state : session.panelStates) {
|
||||
if (state.panelId == panelId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* FindPanelDescriptor(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
std::string_view panelId) {
|
||||
return FindUIEditorPanelDescriptor(panelRegistry, panelId);
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelState* FindPanelRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
||||
return node.panel.panelId == panelId ? &node.panel : nullptr;
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (const UIEditorWorkspacePanelState* found = FindPanelRecursive(child, panelId)) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void CollectWorkspacePanelIdsRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::vector<std::string>& outPanelIds) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
||||
outPanelIds.push_back(node.panel.panelId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
CollectWorkspacePanelIdsRecursive(child, outPanelIds);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId);
|
||||
return state != nullptr && state->open && state->visible;
|
||||
}
|
||||
|
||||
bool IsPanelSelectable(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
return !panelId.empty() &&
|
||||
IsPanelOpenAndVisible(session, panelId) &&
|
||||
ContainsUIEditorWorkspacePanel(workspace, panelId);
|
||||
}
|
||||
|
||||
std::size_t ResolveVisibleTabIndex(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack || node.children.empty()) {
|
||||
return node.selectedTabIndex;
|
||||
}
|
||||
|
||||
if (node.selectedTabIndex < node.children.size()) {
|
||||
const UIEditorWorkspaceNode& selectedChild = node.children[node.selectedTabIndex];
|
||||
if (selectedChild.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisible(session, selectedChild.panel.panelId)) {
|
||||
return node.selectedTabIndex;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = node.children[index];
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisible(session, child.panel.panelId)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return node.children.size();
|
||||
}
|
||||
|
||||
void CollectVisiblePanelsRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view activePanelId,
|
||||
std::vector<UIEditorWorkspaceVisiblePanel>& outPanels) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel: {
|
||||
if (!IsPanelOpenAndVisible(session, node.panel.panelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceVisiblePanel panel = {};
|
||||
panel.panelId = node.panel.panelId;
|
||||
panel.title = node.panel.title;
|
||||
panel.active = node.panel.panelId == activePanelId;
|
||||
panel.placeholder = node.panel.placeholder;
|
||||
outPanels.push_back(std::move(panel));
|
||||
return;
|
||||
}
|
||||
|
||||
case UIEditorWorkspaceNodeKind::TabStack: {
|
||||
const std::size_t resolvedIndex = ResolveVisibleTabIndex(node, session);
|
||||
if (resolvedIndex < node.children.size()) {
|
||||
CollectVisiblePanelsRecursive(
|
||||
node.children[resolvedIndex],
|
||||
session,
|
||||
activePanelId,
|
||||
outPanels);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
CollectVisiblePanelsRecursive(child, session, activePanelId, outPanels);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void NormalizeSessionStatesAgainstRegistry(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceSession& session) {
|
||||
for (UIEditorPanelSessionState& state : session.panelStates) {
|
||||
if (!state.open) {
|
||||
state.visible = false;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, state.panelId);
|
||||
if (descriptor == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!descriptor->canClose) {
|
||||
state.open = true;
|
||||
state.visible = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!descriptor->canHide && state.open) {
|
||||
state.visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NormalizeWorkspaceSession(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view preferredActivePanelId) {
|
||||
NormalizeSessionStatesAgainstRegistry(panelRegistry, session);
|
||||
|
||||
std::string targetActivePanelId = {};
|
||||
if (IsPanelSelectable(workspace, session, preferredActivePanelId)) {
|
||||
targetActivePanelId = std::string(preferredActivePanelId);
|
||||
} else if (IsPanelSelectable(workspace, session, workspace.activePanelId)) {
|
||||
targetActivePanelId = workspace.activePanelId;
|
||||
} else {
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
if (!visiblePanels.empty()) {
|
||||
targetActivePanelId = visiblePanels.front().panelId;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetActivePanelId.empty()) {
|
||||
workspace.activePanelId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
TryActivateUIEditorWorkspacePanel(workspace, targetActivePanelId);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWorkspaceSession BuildDefaultUIEditorWorkspaceSession(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
UIEditorWorkspaceSession session = {};
|
||||
std::vector<std::string> panelIds = {};
|
||||
CollectWorkspacePanelIdsRecursive(workspace.root, panelIds);
|
||||
session.panelStates.reserve(panelIds.size());
|
||||
for (std::string& panelId : panelIds) {
|
||||
UIEditorPanelSessionState state = {};
|
||||
state.panelId = std::move(panelId);
|
||||
session.panelStates.push_back(std::move(state));
|
||||
}
|
||||
|
||||
NormalizeSessionStatesAgainstRegistry(panelRegistry, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
const UIEditorPanelSessionState* FindUIEditorPanelSessionState(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorPanelSessionState& state : session.panelStates) {
|
||||
if (state.panelId == panelId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSessionValidationResult ValidateUIEditorWorkspaceSession(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
std::vector<std::string> workspacePanelIds = {};
|
||||
CollectWorkspacePanelIdsRecursive(workspace.root, workspacePanelIds);
|
||||
|
||||
std::unordered_set<std::string> expectedPanelIds = {};
|
||||
expectedPanelIds.insert(workspacePanelIds.begin(), workspacePanelIds.end());
|
||||
|
||||
std::unordered_set<std::string> seenPanelIds = {};
|
||||
for (const UIEditorPanelSessionState& state : session.panelStates) {
|
||||
if (!seenPanelIds.insert(state.panelId).second) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::DuplicatePanelId,
|
||||
"Workspace session contains duplicated panel state '" + state.panelId + "'.");
|
||||
}
|
||||
|
||||
if (!expectedPanelIds.contains(state.panelId)) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::UnknownPanelId,
|
||||
"Workspace session state '" + state.panelId + "' is not present in the workspace tree.");
|
||||
}
|
||||
|
||||
if (!state.open && state.visible) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::ClosedPanelVisible,
|
||||
"Workspace session state '" + state.panelId + "' cannot be visible while closed.");
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, state.panelId);
|
||||
if (descriptor == nullptr) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::UnknownPanelId,
|
||||
"Workspace session state '" + state.panelId + "' is missing from the panel registry.");
|
||||
}
|
||||
|
||||
if (!descriptor->canClose && !state.open) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::NonCloseablePanelClosed,
|
||||
"Workspace session state '" + state.panelId + "' cannot be closed.");
|
||||
}
|
||||
|
||||
if (!descriptor->canHide && state.open && !state.visible) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::NonHideablePanelHidden,
|
||||
"Workspace session state '" + state.panelId + "' cannot be hidden.");
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& panelId : workspacePanelIds) {
|
||||
if (!seenPanelIds.contains(panelId)) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::MissingPanelState,
|
||||
"Workspace panel '" + panelId + "' is missing from the workspace session.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!workspace.activePanelId.empty() &&
|
||||
FindUIEditorWorkspaceActivePanel(workspace, session) == nullptr) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::InvalidActivePanelId,
|
||||
"Active panel id '" + workspace.activePanelId + "' is missing, closed, or hidden.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceSessionsEquivalent(
|
||||
const UIEditorWorkspaceSession& lhs,
|
||||
const UIEditorWorkspaceSession& rhs) {
|
||||
if (lhs.panelStates.size() != rhs.panelStates.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.panelStates.size(); ++index) {
|
||||
const UIEditorPanelSessionState& lhsState = lhs.panelStates[index];
|
||||
const UIEditorPanelSessionState& rhsState = rhs.panelStates[index];
|
||||
if (lhsState.panelId != rhsState.panelId ||
|
||||
lhsState.open != rhsState.open ||
|
||||
lhsState.visible != rhsState.visible) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels = {};
|
||||
CollectVisiblePanelsRecursive(workspace.root, session, workspace.activePanelId, visiblePanels);
|
||||
return visiblePanels;
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (workspace.activePanelId.empty() ||
|
||||
!IsPanelOpenAndVisible(session, workspace.activePanelId)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) {
|
||||
if (panel.panelId == workspace.activePanelId) {
|
||||
return FindPanelRecursive(workspace.root, workspace.activePanelId);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool TryOpenUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorPanelSessionState* state = FindMutablePanelSessionState(session, panelId);
|
||||
if (state == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->open = true;
|
||||
state->visible = true;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryCloseUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorPanelSessionState* state = FindMutablePanelSessionState(session, panelId);
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, panelId);
|
||||
if (state == nullptr || descriptor == nullptr || !descriptor->canClose) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->open = false;
|
||||
state->visible = false;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, {});
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryShowUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorPanelSessionState* state = FindMutablePanelSessionState(session, panelId);
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, panelId);
|
||||
if (state == nullptr || descriptor == nullptr || !state->open || !descriptor->canHide) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->visible = true;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryHideUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorPanelSessionState* state = FindMutablePanelSessionState(session, panelId);
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, panelId);
|
||||
if (state == nullptr || descriptor == nullptr || !state->open || !descriptor->canHide) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->visible = false;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, {});
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryActivateUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
if (!IsPanelSelectable(workspace, session, panelId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
Reference in New Issue
Block a user