载入中...
搜索中...
未找到
UiDocumentProperty.cpp
浏览该文件的文档.
2
3#include <optional>
4#include <utility>
5
6namespace eve::ui_editing {
7namespace {
8
9PropertyDescriptor uiProperty(const char* path, const char* label, PropertyType type,
10 EditorValue defaultValue, const char* category) {
11 PropertyDescriptor result;
12 result.path = PropertyPath(path);
13 result.displayNameKey = label;
14 result.category = category;
15 result.type = type;
16 result.flags = PropertyFlag::Runtime | PropertyFlag::MultiEdit;
17 result.defaultValue = std::move(defaultValue);
18 return result;
19}
20
21EditorValue component(const UiWidgetSnapshot& widget, const PropertyPath& path) {
22 if (path == PropertyPath("widget.name")) return widget.name;
23 if (path == PropertyPath("widget.text")) return widget.text;
24 if (path == PropertyPath("content.fontAsset")) return widget.content.fontAsset;
25 if (path == PropertyPath("content.fontSize")) return widget.content.fontSize;
26 if (path == PropertyPath("content.textColor"))
27 return EditorValue::Array{widget.content.textR, widget.content.textG,
28 widget.content.textB, widget.content.textA};
29 if (path == PropertyPath("content.horizontalAlign")) return widget.content.horizontalAlign;
30 if (path == PropertyPath("content.verticalAlign")) return widget.content.verticalAlign;
31 if (path == PropertyPath("content.textureAsset")) return widget.content.textureAsset;
32 if (path == PropertyPath("content.imageFit")) return widget.content.imageFit;
33 if (path == PropertyPath("content.clip")) return widget.content.clip;
34 if (path == PropertyPath("widget.visible")) return widget.visible;
35 if (path == PropertyPath("widget.enabled")) return widget.enabled;
36 if (path == PropertyPath("layout.position")) return EditorValue::Array{widget.layout.x, widget.layout.y};
37 if (path == PropertyPath("layout.size"))
38 return EditorValue::Array{widget.layout.width, widget.layout.height};
39 if (path == PropertyPath("layout.anchor"))
40 return EditorValue::Array{widget.layout.anchorX, widget.layout.anchorY};
41 if (path == PropertyPath("layout.pivot"))
42 return EditorValue::Array{widget.layout.pivotX, widget.layout.pivotY};
43 if (path == PropertyPath("style.margin"))
44 return EditorValue::Array{widget.style.marginLeft, widget.style.marginTop,
45 widget.style.marginRight, widget.style.marginBottom};
46 if (path == PropertyPath("style.padding"))
47 return EditorValue::Array{widget.style.paddingLeft, widget.style.paddingTop,
48 widget.style.paddingRight, widget.style.paddingBottom};
49 if (path == PropertyPath("style.tint"))
50 return EditorValue::Array{widget.style.tintR, widget.style.tintG,
51 widget.style.tintB, widget.style.tintA};
52 if (path == PropertyPath("style.cornerRadius")) return widget.style.cornerRadius;
53 if (path == PropertyPath("style.gap")) return widget.style.gap;
54 if (path == PropertyPath("style.flexGrow")) return widget.style.flexGrow;
55 if (path == PropertyPath("style.direction")) return widget.style.direction;
56 if (path == PropertyPath("style.align")) return widget.style.align;
57 if (path == PropertyPath("style.justify")) return widget.style.justify;
58 return {};
59}
60
61bool assign(UiWidgetSnapshot& widget, const PropertyPath& path, const EditorValue& value) {
62 if (path == PropertyPath("content.fontAsset") || path == PropertyPath("content.textureAsset")) {
63 const auto* asset = value.getIf<std::string>(); if (!asset) return false;
64 if (path == PropertyPath("content.fontAsset")) widget.content.fontAsset = *asset;
65 else widget.content.textureAsset = *asset;
66 return true;
67 }
68 if (path == PropertyPath("content.fontSize")) {
69 const auto* number = value.getIf<double>();
70 if (!number || *number <= 0.0 || *number > 512.0) return false;
71 widget.content.fontSize = *number; return true;
72 }
73 if (path == PropertyPath("content.clip")) {
74 const auto* flag = value.getIf<bool>(); if (!flag) return false;
75 widget.content.clip = *flag; return true;
76 }
77 if (path == PropertyPath("content.horizontalAlign") ||
78 path == PropertyPath("content.verticalAlign") || path == PropertyPath("content.imageFit")) {
79 const auto* text = value.getIf<std::string>(); if (!text) return false;
80 if (path == PropertyPath("content.imageFit")) {
81 if (*text != "stretch" && *text != "contain" && *text != "cover") return false;
82 widget.content.imageFit = *text;
83 } else {
84 if (*text != "start" && *text != "center" && *text != "end") return false;
85 if (path == PropertyPath("content.horizontalAlign")) widget.content.horizontalAlign = *text;
86 else widget.content.verticalAlign = *text;
87 }
88 return true;
89 }
90 if (path == PropertyPath("widget.name") || path == PropertyPath("widget.text")) {
91 const auto* text = value.getIf<std::string>();
92 if (!text || (path == PropertyPath("widget.name") && text->empty())) return false;
93 if (path == PropertyPath("widget.name")) widget.name = *text;
94 else widget.text = *text;
95 return true;
96 }
97 if (path == PropertyPath("widget.visible") || path == PropertyPath("widget.enabled")) {
98 const auto* flag = value.getIf<bool>();
99 if (!flag) return false;
100 if (path == PropertyPath("widget.visible")) widget.visible = *flag;
101 else widget.enabled = *flag;
102 return true;
103 }
104 if (path == PropertyPath("style.cornerRadius") || path == PropertyPath("style.gap") ||
105 path == PropertyPath("style.flexGrow")) {
106 const auto* number = value.getIf<double>();
107 if (!number || *number < 0.0) return false;
108 if (path == PropertyPath("style.cornerRadius")) widget.style.cornerRadius = *number;
109 else if (path == PropertyPath("style.gap")) widget.style.gap = *number;
110 else widget.style.flexGrow = *number;
111 return true;
112 }
113 if (path == PropertyPath("style.direction") || path == PropertyPath("style.align") ||
114 path == PropertyPath("style.justify")) {
115 const auto* text = value.getIf<std::string>();
116 if (!text) return false;
117 if (path == PropertyPath("style.direction")) {
118 if (*text != "row" && *text != "column") return false;
119 widget.style.direction = *text;
120 } else if (path == PropertyPath("style.align")) {
121 if (*text != "start" && *text != "center" && *text != "end" && *text != "stretch") return false;
122 widget.style.align = *text;
123 } else {
124 if (*text != "start" && *text != "center" && *text != "end" &&
125 *text != "space-between" && *text != "space-around") return false;
126 widget.style.justify = *text;
127 }
128 return true;
129 }
130 const auto* tuple = value.getIf<EditorValue::Array>();
131 if ((path == PropertyPath("style.margin") || path == PropertyPath("style.padding") ||
132 path == PropertyPath("style.tint") || path == PropertyPath("content.textColor"))) {
133 if (!tuple || tuple->size() != 4) return false;
134 double values[4];
135 for (int i = 0; i < 4; ++i) { const auto* number = (*tuple)[i].getIf<double>(); if (!number || *number < 0.0) return false; values[i] = *number; }
136 if (path == PropertyPath("style.tint") || path == PropertyPath("content.textColor")) {
137 for (double number : values) if (number > 1.0) return false;
138 if (path == PropertyPath("style.tint")) {
139 widget.style.tintR = values[0]; widget.style.tintG = values[1]; widget.style.tintB = values[2]; widget.style.tintA = values[3];
140 } else {
141 widget.content.textR = values[0]; widget.content.textG = values[1]; widget.content.textB = values[2]; widget.content.textA = values[3];
142 }
143 } else if (path == PropertyPath("style.margin")) {
144 widget.style.marginLeft = values[0]; widget.style.marginTop = values[1]; widget.style.marginRight = values[2]; widget.style.marginBottom = values[3];
145 } else {
146 widget.style.paddingLeft = values[0]; widget.style.paddingTop = values[1]; widget.style.paddingRight = values[2]; widget.style.paddingBottom = values[3];
147 }
148 return true;
149 }
150 if (!tuple || tuple->size() != 2) return false;
151 const auto* first = (*tuple)[0].getIf<double>();
152 const auto* second = (*tuple)[1].getIf<double>();
153 if (!first || !second) return false;
154 if (path == PropertyPath("layout.position")) {
155 widget.layout.x = *first;
156 widget.layout.y = *second;
157 } else if (path == PropertyPath("layout.size")) {
158 if (*first < 0.0 || *second < 0.0) return false;
159 widget.layout.width = *first;
160 widget.layout.height = *second;
161 } else if (path == PropertyPath("layout.anchor")) {
162 if (*first < 0.0 || *first > 1.0 || *second < 0.0 || *second > 1.0) return false;
163 widget.layout.anchorX = *first;
164 widget.layout.anchorY = *second;
165 } else if (path == PropertyPath("layout.pivot")) {
166 if (*first < 0.0 || *first > 1.0 || *second < 0.0 || *second > 1.0) return false;
167 widget.layout.pivotX = *first;
168 widget.layout.pivotY = *second;
169 } else {
170 return false;
171 }
172 return true;
173}
174
175} // namespace
176
178 if (selection.items.empty())
180 eve::DiagnosticCode::InvalidArgument, "UI selection is empty", "editor.ui.selection", {},
181 "editor.UiDocumentTarget"));
182 for (const SelectionItem& item : selection.items)
183 if (item.target != TargetId(id_) || !widgets_.contains(ObjectId(item.item.value())))
185 eve::DiagnosticCode::InvalidArgument, "UI selection contains an invalid widget",
186 "editor.ui.selection", {}, "editor.UiDocumentTarget"));
188}
189
191 PropertySchema result;
192 result.typeId = "ui.widget";
193 result.properties = {
194 uiProperty("widget.name", "editor.ui.name", PropertyType::String, "Widget", "widget"),
195 uiProperty("widget.text", "editor.ui.text", PropertyType::String, "", "content"),
196 uiProperty("content.fontAsset", "editor.ui.fontAsset", PropertyType::AssetRef, "", "content"),
197 uiProperty("content.fontSize", "editor.ui.fontSize", PropertyType::Float, 16.0, "content"),
198 uiProperty("content.textColor", "editor.ui.textColor", PropertyType::Color,
199 EditorValue::Array{1.0, 1.0, 1.0, 1.0}, "content"),
200 uiProperty("content.horizontalAlign", "editor.ui.horizontalAlign", PropertyType::Enum, "start", "content"),
201 uiProperty("content.verticalAlign", "editor.ui.verticalAlign", PropertyType::Enum, "start", "content"),
202 uiProperty("content.textureAsset", "editor.ui.textureAsset", PropertyType::AssetRef, "", "content"),
203 uiProperty("content.imageFit", "editor.ui.imageFit", PropertyType::Enum, "stretch", "content"),
204 uiProperty("content.clip", "editor.ui.clip", PropertyType::Bool, true, "content"),
205 uiProperty("widget.visible", "editor.ui.visible", PropertyType::Bool, true, "widget"),
206 uiProperty("widget.enabled", "editor.ui.enabled", PropertyType::Bool, true, "widget"),
207 uiProperty("layout.position", "editor.ui.position", PropertyType::Vec2,
208 EditorValue::Array{0.0, 0.0}, "layout"),
209 uiProperty("layout.size", "editor.ui.size", PropertyType::Vec2,
210 EditorValue::Array{0.0, 0.0}, "layout"),
211 uiProperty("layout.anchor", "editor.ui.anchor", PropertyType::Vec2,
212 EditorValue::Array{0.0, 0.0}, "layout"),
213 uiProperty("layout.pivot", "editor.ui.pivot", PropertyType::Vec2,
214 EditorValue::Array{0.0, 0.0}, "layout"),
215 uiProperty("style.margin", "editor.ui.margin", PropertyType::Vec4,
216 EditorValue::Array{0.0, 0.0, 0.0, 0.0}, "style"),
217 uiProperty("style.padding", "editor.ui.padding", PropertyType::Vec4,
218 EditorValue::Array{0.0, 0.0, 0.0, 0.0}, "style"),
219 uiProperty("style.tint", "editor.ui.tint", PropertyType::Color,
220 EditorValue::Array{1.0, 1.0, 1.0, 1.0}, "style"),
221 uiProperty("style.cornerRadius", "editor.ui.cornerRadius", PropertyType::Float, 0.0, "style"),
222 uiProperty("style.gap", "editor.ui.gap", PropertyType::Float, 0.0, "flex"),
223 uiProperty("style.flexGrow", "editor.ui.flexGrow", PropertyType::Float, 0.0, "flex"),
224 uiProperty("style.direction", "editor.ui.direction", PropertyType::Enum, "row", "flex"),
225 uiProperty("style.align", "editor.ui.align", PropertyType::Enum, "start", "flex"),
226 uiProperty("style.justify", "editor.ui.justify", PropertyType::Enum, "start", "flex")};
227 return result;
228}
229
231 const PropertyPath& path) const {
232 std::optional<EditorValue> common;
233 for (const SelectionItem& item : selection.items) {
234 if (item.target != TargetId(id_)) return {};
235 const auto found = widgets_.find(ObjectId(item.item.value()));
236 if (found == widgets_.end()) return {};
237 EditorValue value = component(found->second, path);
238 if (value.type() == EditorValue::Type::Null) return {};
239 if (!common) common = value;
240 else if (*common != value) return {PropertyReadState::Mixed, {}, {}};
241 }
242 return common ? PropertyReadResult{PropertyReadState::Value, *common, {}} : PropertyReadResult{};
243}
244
246 const PropertyPath& path,
247 const EditorValue& value,
248 PropertySetMode mode) const {
249 if (mode == PropertySetMode::Reset) return makeReset(selection, path);
250 if (mode != PropertySetMode::Absolute || selection.items.empty())
251 return eve::editing::failed<DomainOperation>(EditorStatus::Unsupported, RuleId("editor.ui.property-mode"),
252 "UI properties require a non-empty absolute assignment");
253 if (!schema(selection).find(path))
254 return eve::editing::failed<DomainOperation>(EditorStatus::Unsupported, RuleId("editor.ui.property-path"),
255 "Unknown UI property: " + path.value());
257 EditorValue::Array inverse;
259 for (const SelectionItem& item : selection.items) {
260 if (item.target != TargetId(id_))
261 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui.property-target"),
262 "UI selection spans another target");
263 const auto found = widgets_.find(ObjectId(item.item.value()));
264 if (found == widgets_.end())
265 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui.widget-not-found"),
266 "Selected UI widget does not exist");
267 UiWidgetSnapshot changed = found->second;
268 if (!assign(changed, path, value))
269 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui.property-value"),
270 "UI property value is invalid for " + path.value());
271 payload.push_back(widgetValue(changed));
272 inverse.push_back(widgetValue(found->second));
273 operation.affectedObjects.push_back({TargetId(id_), item.item.value(), 0});
274 }
275 operation.type = "ui.widget.multi-replace.v1";
276 operation.target = TargetId(id_);
277 operation.payload = EditorValue(std::move(payload));
278 operation.inverse = EditorValue(std::move(inverse));
279 operation.hasInverse = true;
280 operation.affectedProperties.push_back(path.value());
281 operation.mergeKey = "ui.selection:" + path.value();
282 return eve::editing::applied<DomainOperation>(std::move(operation));
283}
284
286 const PropertyPath& path) const {
287 auto property = schema(selection).find(path);
288 if (!property)
289 return eve::editing::failed<DomainOperation>(EditorStatus::Unsupported, RuleId("editor.ui.property-path"),
290 "Unknown UI property: " + path.value());
291 return makeSet(selection, path, property->defaultValue, PropertySetMode::Absolute);
292}
293
294} // namespace eve::ui_editing
ActionParameterOperation operation
double value
Value::Object payload
std::string label
std::map< std::string, Var > values
std::string widget
std::int32_t second
std::int32_t first
std::string text
std::string path
Definition PlayHost.cpp:110
double number
bool found
const char * category
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
Dotted property path (layout.size) compared and persisted as text.
Definition EditingIds.h:78
bool empty() const noexcept
Empty.
Definition EditingIds.h:92
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
const T * getIf() const
Return an immediately borrowed typed value.
PropertyReadResult read(const SelectionSnapshot &selection, const PropertyPath &path) const override
Reads .
PropertySchema schema(const SelectionSnapshot &selection) const override
Schema.
Result< DomainOperation > makeReset(const SelectionSnapshot &selection, const PropertyPath &path) const override
Make reset.
Result< DomainOperation > makeSet(const SelectionSnapshot &selection, const PropertyPath &path, const EditorValue &value, PropertySetMode mode) const override
Make set.
eve::Result< eve::Revision > currentRevision(const SelectionSnapshot &selection) const override
Current revision.
const char * defaultValue
editing::PropertyDescriptor PropertyDescriptor
editing::PropertyPath PropertyPath
editing::Value EditorValue
Definition UiDocument.h:44
DomainOperation public API.
PropertyReadResult public API.
EVENGINE_API_PLATFORM public API.
std::vector< PropertyDescriptor > properties
PropertyDescriptorLookup find(const PropertyPath &path) const
Finds find.
SelectionItem public API.
SelectionSnapshot public API.
std::vector< SelectionItem > items
Immutable widget snapshot used across editor/runtime boundaries.
Definition UiDocument.h:94