Refactor XCUI editor module layout

This commit is contained in:
2026-04-10 00:41:28 +08:00
parent 4b47764f26
commit 02a0e626fe
263 changed files with 12396 additions and 7592 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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