#include #include #include #include #include #include #include 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(outStream >> outTag); } UIEditorWorkspaceLayoutLoadResult ParseNodeRecursive( const std::vector& 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::max_digits10) << node.splitRatio << '\n'; for (const UIEditorWorkspaceNode& child : node.children) { SerializeNodeRecursive(child, stream); } return; } } std::vector CollectNonEmptyLines(std::string_view serializedLayout) { std::vector 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& 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& 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& 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 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