yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dock_tree_json.cc
Go to the documentation of this file.
2
3#include <memory>
4#include <string>
5#include <utility>
6#include <vector>
7
8#include "absl/status/status.h"
9#include "absl/status/statusor.h"
10#include "absl/strings/str_cat.h"
11
12namespace yaze {
13namespace editor {
14namespace layout_designer {
15
16namespace {
17
18// The schema version this code emits and parses against. Bump in
19// lockstep with format-changing edits; legacy JSON without a schema
20// field is still accepted (treated as v1 → fresh ids on parse).
21constexpr std::uint64_t kCurrentDockTreeSchemaVersion = 2;
22
24 switch (d) {
26 return "left";
28 return "right";
30 return "up";
32 return "down";
33 }
34 return "left";
35}
36
37absl::StatusOr<SplitDirection> SplitDirectionFromString(const std::string& s) {
38 if (s == "left")
40 if (s == "right")
42 if (s == "up")
44 if (s == "down")
46 return absl::InvalidArgumentError(
47 absl::StrCat("unknown split direction: '", s, "'"));
48}
49
50nlohmann::json PanelToJson(const PanelEntry& p) {
51 nlohmann::json j;
52 j["panel_id"] = p.panel_id;
53 j["display_name"] = p.display_name;
54 j["icon"] = p.icon;
55 return j;
56}
57
58absl::StatusOr<PanelEntry> PanelFromJson(const nlohmann::json& j) {
59 if (!j.is_object()) {
60 return absl::InvalidArgumentError("panel entry must be a JSON object");
61 }
62 if (!j.contains("panel_id") || !j.at("panel_id").is_string()) {
63 return absl::InvalidArgumentError("panel entry missing string panel_id");
64 }
65 PanelEntry p;
66 p.panel_id = j.at("panel_id").get<std::string>();
67 p.display_name = j.value("display_name", "");
68 p.icon = j.value("icon", "");
69 return p;
70}
71
72nlohmann::json NodeToJson(const DockNode& node) {
73 nlohmann::json j;
74 // Schema v2: every real node carries a `DockNodeId`. The default-
75 // constructed sentinel (kInvalidDockNodeId) doesn't survive serialization
76 // — that only happens for raw `DockNode{}` instances which the live tree
77 // never holds. Emit the id unconditionally for non-zero values so v2
78 // round-trips faithfully; v1-loaded trees that didn't carry ids will
79 // have been re-allocated at parse time and emit cleanly.
80 if (node.id != kInvalidDockNodeId) {
81 j["id"] = node.id;
82 }
83 if (node.type == DockNode::Type::kLeaf) {
84 j["type"] = "leaf";
85 j["active_tab_index"] = node.active_tab_index;
86 nlohmann::json panels = nlohmann::json::array();
87 for (const auto& p : node.panels) {
88 panels.push_back(PanelToJson(p));
89 }
90 j["panels"] = std::move(panels);
91 } else {
92 j["type"] = "split";
93 j["direction"] = SplitDirectionToString(node.split_direction);
94 j["ratio"] = node.split_ratio;
95 j["child_a"] = node.child_a ? NodeToJson(*node.child_a) : nlohmann::json();
96 j["child_b"] = node.child_b ? NodeToJson(*node.child_b) : nlohmann::json();
97 }
98 return j;
99}
100
101// Read the `id` field if present; otherwise allocate a fresh one. Either
102// way, the result is non-zero so live trees never carry the sentinel.
103DockNodeId ResolveNodeId(const nlohmann::json& j) {
104 if (j.contains("id") && j.at("id").is_number_unsigned()) {
105 const auto id = j.at("id").get<DockNodeId>();
106 if (id != kInvalidDockNodeId) {
107 // Bump the global counter so subsequent allocations don't collide
108 // with a future load of the same persisted tree.
110 return id;
111 }
112 }
114}
115
116absl::StatusOr<std::unique_ptr<DockNode>> NodeFromJson(
117 const nlohmann::json& j) {
118 if (!j.is_object()) {
119 return absl::InvalidArgumentError("dock node must be a JSON object");
120 }
121 const std::string type = j.value("type", "");
122 if (type == "leaf") {
123 auto node = std::make_unique<DockNode>();
124 node->id = ResolveNodeId(j);
125 node->type = DockNode::Type::kLeaf;
126 node->active_tab_index = j.value("active_tab_index", 0);
127 if (j.contains("panels")) {
128 const auto& panels = j.at("panels");
129 if (!panels.is_array()) {
130 return absl::InvalidArgumentError("leaf 'panels' must be an array");
131 }
132 for (const auto& p : panels) {
133 auto entry = PanelFromJson(p);
134 if (!entry.ok())
135 return entry.status();
136 node->panels.push_back(std::move(*entry));
137 }
138 }
139 return node;
140 }
141 if (type == "split") {
142 auto node = std::make_unique<DockNode>();
143 node->id = ResolveNodeId(j);
144 node->type = DockNode::Type::kSplit;
145 const std::string dir_str = j.value("direction", "left");
146 auto dir = SplitDirectionFromString(dir_str);
147 if (!dir.ok())
148 return dir.status();
149 node->split_direction = *dir;
150 node->split_ratio = j.value("ratio", 0.5f);
151 if (!j.contains("child_a") || !j.contains("child_b")) {
152 return absl::InvalidArgumentError("split node missing child_a/child_b");
153 }
154 auto a = NodeFromJson(j.at("child_a"));
155 if (!a.ok())
156 return a.status();
157 auto b = NodeFromJson(j.at("child_b"));
158 if (!b.ok())
159 return b.status();
160 node->child_a = std::move(*a);
161 node->child_b = std::move(*b);
162 return node;
163 }
164 return absl::InvalidArgumentError(
165 absl::StrCat("unknown dock node type: '", type, "'"));
166}
167
168} // namespace
169
170nlohmann::json DockTreeToJson(const DockTree& tree) {
171 nlohmann::json j;
172 // The writer is the authority on schema version: every saved file
173 // carries the current schema regardless of what `tree.schema_version`
174 // happens to hold. This keeps legacy v1 trees from re-saving as a
175 // self-contradictory document (v1 header, v2 body). The parser
176 // normalizes the in-memory representation to the same constant so
177 // round-trips are stable.
178 j["schema_version"] = kCurrentDockTreeSchemaVersion;
179 j["name"] = tree.name;
180 j["description"] = tree.description;
181 j["root"] = tree.root ? NodeToJson(*tree.root)
182 : NodeToJson(DockNode{}); // empty leaf
183 return j;
184}
185
186absl::StatusOr<DockTree> DockTreeFromJson(const nlohmann::json& j) {
187 if (!j.is_object()) {
188 return absl::InvalidArgumentError("dock tree must be a JSON object");
189 }
190 // Read the on-disk version for diagnostics / future migration logic
191 // hooks, but the in-memory tree always lands at the current schema
192 // because every node ends up with a real id (the parser allocates
193 // fresh ones for v1 input). Keeping `tree.schema_version` synced to
194 // the in-memory shape avoids the v1-header / v2-body class of bug.
195 (void)j.value("schema_version", 1ULL);
196
197 DockTree tree;
198 tree.schema_version = kCurrentDockTreeSchemaVersion;
199 tree.name = j.value("name", "");
200 tree.description = j.value("description", "");
201 if (j.contains("root")) {
202 auto root = NodeFromJson(j.at("root"));
203 if (!root.ok())
204 return root.status();
205 tree.root = std::move(*root);
206 }
207 // If "root" is missing, the default-constructed empty leaf stays —
208 // its id was already allocated by `DockTree::DockTree()`'s call to
209 // `MakeLeaf({})`.
210 return tree;
211}
212
213} // namespace layout_designer
214} // namespace editor
215} // namespace yaze
absl::StatusOr< SplitDirection > SplitDirectionFromString(const std::string &s)
absl::StatusOr< PanelEntry > PanelFromJson(const nlohmann::json &j)
absl::StatusOr< std::unique_ptr< DockNode > > NodeFromJson(const nlohmann::json &j)
absl::StatusOr< DockTree > DockTreeFromJson(const nlohmann::json &j)
nlohmann::json DockTreeToJson(const DockTree &tree)
constexpr DockNodeId kInvalidDockNodeId
Definition dock_tree.h:22
Represents a dock node in the layout tree.
Definition dock_tree.h:61
std::unique_ptr< DockNode > child_a
Definition dock_tree.h:78
std::unique_ptr< DockNode > child_b
Definition dock_tree.h:79
std::vector< PanelEntry > panels
Definition dock_tree.h:72
std::unique_ptr< DockNode > root
Definition dock_tree.h:115