Files
XCEngine/new_editor/src/Shell/UIEditorWorkspaceLayoutPersistence.cpp

500 lines
16 KiB
C++

#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 = CanonicalizeUIEditorWorkspaceModel(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.workspace = CanonicalizeUIEditorWorkspaceModel(std::move(snapshot.workspace));
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