载入中...
搜索中...
未找到
UiDocumentSerialization.cpp
浏览该文件的文档.
2
3#include <cmath>
4#include <set>
5#include <utility>
6
7namespace eve::ui_editing {
8namespace {
9
10const EditorValue* field(const EditorValue& value, const char* key) {
11 const auto* object = value.getIf<EditorValue::Object>();
12 if (!object) return nullptr;
13 const auto found = object->find(key);
14 return found == object->end() ? nullptr : &found->second;
15}
16
17const double* numberField(const EditorValue& value, const char* key) {
18 const EditorValue* entry = field(value, key);
19 return entry ? entry->getIf<double>() : nullptr;
20}
21
22} // namespace
23
24EditorValue UiDocumentTarget::layoutValue(const UiLayoutValue& layout) {
26 value["x"] = layout.x;
27 value["y"] = layout.y;
28 value["width"] = layout.width;
29 value["height"] = layout.height;
30 value["anchorX"] = layout.anchorX;
31 value["anchorY"] = layout.anchorY;
32 value["pivotX"] = layout.pivotX;
33 value["pivotY"] = layout.pivotY;
34 return EditorValue(std::move(value));
35}
36
37Result<UiLayoutValue> UiDocumentTarget::parseLayout(const EditorValue& value) {
38 const double* x = numberField(value, "x");
39 const double* y = numberField(value, "y");
40 const double* width = numberField(value, "width");
41 const double* height = numberField(value, "height");
42 const double* anchorX = numberField(value, "anchorX");
43 const double* anchorY = numberField(value, "anchorY");
44 const double* pivotX = numberField(value, "pivotX");
45 const double* pivotY = numberField(value, "pivotY");
46 if (!x || !y || !width || !height || !anchorX || !anchorY || !pivotX || !pivotY ||
47 *width < 0.0 || *height < 0.0 || *anchorX < 0.0 || *anchorX > 1.0 ||
48 *anchorY < 0.0 || *anchorY > 1.0 || *pivotX < 0.0 || *pivotX > 1.0 ||
49 *pivotY < 0.0 || *pivotY > 1.0)
50 return eve::editing::failed<UiLayoutValue>(EditorStatus::Rejected, RuleId("editor.ui.invalid-layout"),
51 "UI layout fields are missing or out of range");
52 return eve::editing::applied<UiLayoutValue>(
53 {*x, *y, *width, *height, *anchorX, *anchorY, *pivotX, *pivotY});
54}
55
56EditorValue UiDocumentTarget::styleValue(const UiStyleValue& style) {
57 return EditorValue::Object{{"marginLeft", style.marginLeft}, {"marginTop", style.marginTop},
58 {"marginRight", style.marginRight}, {"marginBottom", style.marginBottom},
59 {"paddingLeft", style.paddingLeft}, {"paddingTop", style.paddingTop},
60 {"paddingRight", style.paddingRight}, {"paddingBottom", style.paddingBottom},
61 {"tintR", style.tintR}, {"tintG", style.tintG}, {"tintB", style.tintB},
62 {"tintA", style.tintA}, {"cornerRadius", style.cornerRadius}, {"gap", style.gap},
63 {"flexGrow", style.flexGrow}, {"direction", style.direction}, {"align", style.align},
64 {"justify", style.justify}};
65}
66
67Result<UiStyleValue> UiDocumentTarget::parseStyle(const EditorValue& value) {
68 UiStyleValue style;
69 double* outputs[]{&style.marginLeft, &style.marginTop, &style.marginRight, &style.marginBottom,
70 &style.paddingLeft, &style.paddingTop, &style.paddingRight, &style.paddingBottom,
71 &style.tintR, &style.tintG, &style.tintB, &style.tintA, &style.cornerRadius,
72 &style.gap, &style.flexGrow};
73 const char* keys[]{"marginLeft", "marginTop", "marginRight", "marginBottom", "paddingLeft",
74 "paddingTop", "paddingRight", "paddingBottom", "tintR", "tintG", "tintB",
75 "tintA", "cornerRadius", "gap", "flexGrow"};
76 for (std::size_t i = 0; i < std::size(outputs); ++i) {
77 const double* number = numberField(value, keys[i]);
78 if (!number || !std::isfinite(*number))
79 return eve::editing::failed<UiStyleValue>(EditorStatus::Rejected, RuleId("editor.ui.invalid-style"),
80 "UI style numeric fields are missing or invalid");
81 *outputs[i] = *number;
82 }
83 const EditorValue* directionValue = field(value, "direction");
84 const EditorValue* alignValue = field(value, "align");
85 const EditorValue* justifyValue = field(value, "justify");
86 const auto* direction = directionValue ? directionValue->getIf<std::string>() : nullptr;
87 const auto* align = alignValue ? alignValue->getIf<std::string>() : nullptr;
88 const auto* justify = justifyValue ? justifyValue->getIf<std::string>() : nullptr;
89 if (!direction || !align || !justify)
90 return eve::editing::failed<UiStyleValue>(EditorStatus::Rejected, RuleId("editor.ui.invalid-style"),
91 "UI flex style fields are missing");
92 style.direction = *direction; style.align = *align; style.justify = *justify;
93 const std::set<std::string> directions{"row", "column"};
94 const std::set<std::string> aligns{"start", "center", "end", "stretch"};
95 const std::set<std::string> justifies{"start", "center", "end", "space-between", "space-around"};
96 for (std::size_t i = 0; i < std::size(outputs); ++i)
97 if (*outputs[i] < 0.0 || (i >= 8 && i <= 11 && *outputs[i] > 1.0))
98 return eve::editing::failed<UiStyleValue>(EditorStatus::Rejected, RuleId("editor.ui.invalid-style-range"),
99 "UI box values must be non-negative and tint normalized");
100 if (!directions.contains(style.direction) || !aligns.contains(style.align) ||
101 !justifies.contains(style.justify))
102 return eve::editing::failed<UiStyleValue>(EditorStatus::Rejected, RuleId("editor.ui.invalid-flex-style"),
103 "UI flex direction, alignment or justification is invalid");
104 return eve::editing::applied<UiStyleValue>(std::move(style));
105}
106
107EditorValue UiDocumentTarget::contentValue(const UiContentValue& content) {
108 return EditorValue::Object{{"fontAsset", content.fontAsset}, {"fontSize", content.fontSize},
109 {"textR", content.textR}, {"textG", content.textG}, {"textB", content.textB},
110 {"textA", content.textA}, {"horizontalAlign", content.horizontalAlign},
111 {"verticalAlign", content.verticalAlign}, {"textureAsset", content.textureAsset},
112 {"imageFit", content.imageFit}, {"clip", content.clip}};
113}
114
115Result<UiContentValue> UiDocumentTarget::parseContent(const EditorValue& value) {
116 UiContentValue content;
117 const EditorValue* fontAssetValue = field(value, "fontAsset");
118 const EditorValue* horizontalValue = field(value, "horizontalAlign");
119 const EditorValue* verticalValue = field(value, "verticalAlign");
120 const EditorValue* textureAssetValue = field(value, "textureAsset");
121 const EditorValue* fitValue = field(value, "imageFit");
122 const EditorValue* clipValue = field(value, "clip");
123 const auto* fontAsset = fontAssetValue ? fontAssetValue->getIf<std::string>() : nullptr;
124 const auto* horizontal = horizontalValue ? horizontalValue->getIf<std::string>() : nullptr;
125 const auto* vertical = verticalValue ? verticalValue->getIf<std::string>() : nullptr;
126 const auto* textureAsset = textureAssetValue ? textureAssetValue->getIf<std::string>() : nullptr;
127 const auto* fit = fitValue ? fitValue->getIf<std::string>() : nullptr;
128 const auto* clip = clipValue ? clipValue->getIf<bool>() : nullptr;
129 const double* fontSize = numberField(value, "fontSize");
130 const double* textR = numberField(value, "textR");
131 const double* textG = numberField(value, "textG");
132 const double* textB = numberField(value, "textB");
133 const double* textA = numberField(value, "textA");
134 if (!fontAsset || !horizontal || !vertical || !textureAsset || !fit || !clip || !fontSize ||
135 !textR || !textG || !textB || !textA)
136 return eve::editing::failed<UiContentValue>(EditorStatus::Rejected, RuleId("editor.ui.invalid-content"),
137 "UI content fields are missing");
138 content = {*fontAsset, *fontSize, *textR, *textG, *textB, *textA, *horizontal, *vertical,
139 *textureAsset, *fit, *clip};
140 const std::set<std::string> aligns{"start", "center", "end"};
141 const std::set<std::string> fits{"stretch", "contain", "cover"};
142 if (!std::isfinite(content.fontSize) || content.fontSize <= 0.0 || content.fontSize > 512.0 ||
143 !std::isfinite(content.textR) || !std::isfinite(content.textG) ||
144 !std::isfinite(content.textB) || !std::isfinite(content.textA) || content.textR < 0.0 ||
145 content.textR > 1.0 || content.textG < 0.0 || content.textG > 1.0 ||
146 content.textB < 0.0 || content.textB > 1.0 || content.textA < 0.0 ||
147 content.textA > 1.0 || !aligns.contains(content.horizontalAlign) ||
148 !aligns.contains(content.verticalAlign) || !fits.contains(content.imageFit))
149 return eve::editing::failed<UiContentValue>(EditorStatus::Rejected, RuleId("editor.ui.invalid-content-range"),
150 "UI content size, color, alignment or image fit is invalid");
151 return eve::editing::applied<UiContentValue>(std::move(content));
152}
153
154EditorValue UiDocumentTarget::widgetValue(const UiWidgetSnapshot& widget) {
156 value["id"] = widget.id.value();
157 value["parent"] = widget.parent.value();
158 value["type"] = widget.type;
159 value["name"] = widget.name;
160 value["text"] = widget.text;
161 value["visible"] = widget.visible;
162 value["enabled"] = widget.enabled;
163 value["layout"] = layoutValue(widget.layout);
164 value["style"] = styleValue(widget.style);
165 value["content"] = contentValue(widget.content);
166 return EditorValue(std::move(value));
167}
168
169Result<UiWidgetSnapshot> UiDocumentTarget::parseWidget(const EditorValue& value) {
170 const EditorValue* idValue = field(value, "id");
171 const EditorValue* parentValue = field(value, "parent");
172 const EditorValue* typeValue = field(value, "type");
173 const EditorValue* nameValue = field(value, "name");
174 const EditorValue* textValue = field(value, "text");
175 const EditorValue* visibleValue = field(value, "visible");
176 const EditorValue* enabledValue = field(value, "enabled");
177 const EditorValue* layout = field(value, "layout");
178 const EditorValue* styleValueEntry = field(value, "style");
179 const EditorValue* contentValueEntry = field(value, "content");
180 const auto* id = idValue ? idValue->getIf<std::string>() : nullptr;
181 const auto* parent = parentValue ? parentValue->getIf<std::string>() : nullptr;
182 const auto* type = typeValue ? typeValue->getIf<std::string>() : nullptr;
183 const auto* name = nameValue ? nameValue->getIf<std::string>() : nullptr;
184 const auto* text = textValue ? textValue->getIf<std::string>() : nullptr;
185 const auto* visible = visibleValue ? visibleValue->getIf<bool>() : nullptr;
186 const auto* enabled = enabledValue ? enabledValue->getIf<bool>() : nullptr;
187 if (!id || id->empty() || !parent || !type || type->empty() || !name || name->empty() ||
188 !text || !visible || !enabled || !layout)
189 return eve::editing::failed<UiWidgetSnapshot>(EditorStatus::Rejected, RuleId("editor.ui.invalid-widget"),
190 "UI widget fields are missing or invalid");
191 auto parsedLayout = parseLayout(*layout);
192 if (!parsedLayout.ok())
193 return eve::editing::failed<UiWidgetSnapshot>(EditorStatus::Rejected, RuleId("editor.ui.invalid-widget-layout"),
194 "UI widget layout is invalid");
195 UiStyleValue parsedStyle;
196 if (styleValueEntry) {
197 auto style = parseStyle(*styleValueEntry);
198 if (!style.ok())
199 return eve::editing::failed<UiWidgetSnapshot>(
200 EditorStatus::Rejected, RuleId("editor.ui.invalid-widget-style"), "UI widget style is invalid");
201 parsedStyle = std::move(style).value();
202 }
203 UiContentValue parsedContent;
204 if (contentValueEntry) {
205 auto content = parseContent(*contentValueEntry);
206 if (!content.ok())
207 return eve::editing::failed<UiWidgetSnapshot>(EditorStatus::Rejected,
208 RuleId("editor.ui.invalid-widget-content"),
209 "UI widget content skin is invalid");
210 parsedContent = std::move(content).value();
211 }
212 return eve::editing::applied<UiWidgetSnapshot>(
213 {ObjectId(*id), ObjectId(*parent), *type, *name, *text, *visible, *enabled,
214 std::move(parsedLayout).value(), std::move(parsedStyle), std::move(parsedContent)});
215}
216
218 EditorValue::Array widgets;
219 widgets.reserve(widgets_.size());
220 for (const auto& [id, widget] : widgets_) {
221 (void)id;
222 widgets.push_back(widgetValue(widget));
223 }
225 root["schemaVersion"] = 2;
226 root["widgets"] = EditorValue(std::move(widgets));
227 return EditorValue(std::move(root));
228}
229
231 const EditorValue* versionValue = field(snapshot, "schemaVersion");
232 const EditorValue* widgetsValue = field(snapshot, "widgets");
233 const auto* version = versionValue ? versionValue->getIf<int64_t>() : nullptr;
234 const auto* widgets = widgetsValue ? widgetsValue->getIf<EditorValue::Array>() : nullptr;
235 if (!version || (*version != 1 && *version != 2) || !widgets)
236 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.snapshot-format"),
237 "UI snapshot requires schemaVersion 1 or 2 and a widget array");
238 std::map<ObjectId, UiWidgetSnapshot> candidate;
239 for (const EditorValue& value : *widgets) {
240 auto parsed = parseWidget(value);
241 if (!parsed.ok())
242 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.snapshot-widget"),
243 "UI snapshot contains an invalid widget");
244 const auto& widget = parsed.value();
245 if (!candidate.emplace(widget.id, widget).second)
246 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.ui.snapshot-duplicate"),
247 "UI snapshot contains duplicate widget ids");
248 }
249 for (const auto& [id, widget] : candidate) {
250 if ((!widget.parent.empty() && !candidate.contains(widget.parent)) ||
251 wouldCycle(id, widget.parent, candidate))
252 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.snapshot-hierarchy"),
253 "UI snapshot hierarchy is missing a parent or contains a cycle");
254 }
255 widgets_ = std::move(candidate);
256 bumpRevision();
258 return eve::editing::applied<void>();
259}
260
261} // namespace eve::ui_editing
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::string widget
std::string layout
std::uint32_t key
glm::vec4 clip
std::uint32_t height
std::uint32_t width
std::string text
std::int32_t parent
std::string name
size_t directions
Definition OnnxLstm.cpp:29
std::string id
Definition PlayHost.cpp:108
RoadLaneDirection direction
double number
bool found
bool visible
Move-only operation result carrying either a value or Status.
Definition Result.h:155
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
void bumpRevision()
Advance the revision without touching the dirty region.
virtual void clearDirtyRegion()=0
Clears dirty region.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
Type type() const
Return the active value type.
const T * getIf() const
Return an immediately borrowed typed value.
EditorValue snapshotValue() const
Capture deterministic UI document content.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load and validate a persisted UI document.
editing::ObjectId ObjectId
ui_editing::UiContentValue UiContentValue
ui_editing::UiStyleValue UiStyleValue
const EditorValue * field(const EditorValue &value, const char *name)
editing::Value EditorValue
Definition UiDocument.h:44
bool enabled