载入中...
搜索中...
未找到
UiDocument.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <optional>
6#include <set>
7#include <utility>
8
9namespace eve::ui_editing {
10namespace {
11
12EditorDiagnostic uiDiagnostic(const char* rule, DiagnosticSeverity severity, std::string message) {
14 std::move(message));
15}
16
17} // namespace
18
19UiDocumentTarget::UiDocumentTarget(std::string id) : id_(std::move(id)) {}
20
22 return {TargetId(id_),
23 "ui-document",
25 false,
26 {IUiDocumentEditTarget::editorCapabilityId(), CapabilityId("eve.editor.target.ui-properties")}};
27}
28
29void* UiDocumentTarget::queryCapability(const CapabilityId& capability) {
31 if (capability == CapabilityId("eve.editor.target.ui-properties")) return static_cast<IPropertyProvider*>(this);
32 return nullptr;
33}
34
36 if (operation.target != TargetId(id_))
37 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.target-mismatch"),
38 "UI operation targets another document");
39 if (operation.type == "ui.widget.create.v1") {
40 auto parsed = parseWidget(operation.payload);
41 if (!parsed.ok())
42 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.create-payload"),
43 "UI create payload is invalid");
44 const UiWidgetSnapshot& value = parsed.value();
45 if (value.id.empty() || widgets_.contains(value.id))
46 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.ui.widget-exists"),
47 "UI widget id is empty or already exists");
48 if (!value.parent.empty() && !widgets_.contains(value.parent))
49 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.ui.parent-not-found"),
50 "UI widget parent does not exist");
51 widgets_.emplace(value.id, value);
52 } else if (operation.type == "ui.widget.delete.v1") {
53 auto parsed = parseWidget(operation.payload);
54 if (!parsed.ok() || !widgets_.contains(parsed.value().id))
55 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
56 "UI widget does not exist");
57 if (!children(parsed.value().id).empty())
58 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.widget-has-children"),
59 "UI widget with children cannot be deleted");
60 widgets_.erase(parsed.value().id);
61 } else if (operation.type == "ui.widget.replace.v1") {
62 auto parsed = parseWidget(operation.payload);
63 if (!parsed.ok())
64 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.replace-payload"),
65 "UI replacement payload is invalid");
66 auto current = widgets_.find(parsed.value().id);
67 if (current == widgets_.end())
68 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
69 "UI widget does not exist");
70 if (!parsed.value().parent.empty() && !widgets_.contains(parsed.value().parent))
71 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.ui.parent-not-found"),
72 "UI widget parent does not exist");
73 if (wouldCycle(parsed.value().id, parsed.value().parent, widgets_))
74 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.hierarchy-cycle"),
75 "UI reparenting would create a hierarchy cycle");
76 current->second = parsed.value();
77 } else if (operation.type == "ui.widget.multi-replace.v1") {
78 const auto* entries = operation.payload.getIf<EditorValue::Array>();
79 if (!entries)
80 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.multi-payload"),
81 "UI multi-edit requires an array payload");
82 auto candidate = widgets_;
83 for (const EditorValue& entry : *entries) {
84 auto parsed = parseWidget(entry);
85 if (!parsed.ok() || !candidate.contains(parsed.value().id))
86 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.multi-widget"),
87 "UI multi-edit references an invalid widget");
88 candidate[parsed.value().id] = parsed.value();
89 }
90 for (const auto& [widgetId, value] : candidate) {
91 if ((!value.parent.empty() && !candidate.contains(value.parent)) ||
92 wouldCycle(widgetId, value.parent, candidate))
93 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui.multi-hierarchy"),
94 "UI multi-edit produces an invalid hierarchy");
95 }
96 widgets_ = std::move(candidate);
97 } else {
98 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.ui.operation-unsupported"),
99 "Unsupported UI operation: " + operation.type);
100 }
101 bumpRevision();
102 widenDirty(0, 0);
103 return eve::editing::applied<void>();
104}
105
106std::unique_ptr<IDomainOperationTarget> UiDocumentTarget::cloneDomainState() const {
107 return std::make_unique<UiDocumentTarget>(*this);
108}
109
110Result<void> UiDocumentTarget::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
111 auto* document = dynamic_cast<UiDocumentTarget*>(candidate.get());
112 if (!document || document->id_ != id_)
113 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.ui.candidate-mismatch"),
114 "UI compensation candidate belongs to another document");
115 *this = *document;
116 return eve::editing::applied<void>();
117}
118
120 const auto found = widgets_.find(id);
121 if (found == widgets_.end())
122 return eve::editing::failed<UiWidgetSnapshot>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
123 "UI widget does not exist: " + id.value());
124 return eve::editing::applied<UiWidgetSnapshot>(found->second);
125}
126
127std::vector<ObjectId> UiDocumentTarget::children(const ObjectId& parent) const {
128 std::vector<ObjectId> result;
129 for (const auto& [id, value] : widgets_)
130 if (value.parent == parent) result.push_back(id);
131 return result;
132}
133
135 if (request.id.empty() || request.type.empty() || request.name.empty())
136 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui.invalid-widget"),
137 "UI widget id, type and name are required");
138 if (widgets_.contains(request.id))
139 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict, RuleId("editor.ui.widget-exists"),
140 "UI widget already exists: " + request.id.value());
141 if (!request.parent.empty() && !widgets_.contains(request.parent))
142 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.parent-not-found"),
143 "UI widget parent does not exist");
144 UiWidgetSnapshot value{request.id, request.parent, request.type, request.name, "",
145 true, true, request.layout, {}, {}};
147 operation.type = "ui.widget.create.v1";
148 operation.inverseType = "ui.widget.delete.v1";
149 operation.target = TargetId(id_);
150 operation.payload = widgetValue(value);
151 operation.inverse = operation.payload;
152 operation.hasInverse = true;
153 operation.affectedObjects.push_back({TargetId(id_), request.id.value(), 0});
154 return eve::editing::applied<DomainOperation>(std::move(operation));
155}
156
158 auto current = widget(id);
159 if (!current.ok())
160 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
161 "UI widget does not exist");
162 if (!children(id).empty())
163 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui.widget-has-children"),
164 "Delete or reparent children before deleting their parent");
166 operation.type = "ui.widget.delete.v1";
167 operation.inverseType = "ui.widget.create.v1";
168 operation.target = TargetId(id_);
169 operation.payload = widgetValue(current.value());
170 operation.inverse = operation.payload;
171 operation.hasInverse = true;
172 operation.affectedObjects.push_back({TargetId(id_), id.value(), 0});
173 return eve::editing::applied<DomainOperation>(std::move(operation));
174}
175
176Result<DomainOperation> UiDocumentTarget::makeRename(const ObjectId& id, const std::string& name) const {
177 auto current = widget(id);
178 if (!current.ok())
179 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
180 "UI widget does not exist");
181 if (name.empty())
182 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui.empty-name"),
183 "UI widget name must not be empty");
184 UiWidgetSnapshot changed = current.value();
185 changed.name = name;
186 return makeReplace(current.value(), std::move(changed), "widget.name");
187}
188
189Result<DomainOperation> UiDocumentTarget::makeReparent(const ObjectId& id, const ObjectId& parent) const {
190 auto current = widget(id);
191 if (!current.ok())
192 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
193 "UI widget does not exist");
194 if (!parent.empty() && !widgets_.contains(parent))
195 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.parent-not-found"),
196 "UI widget parent does not exist");
197 if (wouldCycle(id, parent, widgets_))
198 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui.hierarchy-cycle"),
199 "UI reparenting would create a hierarchy cycle");
200 UiWidgetSnapshot changed = current.value();
201 changed.parent = parent;
202 return makeReplace(current.value(), std::move(changed), "widget.parent");
203}
204
206 auto current = widget(id);
207 if (!current.ok())
208 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
209 "UI widget does not exist");
210 if (layout.width < 0.0 || layout.height < 0.0 || layout.anchorX < 0.0 || layout.anchorX > 1.0 ||
211 layout.anchorY < 0.0 || layout.anchorY > 1.0 || layout.pivotX < 0.0 || layout.pivotX > 1.0 ||
212 layout.pivotY < 0.0 || layout.pivotY > 1.0)
213 return eve::editing::failed<DomainOperation>(
214 EditorStatus::Rejected, RuleId("editor.ui.invalid-layout"),
215 "UI size must be non-negative and anchors/pivots must be within 0..1");
216 UiWidgetSnapshot changed = current.value();
217 changed.layout = layout;
218 return makeReplace(current.value(), std::move(changed), "layout");
219}
220
222 auto current = widget(id);
223 if (!current.ok())
224 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
225 "UI widget does not exist");
226 const double values[]{style.marginLeft, style.marginTop, style.marginRight, style.marginBottom,
227 style.paddingLeft, style.paddingTop, style.paddingRight, style.paddingBottom,
228 style.tintR, style.tintG, style.tintB, style.tintA,
229 style.cornerRadius, style.gap, style.flexGrow};
230 for (double value : values)
231 if (!std::isfinite(value))
232 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui.nonfinite-style"),
233 "UI style values must be finite");
234 const std::set<std::string> directions{"row", "column"};
235 const std::set<std::string> aligns{"start", "center", "end", "stretch"};
236 const std::set<std::string> justifies{"start", "center", "end", "space-between", "space-around"};
237 if (style.marginLeft < 0.0 || style.marginTop < 0.0 || style.marginRight < 0.0 || style.marginBottom < 0.0 ||
238 style.paddingLeft < 0.0 || style.paddingTop < 0.0 || style.paddingRight < 0.0 || style.paddingBottom < 0.0 ||
239 style.cornerRadius < 0.0 || style.gap < 0.0 || style.flexGrow < 0.0 || style.tintR < 0.0 || style.tintR > 1.0 ||
240 style.tintG < 0.0 || style.tintG > 1.0 || style.tintB < 0.0 || style.tintB > 1.0 || style.tintA < 0.0 ||
241 style.tintA > 1.0 || !directions.contains(style.direction) || !aligns.contains(style.align) ||
242 !justifies.contains(style.justify))
243 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui.invalid-style"),
244 "UI style contains invalid box, color or flex values");
245 UiWidgetSnapshot changed = current.value();
246 changed.style = style;
247 return makeReplace(current.value(), std::move(changed), "style");
248}
249
251 const UiContentValue& content) const {
252 auto current = widget(id);
253 if (!current.ok())
254 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
255 "UI widget does not exist");
256 const double colors[]{content.textR, content.textG, content.textB, content.textA};
257 const std::set<std::string> aligns{"start", "center", "end"};
258 const std::set<std::string> fits{"stretch", "contain", "cover"};
259 if (!std::isfinite(content.fontSize) || content.fontSize <= 0.0 || content.fontSize > 512.0 ||
260 std::any_of(std::begin(colors), std::end(colors),
261 [](double value) { return !std::isfinite(value) || value < 0.0 || value > 1.0; }) ||
262 !aligns.contains(content.horizontalAlign) || !aligns.contains(content.verticalAlign) ||
263 !fits.contains(content.imageFit))
264 return eve::editing::failed<DomainOperation>(
265 EditorStatus::Rejected, RuleId("editor.ui.invalid-content"),
266 "UI content contains an invalid font size, color, alignment or image fit");
267 UiWidgetSnapshot changed = current.value();
268 changed.content = content;
269 return makeReplace(current.value(), std::move(changed), "content");
270}
271
272std::vector<EditorDiagnostic> UiDocumentTarget::validate() const {
273 std::vector<EditorDiagnostic> diagnostics;
274 int roots = 0;
275 for (const auto& [id, value] : widgets_) {
276 if (value.parent.empty()) ++roots;
277 if (value.layout.width == 0.0 || value.layout.height == 0.0)
278 diagnostics.push_back(uiDiagnostic("editor.ui.zero-size-widget", DiagnosticSeverity::Warning,
279 "Widget has zero preview size: " + id.value()));
280 if (value.style.cornerRadius * 2.0 > std::min(value.layout.width, value.layout.height) &&
281 value.layout.width > 0.0 && value.layout.height > 0.0)
282 diagnostics.push_back(uiDiagnostic("editor.ui.excessive-corner-radius", DiagnosticSeverity::Warning,
283 "Corner radius exceeds half of widget size: " + id.value()));
284 if (!value.text.empty() && value.content.fontAsset.empty())
285 diagnostics.push_back(uiDiagnostic("editor.ui.missing-font", DiagnosticSeverity::Warning,
286 "Text widget has no font asset: " + id.value()));
287 }
288 if (!widgets_.empty() && roots == 0)
289 diagnostics.push_back(uiDiagnostic("editor.ui.missing-root", DiagnosticSeverity::Error,
290 "UI document has no root widget"));
291 if (roots > 1)
292 diagnostics.push_back(uiDiagnostic("editor.ui.multiple-roots", DiagnosticSeverity::Warning,
293 "UI document contains multiple root widgets"));
294 return diagnostics;
295}
296
297Result<DomainOperation> UiDocumentTarget::makeReplace(const UiWidgetSnapshot& before, UiWidgetSnapshot after,
298 std::string property) const {
300 operation.type = "ui.widget.replace.v1";
301 operation.target = TargetId(id_);
302 operation.payload = widgetValue(after);
303 operation.inverse = widgetValue(before);
304 operation.hasInverse = true;
305 operation.affectedObjects.push_back({TargetId(id_), before.id.value(), 0});
306 if (!property.empty()) operation.affectedProperties.push_back(std::move(property));
307 operation.mergeKey = "ui.widget:" + before.id.value() + ":" +
308 (operation.affectedProperties.empty() ? "replace" : operation.affectedProperties.front());
309 return eve::editing::applied<DomainOperation>(std::move(operation));
310}
311
312bool UiDocumentTarget::wouldCycle(const ObjectId& id, const ObjectId& parent,
313 const std::map<ObjectId, UiWidgetSnapshot>& widgets) const {
314 ObjectId ancestor = parent;
315 while (!ancestor.empty()) {
316 if (ancestor == id) return true;
317 const auto current = widgets.find(ancestor);
318 if (current == widgets.end()) return false;
319 ancestor = current->second.parent;
320 }
321 return false;
322}
323
324} // namespace eve::ui_editing
ActionParameterOperation operation
double value
std::unordered_map< std::string, QuestRuntime > entries
std::map< std::string, Var > values
std::string widget
std::string layout
std::string message
const GltfImportRequest & request
std::int32_t parent
std::string name
size_t directions
Definition OnnxLstm.cpp:29
std::string id
Definition PlayHost.cpp:108
std::vector< float > colors
bool found
double current
int children
Definition TreeMesh.cpp:295
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
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.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
UI-independent property access for an immutable selection snapshot.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
Hierarchy/layout capability consumed by UI tree and canvas tools.
Definition UiDocument.h:117
static CapabilityId editorCapabilityId()
Editor capability id.
Definition UiDocument.h:122
Serializable UI authoring document with hierarchy and inspector capabilities.
Definition UiDocument.h:149
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Publish a validated candidate belonging to this document.
Result< DomainOperation > makeSetContent(const ObjectId &id, const UiContentValue &content) const override
Make set content.
Result< DomainOperation > makeCreate(const CreateUiWidgetRequest &request) const override
Make create.
std::vector< EditorDiagnostic > validate() const
Validate style, hierarchy and viewport layout constraints.
std::vector< ObjectId > children(const ObjectId &parent) const override
Children.
Result< DomainOperation > makeReparent(const ObjectId &id, const ObjectId &parent) const override
Make reparent.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
Result< DomainOperation > makeRename(const ObjectId &id, const std::string &name) const override
Make rename.
void * queryCapability(const CapabilityId &capability) override
Query an optional target capability.
TargetDescriptor describe() const override
Describe.
Result< DomainOperation > makeSetLayout(const ObjectId &id, const UiLayoutValue &layout) const override
Make set layout.
UiDocumentTarget(std::string id)
Ui document target.
Result< DomainOperation > makeSetStyle(const ObjectId &id, const UiStyleValue &style) const override
Make set style.
Result< UiWidgetSnapshot > widget(const ObjectId &id) const override
Widget.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clone an isolated owning candidate for atomic compensation.
Result< DomainOperation > makeDelete(const ObjectId &id) const override
Make delete.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
DomainOperation public API.
Stable discovery metadata exposed by every editable target.
Request for creating one widget with stable identity.
Definition UiDocument.h:108
Renderer-neutral text and image skin authored per widget.
Definition UiDocument.h:79
Renderer-neutral layout values shared by UI document hosts.
Definition UiDocument.h:48
Renderer-neutral visual and box-model style authored per widget.
Definition UiDocument.h:63
Immutable widget snapshot used across editor/runtime boundaries.
Definition UiDocument.h:94