feat(xcui): advance core and editor validation flow
This commit is contained in:
@@ -13,7 +13,10 @@ set(NEW_EDITOR_RESOURCE_FILES
|
||||
|
||||
add_library(XCNewEditorLib STATIC
|
||||
src/editor/EditorShellAsset.cpp
|
||||
src/editor/UIEditorPanelRegistry.cpp
|
||||
src/editor/UIEditorWorkspaceController.cpp
|
||||
src/editor/UIEditorWorkspaceModel.cpp
|
||||
src/editor/UIEditorWorkspaceSession.cpp
|
||||
src/Widgets/UIEditorCollectionPrimitives.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::NewEditor {
|
||||
|
||||
enum class UIEditorPanelPresentationKind : std::uint8_t {
|
||||
Placeholder = 0
|
||||
};
|
||||
|
||||
struct UIEditorPanelDescriptor {
|
||||
std::string panelId = {};
|
||||
std::string defaultTitle = {};
|
||||
UIEditorPanelPresentationKind presentationKind = UIEditorPanelPresentationKind::Placeholder;
|
||||
bool placeholder = true;
|
||||
bool canHide = true;
|
||||
bool canClose = true;
|
||||
};
|
||||
|
||||
struct UIEditorPanelRegistry {
|
||||
std::vector<UIEditorPanelDescriptor> panels = {};
|
||||
};
|
||||
|
||||
enum class UIEditorPanelRegistryValidationCode : std::uint8_t {
|
||||
None = 0,
|
||||
EmptyPanelId,
|
||||
EmptyDefaultTitle,
|
||||
DuplicatePanelId
|
||||
};
|
||||
|
||||
struct UIEditorPanelRegistryValidationResult {
|
||||
UIEditorPanelRegistryValidationCode code = UIEditorPanelRegistryValidationCode::None;
|
||||
std::string message = {};
|
||||
|
||||
[[nodiscard]] bool IsValid() const {
|
||||
return code == UIEditorPanelRegistryValidationCode::None;
|
||||
}
|
||||
};
|
||||
|
||||
UIEditorPanelRegistry BuildDefaultEditorShellPanelRegistry();
|
||||
|
||||
const UIEditorPanelDescriptor* FindUIEditorPanelDescriptor(
|
||||
const UIEditorPanelRegistry& registry,
|
||||
std::string_view panelId);
|
||||
|
||||
UIEditorPanelRegistryValidationResult ValidateUIEditorPanelRegistry(
|
||||
const UIEditorPanelRegistry& registry);
|
||||
|
||||
} // namespace XCEngine::NewEditor
|
||||
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCNewEditor/Editor/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::NewEditor {
|
||||
|
||||
enum class UIEditorWorkspaceCommandKind : std::uint8_t {
|
||||
OpenPanel = 0,
|
||||
ClosePanel,
|
||||
ShowPanel,
|
||||
HidePanel,
|
||||
ActivatePanel,
|
||||
ResetWorkspace
|
||||
};
|
||||
|
||||
enum class UIEditorWorkspaceCommandStatus : std::uint8_t {
|
||||
Changed = 0,
|
||||
NoOp,
|
||||
Rejected
|
||||
};
|
||||
|
||||
struct UIEditorWorkspaceCommand {
|
||||
UIEditorWorkspaceCommandKind kind = UIEditorWorkspaceCommandKind::ActivatePanel;
|
||||
std::string panelId = {};
|
||||
};
|
||||
|
||||
struct UIEditorWorkspaceCommandResult {
|
||||
UIEditorWorkspaceCommandKind kind = UIEditorWorkspaceCommandKind::ActivatePanel;
|
||||
UIEditorWorkspaceCommandStatus status = UIEditorWorkspaceCommandStatus::Rejected;
|
||||
std::string panelId = {};
|
||||
std::string message = {};
|
||||
std::string activePanelId = {};
|
||||
std::vector<std::string> visiblePanelIds = {};
|
||||
};
|
||||
|
||||
enum class UIEditorWorkspaceControllerValidationCode : std::uint8_t {
|
||||
None = 0,
|
||||
InvalidPanelRegistry,
|
||||
InvalidWorkspace,
|
||||
InvalidWorkspaceSession
|
||||
};
|
||||
|
||||
struct UIEditorWorkspaceControllerValidationResult {
|
||||
UIEditorWorkspaceControllerValidationCode code =
|
||||
UIEditorWorkspaceControllerValidationCode::None;
|
||||
std::string message = {};
|
||||
|
||||
[[nodiscard]] bool IsValid() const {
|
||||
return code == UIEditorWorkspaceControllerValidationCode::None;
|
||||
}
|
||||
};
|
||||
|
||||
std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind);
|
||||
std::string_view GetUIEditorWorkspaceCommandStatusName(UIEditorWorkspaceCommandStatus status);
|
||||
|
||||
class UIEditorWorkspaceController {
|
||||
public:
|
||||
UIEditorWorkspaceController() = default;
|
||||
UIEditorWorkspaceController(
|
||||
UIEditorPanelRegistry panelRegistry,
|
||||
UIEditorWorkspaceModel workspace,
|
||||
UIEditorWorkspaceSession session);
|
||||
|
||||
const UIEditorPanelRegistry& GetPanelRegistry() const {
|
||||
return m_panelRegistry;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel& GetWorkspace() const {
|
||||
return m_workspace;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSession& GetSession() const {
|
||||
return m_session;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceControllerValidationResult ValidateState() const;
|
||||
UIEditorWorkspaceCommandResult Dispatch(const UIEditorWorkspaceCommand& command);
|
||||
|
||||
private:
|
||||
UIEditorWorkspaceCommandResult BuildResult(
|
||||
const UIEditorWorkspaceCommand& command,
|
||||
UIEditorWorkspaceCommandStatus status,
|
||||
std::string message) const;
|
||||
|
||||
UIEditorWorkspaceCommandResult FinalizeMutation(
|
||||
const UIEditorWorkspaceCommand& command,
|
||||
bool changed,
|
||||
std::string changedMessage,
|
||||
std::string unexpectedFailureMessage,
|
||||
const UIEditorWorkspaceModel& previousWorkspace,
|
||||
const UIEditorWorkspaceSession& previousSession);
|
||||
|
||||
const UIEditorPanelDescriptor* FindPanelDescriptor(std::string_view panelId) const;
|
||||
|
||||
UIEditorPanelRegistry m_panelRegistry = {};
|
||||
UIEditorWorkspaceModel m_baselineWorkspace = {};
|
||||
UIEditorWorkspaceSession m_baselineSession = {};
|
||||
UIEditorWorkspaceModel m_workspace = {};
|
||||
UIEditorWorkspaceSession m_session = {};
|
||||
};
|
||||
|
||||
UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace);
|
||||
|
||||
} // namespace XCEngine::NewEditor
|
||||
@@ -70,6 +70,8 @@ struct UIEditorWorkspaceVisiblePanel {
|
||||
bool placeholder = false;
|
||||
};
|
||||
|
||||
UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel();
|
||||
|
||||
UIEditorWorkspaceNode BuildUIEditorWorkspacePanel(
|
||||
std::string nodeId,
|
||||
std::string panelId,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCNewEditor/Editor/UIEditorPanelRegistry.h>
|
||||
#include <XCNewEditor/Editor/UIEditorWorkspaceModel.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::NewEditor {
|
||||
|
||||
struct UIEditorPanelSessionState {
|
||||
std::string panelId = {};
|
||||
bool open = true;
|
||||
bool visible = true;
|
||||
};
|
||||
|
||||
struct UIEditorWorkspaceSession {
|
||||
std::vector<UIEditorPanelSessionState> panelStates = {};
|
||||
};
|
||||
|
||||
enum class UIEditorWorkspaceSessionValidationCode : std::uint8_t {
|
||||
None = 0,
|
||||
MissingPanelState,
|
||||
UnknownPanelId,
|
||||
DuplicatePanelId,
|
||||
ClosedPanelVisible,
|
||||
NonHideablePanelHidden,
|
||||
NonCloseablePanelClosed,
|
||||
InvalidActivePanelId
|
||||
};
|
||||
|
||||
struct UIEditorWorkspaceSessionValidationResult {
|
||||
UIEditorWorkspaceSessionValidationCode code = UIEditorWorkspaceSessionValidationCode::None;
|
||||
std::string message = {};
|
||||
|
||||
[[nodiscard]] bool IsValid() const {
|
||||
return code == UIEditorWorkspaceSessionValidationCode::None;
|
||||
}
|
||||
};
|
||||
|
||||
UIEditorWorkspaceSession BuildDefaultUIEditorWorkspaceSession(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace);
|
||||
|
||||
const UIEditorPanelSessionState* FindUIEditorPanelSessionState(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
|
||||
UIEditorWorkspaceSessionValidationResult ValidateUIEditorWorkspaceSession(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session);
|
||||
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session);
|
||||
|
||||
const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session);
|
||||
|
||||
bool TryOpenUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
|
||||
bool TryCloseUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
|
||||
bool TryShowUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
|
||||
bool TryHideUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
|
||||
bool TryActivateUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
|
||||
} // namespace XCEngine::NewEditor
|
||||
@@ -82,6 +82,17 @@ std::string FormatPoint(const UIPoint& point) {
|
||||
return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")";
|
||||
}
|
||||
|
||||
void AppendErrorMessage(std::string& target, const std::string& message) {
|
||||
if (message.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.empty()) {
|
||||
target += " | ";
|
||||
}
|
||||
target += message;
|
||||
}
|
||||
|
||||
std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) {
|
||||
switch (wParam) {
|
||||
case 'A': return static_cast<std::int32_t>(KeyCode::A);
|
||||
@@ -392,9 +403,19 @@ bool Application::LoadStructuredScreen(const char* triggerReason) {
|
||||
m_screenAsset.themePath = m_shellAssetDefinition.themePath.string();
|
||||
|
||||
const bool loaded = m_screenPlayer.Load(m_screenAsset);
|
||||
const EditorShellAssetValidationResult shellAssetValidation =
|
||||
ValidateEditorShellAsset(m_shellAssetDefinition);
|
||||
m_useStructuredScreen = loaded;
|
||||
m_runtimeStatus = loaded ? "XCUI Editor Shell" : "Editor Shell | Load Error";
|
||||
m_runtimeError = loaded ? std::string() : m_screenPlayer.GetLastError();
|
||||
m_runtimeError.clear();
|
||||
if (!loaded) {
|
||||
AppendErrorMessage(m_runtimeError, m_screenPlayer.GetLastError());
|
||||
}
|
||||
if (!shellAssetValidation.IsValid()) {
|
||||
AppendErrorMessage(
|
||||
m_runtimeError,
|
||||
"Editor shell asset invalid: " + shellAssetValidation.message);
|
||||
}
|
||||
RebuildTrackedFileStates();
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,106 @@
|
||||
#include "EditorShellAsset.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::NewEditor {
|
||||
|
||||
namespace {
|
||||
|
||||
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 {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot) {
|
||||
EditorShellAsset asset = {};
|
||||
asset.documentPath = (repoRoot / "new_editor/ui/views/editor_shell.xcui").lexically_normal();
|
||||
asset.themePath = (repoRoot / "new_editor/ui/themes/editor_shell.xctheme").lexically_normal();
|
||||
asset.captureRootPath = (repoRoot / "new_editor/captures").lexically_normal();
|
||||
asset.panelRegistry = BuildDefaultEditorShellPanelRegistry();
|
||||
asset.workspace = BuildDefaultEditorShellWorkspaceModel();
|
||||
asset.workspaceSession = BuildDefaultUIEditorWorkspaceSession(asset.panelRegistry, asset.workspace);
|
||||
return asset;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace XCEngine::NewEditor
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCNewEditor/Editor/UIEditorPanelRegistry.h>
|
||||
#include <XCNewEditor/Editor/UIEditorWorkspaceModel.h>
|
||||
#include <XCNewEditor/Editor/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
@@ -10,8 +15,31 @@ struct EditorShellAsset {
|
||||
std::filesystem::path documentPath = {};
|
||||
std::filesystem::path themePath = {};
|
||||
std::filesystem::path captureRootPath = {};
|
||||
UIEditorPanelRegistry panelRegistry = {};
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
UIEditorWorkspaceSession workspaceSession = {};
|
||||
};
|
||||
|
||||
enum class EditorShellAssetValidationCode : std::uint8_t {
|
||||
None = 0,
|
||||
InvalidPanelRegistry,
|
||||
InvalidWorkspace,
|
||||
InvalidWorkspaceSession,
|
||||
MissingPanelDescriptor,
|
||||
PanelTitleMismatch,
|
||||
PanelPlaceholderMismatch
|
||||
};
|
||||
|
||||
struct EditorShellAssetValidationResult {
|
||||
EditorShellAssetValidationCode code = EditorShellAssetValidationCode::None;
|
||||
std::string message = {};
|
||||
|
||||
[[nodiscard]] bool IsValid() const {
|
||||
return code == EditorShellAssetValidationCode::None;
|
||||
}
|
||||
};
|
||||
|
||||
EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot);
|
||||
EditorShellAssetValidationResult ValidateEditorShellAsset(const EditorShellAsset& asset);
|
||||
|
||||
} // namespace XCEngine::NewEditor
|
||||
|
||||
74
new_editor/src/editor/UIEditorPanelRegistry.cpp
Normal file
74
new_editor/src/editor/UIEditorPanelRegistry.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#include <XCNewEditor/Editor/UIEditorPanelRegistry.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::NewEditor {
|
||||
|
||||
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::NewEditor
|
||||
357
new_editor/src/editor/UIEditorWorkspaceController.cpp
Normal file
357
new_editor/src/editor/UIEditorWorkspaceController.cpp
Normal file
@@ -0,0 +1,357 @@
|
||||
#include <XCNewEditor/Editor/UIEditorWorkspaceController.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::NewEditor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool AreWorkspaceNodesEquivalent(
|
||||
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 (!AreWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AreWorkspaceModelsEquivalent(
|
||||
const UIEditorWorkspaceModel& lhs,
|
||||
const UIEditorWorkspaceModel& rhs) {
|
||||
return lhs.activePanelId == rhs.activePanelId &&
|
||||
AreWorkspaceNodesEquivalent(lhs.root, rhs.root);
|
||||
}
|
||||
|
||||
bool AreWorkspaceSessionsEquivalent(
|
||||
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<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";
|
||||
}
|
||||
|
||||
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 {};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 (AreWorkspaceModelsEquivalent(m_workspace, m_baselineWorkspace) &&
|
||||
AreWorkspaceSessionsEquivalent(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::NewEditor
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <XCNewEditor/Editor/UIEditorPanelRegistry.h>
|
||||
#include <XCNewEditor/Editor/UIEditorWorkspaceModel.h>
|
||||
|
||||
#include <cmath>
|
||||
@@ -21,6 +22,18 @@ 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) {
|
||||
@@ -192,6 +205,21 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive(
|
||||
|
||||
} // namespace
|
||||
|
||||
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,
|
||||
|
||||
476
new_editor/src/editor/UIEditorWorkspaceSession.cpp
Normal file
476
new_editor/src/editor/UIEditorWorkspaceSession.cpp
Normal file
@@ -0,0 +1,476 @@
|
||||
#include <XCNewEditor/Editor/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::NewEditor {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
bool AreWorkspaceNodesEquivalent(
|
||||
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 (!AreWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AreWorkspaceModelsEquivalent(
|
||||
const UIEditorWorkspaceModel& lhs,
|
||||
const UIEditorWorkspaceModel& rhs) {
|
||||
return lhs.activePanelId == rhs.activePanelId &&
|
||||
AreWorkspaceNodesEquivalent(lhs.root, rhs.root);
|
||||
}
|
||||
|
||||
bool AreWorkspaceSessionsEquivalent(
|
||||
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;
|
||||
}
|
||||
|
||||
} // 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 {};
|
||||
}
|
||||
|
||||
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 !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(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 !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(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 !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(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 !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(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 !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::NewEditor
|
||||
@@ -1,15 +1,7 @@
|
||||
<Theme name="EditorShellTheme">
|
||||
<Tokens>
|
||||
<Color name="color.bg.workspace" value="#1C1C1C" />
|
||||
<Color name="color.bg.panel" value="#292929" />
|
||||
<Color name="color.bg.accent" value="#3A3A3A" />
|
||||
<Color name="color.bg.selection" value="#4A4A4A" />
|
||||
<Color name="color.text.primary" value="#EEEEEE" />
|
||||
<Color name="color.text.muted" value="#B0B0B0" />
|
||||
<Spacing name="space.panel" value="12" />
|
||||
<Spacing name="space.shell" value="18" />
|
||||
<Radius name="radius.panel" value="10" />
|
||||
<Radius name="radius.control" value="8" />
|
||||
<Color name="color.bg.workspace" value="#16181C" />
|
||||
<Spacing name="space.shell" value="14" />
|
||||
</Tokens>
|
||||
|
||||
<Widgets>
|
||||
@@ -17,16 +9,5 @@
|
||||
<Property name="background" value="color.bg.workspace" />
|
||||
<Property name="padding" value="space.shell" />
|
||||
</Widget>
|
||||
|
||||
<Widget type="Card" style="EditorPanel">
|
||||
<Property name="background" value="color.bg.panel" />
|
||||
<Property name="radius" value="radius.panel" />
|
||||
<Property name="padding" value="space.panel" />
|
||||
</Widget>
|
||||
|
||||
<Widget type="Button" style="EditorChip">
|
||||
<Property name="background" value="color.bg.selection" />
|
||||
<Property name="radius" value="radius.control" />
|
||||
</Widget>
|
||||
</Widgets>
|
||||
</Theme>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<View
|
||||
name="NewEditorShell"
|
||||
theme="../themes/editor_shell.xctheme">
|
||||
theme="../themes/editor_shell.xctheme"
|
||||
style="EditorWorkspace">
|
||||
<Column
|
||||
id="editor-shell-root"
|
||||
id="editor-foundation-root"
|
||||
width="fill"
|
||||
height="fill" />
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user