12 if (!
object)
return nullptr;
13 const auto found =
object->find(
key);
14 return found ==
object->end() ? nullptr : &
found->second;
19 return entry ? entry->getIf<
double>() : nullptr;
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});
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}};
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) {
79 return eve::editing::failed<UiStyleValue>(EditorStatus::Rejected,
RuleId(
"editor.ui.invalid-style"),
80 "UI style numeric fields are missing or invalid");
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;
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));
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}};
115Result<UiContentValue> UiDocumentTarget::parseContent(
const EditorValue&
value) {
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));
169Result<UiWidgetSnapshot> UiDocumentTarget::parseWidget(
const EditorValue&
value) {
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;
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");
196 if (styleValueEntry) {
197 auto style = parseStyle(*styleValueEntry);
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();
204 if (contentValueEntry) {
205 auto content = parseContent(*contentValueEntry);
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();
212 return eve::editing::applied<UiWidgetSnapshot>(
214 std::move(parsedLayout).
value(), std::move(parsedStyle), std::move(parsedContent)});
219 widgets.reserve(widgets_.size());
220 for (
const auto& [
id,
widget] : widgets_) {
222 widgets.push_back(widgetValue(
widget));
225 root[
"schemaVersion"] = 2;
231 const EditorValue* versionValue = field(snapshot,
"schemaVersion");
232 const EditorValue* widgetsValue = field(snapshot,
"widgets");
233 const auto* version = versionValue ? versionValue->
getIf<int64_t>() :
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;
240 auto parsed = parseWidget(
value);
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();
246 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId(
"editor.ui.snapshot-duplicate"),
247 "UI snapshot contains duplicate widget ids");
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");
255 widgets_ = std::move(candidate);
258 return eve::editing::applied<void>();
RoadLaneDirection direction
Move-only operation result carrying either a value or Status.
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
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