载入中...
搜索中...
未找到
UiTheme.cpp
浏览该文件的文档.
3
4#include <algorithm>
5#include <set>
6#include <utility>
7
8namespace eve::ui_editing {
9namespace {
10
11EditorDiagnostic diagnostic(const char* rule, std::string message) {
13 DiagnosticSeverity::Error, std::move(message));
14}
15
16const EditorValue* field(const EditorValue& value, const char* key) {
17 const auto* object = value.getIf<EditorValue::Object>();
18 if (!object) return nullptr;
19 const auto found = object->find(key);
20 return found == object->end() ? nullptr : &found->second;
21}
22
23bool hasError(const std::vector<EditorDiagnostic>& diagnostics) {
24 return std::any_of(diagnostics.begin(), diagnostics.end(),
25 [](const EditorDiagnostic& item) { return item.severity() == DiagnosticSeverity::Error; });
26}
27
28EditorValue assetValue(const UiThemeAsset& asset) {
29 return EditorValue::Object{{"id", asset.id.value()},
30 {"name", asset.name},
31 {"basePreset", presetName(asset.basePreset)},
32 {"tokens", themeTokensValue(asset.tokens)}};
33}
34
35Result<UiThemeAsset> parseAsset(const EditorValue& value) {
36 const EditorValue* idField = field(value, "id");
37 const EditorValue* nameField = field(value, "name");
38 const EditorValue* presetField = field(value, "basePreset");
39 const EditorValue* tokensField = field(value, "tokens");
40 const auto* id = idField ? idField->getIf<std::string>() : nullptr;
41 const auto* name = nameField ? nameField->getIf<std::string>() : nullptr;
42 const auto* preset = presetField ? presetField->getIf<std::string>() : nullptr;
43 if (!id || id->empty() || !name || name->empty() || !preset || !tokensField)
44 return eve::editing::failed<UiThemeAsset>(EditorStatus::Rejected, RuleId("editor.ui-theme.asset"),
45 "Theme asset id, name, basePreset and tokens are required");
46 auto base = parsePreset(*preset);
47 if (!base.ok()) return Result<UiThemeAsset>::failure(base.status());
48 auto tokens = parseThemeTokens(*tokensField);
49 if (!tokens.ok()) return Result<UiThemeAsset>::failure(tokens.status());
50 UiThemeAsset asset;
51 asset.id = ObjectId(*id);
52 asset.name = *name;
53 asset.basePreset = base.value();
54 asset.tokens = std::move(tokens).value();
55 return eve::editing::applied<UiThemeAsset>(std::move(asset));
56}
57
58Result<void> parseCatalog(const EditorValue& content, std::vector<UiThemeAsset>& themes, ObjectId& activeId) {
59 const EditorValue* activeField = field(content, "activeId");
60 const EditorValue* themesField = field(content, "themes");
61 const auto* active = activeField ? activeField->getIf<std::string>() : nullptr;
62 const auto* list = themesField ? themesField->getIf<EditorValue::Array>() : nullptr;
63 if (!active || active->empty() || !list)
64 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui-theme.catalog"),
65 "Theme catalog requires activeId and a themes array");
66 std::vector<UiThemeAsset> parsed;
67 parsed.reserve(list->size());
68 for (const EditorValue& entry : *list) {
69 auto asset = parseAsset(entry);
70 if (!asset.ok()) return Result<void>::failure(asset.status());
71 parsed.push_back(std::move(asset).value());
72 }
73 themes = std::move(parsed);
74 activeId = ObjectId(*active);
75 return eve::editing::applied<void>();
76}
77
78} // namespace
79
80UiThemeCatalogTarget::UiThemeCatalogTarget(std::string id) : id_(std::move(id)) {
81 UiThemeAsset dark;
82 dark.id = ObjectId("dark");
83 dark.name = "Dark";
85 dark.tokens = ui::Theme::dark();
87 light.id = ObjectId("light");
88 light.name = "Light";
90 light.tokens = ui::Theme::light();
91 themes_.push_back(std::move(dark));
92 themes_.push_back(std::move(light));
93 activeId_ = ObjectId("dark");
94}
95
97 return {TargetId(id_),
98 "ui-theme-catalog",
100 false,
102}
103
104void* UiThemeCatalogTarget::queryCapability(const CapabilityId& capability) {
105 if (capability == propertyCapabilityId()) return static_cast<IPropertyProvider*>(this);
107 return static_cast<IEditingSnapshotProvider*>(this);
108 return nullptr;
109}
110
111EditorValue UiThemeCatalogTarget::contentValue() const {
113 themes.reserve(themes_.size());
114 for (const UiThemeAsset& asset : themes_) themes.push_back(assetValue(asset));
115 return EditorValue::Object{{"activeId", activeId_.value()}, {"themes", std::move(themes)}};
116}
117
118Result<DomainOperation> UiThemeCatalogTarget::replacement(EditorValue content, std::string property) const {
120 operation.type = "ui.theme.catalog.replace.v1";
121 operation.inverseType = operation.type;
122 operation.target = TargetId(id_);
123 operation.payload = std::move(content);
124 operation.inverse = contentValue();
125 operation.hasInverse = true;
126 if (!property.empty()) operation.affectedProperties.push_back(property);
127 operation.mergeKey = "ui-theme:" + id_ + ":" + (property.empty() ? "structure" : property);
128 return eve::editing::applied<DomainOperation>(std::move(operation));
129}
130
131const UiThemeAsset* UiThemeCatalogTarget::findTheme(const ObjectId& id) const {
132 for (const UiThemeAsset& asset : themes_)
133 if (asset.id == id) return &asset;
134 return nullptr;
135}
136
137UiThemeAsset* UiThemeCatalogTarget::mutableTheme(const ObjectId& id) {
138 for (UiThemeAsset& asset : themes_)
139 if (asset.id == id) return &asset;
140 return nullptr;
141}
142
144 const UiThemeAsset* asset = findTheme(id);
145 if (!asset)
146 return eve::editing::failed<UiThemeAsset>(EditorStatus::NotFound, RuleId("editor.ui-theme.missing"),
147 "Theme asset does not exist: " + id.value());
148 return eve::editing::applied<UiThemeAsset>(*asset);
149}
150
151std::string UiThemeCatalogTarget::runtimeName(const ObjectId& id) const {
152 if (id.value() == "dark" || id.value() == "light") return id.value();
153 return "custom";
154}
155
156std::vector<EditorDiagnostic> UiThemeCatalogTarget::validate() const {
157 std::vector<EditorDiagnostic> diagnostics;
158 if (themes_.empty())
159 diagnostics.push_back(diagnostic("editor.ui-theme.empty", "Theme catalog must contain at least one asset"));
160 if (themes_.size() > 32)
161 diagnostics.push_back(diagnostic("editor.ui-theme.budget", "Theme catalog exceeds 32 named assets"));
162 std::set<std::string> ids;
163 std::set<std::string> names;
164 bool activeFound = false;
165 for (const UiThemeAsset& asset : themes_) {
166 if (asset.id.empty() || !ids.insert(asset.id.value()).second)
167 diagnostics.push_back(diagnostic("editor.ui-theme.id", "Theme asset id is empty or duplicated"));
168 if (asset.name.empty() || !names.insert(asset.name).second)
169 diagnostics.push_back(diagnostic("editor.ui-theme.name", "Theme asset name is empty or duplicated"));
170 if (asset.id == activeId_) activeFound = true;
171 auto tokenDiagnostics = validateThemeTokens(asset.tokens);
172 diagnostics.insert(diagnostics.end(), tokenDiagnostics.begin(), tokenDiagnostics.end());
173 }
174 if (!activeFound)
175 diagnostics.push_back(diagnostic("editor.ui-theme.active", "Active theme id is missing from the catalog"));
176 return diagnostics;
177}
178
180 if (operation.target != TargetId(id_))
181 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui-theme.target"),
182 "Theme operation targets another catalog");
183 if (operation.type != "ui.theme.catalog.replace.v1")
184 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.ui-theme.operation"),
185 "Unsupported theme operation: " + operation.type);
186 auto candidate = *this;
187 auto parsed = parseCatalog(operation.payload, candidate.themes_, candidate.activeId_);
188 if (!parsed.ok()) return parsed;
189 if (hasError(candidate.validate()))
190 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui-theme.invalid"),
191 "Theme catalog replacement is invalid");
192 themes_ = std::move(candidate.themes_);
193 activeId_ = candidate.activeId_;
194 bumpRevision();
195 widenDirty(0, 0);
196 return eve::editing::applied<void>();
197}
198
199std::unique_ptr<IDomainOperationTarget> UiThemeCatalogTarget::cloneDomainState() const {
200 return std::make_unique<UiThemeCatalogTarget>(*this);
201}
202
203Result<void> UiThemeCatalogTarget::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
204 auto* catalog = dynamic_cast<UiThemeCatalogTarget*>(candidate.get());
205 if (!catalog || catalog->id_ != id_)
206 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.ui-theme.candidate"),
207 "Theme compensation candidate belongs to another catalog");
208 *this = *catalog;
209 return eve::editing::applied<void>();
210}
211
213 UiThemeBasePreset preset) const {
214 if (id.empty() || name.empty())
215 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.create"),
216 "Theme id and name are required");
217 if (preset == UiThemeBasePreset::Custom)
218 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.preset-source"),
219 "New themes must be created from dark or light");
220 if (findTheme(id) ||
221 std::any_of(themes_.begin(), themes_.end(), [&](const UiThemeAsset& asset) { return asset.name == name; }))
222 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict, RuleId("editor.ui-theme.exists"),
223 "Theme id or name already exists");
224 auto candidate = *this;
225 UiThemeAsset asset;
226 asset.id = id;
227 asset.name = std::move(name);
228 asset.basePreset = preset;
229 asset.tokens = themeFromPreset(preset);
230 candidate.themes_.push_back(std::move(asset));
231 if (hasError(candidate.validate()))
232 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.invalid"),
233 "Creating the theme would invalidate the catalog");
234 return replacement(candidate.contentValue());
235}
236
238 std::string name) const {
239 const UiThemeAsset* original = findTheme(source);
240 if (!original)
241 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui-theme.missing"),
242 "Theme asset does not exist");
243 if (id.empty() || name.empty() || findTheme(id) ||
244 std::any_of(themes_.begin(), themes_.end(), [&](const UiThemeAsset& asset) { return asset.name == name; }))
245 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.duplicate"),
246 "Duplicated theme needs a unique id and name");
247 auto candidate = *this;
248 UiThemeAsset asset = *original;
249 asset.id = id;
250 asset.name = std::move(name);
252 candidate.themes_.push_back(std::move(asset));
253 if (hasError(candidate.validate()))
254 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.invalid"),
255 "Duplicating the theme would invalidate the catalog");
256 return replacement(candidate.contentValue());
257}
258
259Result<DomainOperation> UiThemeCatalogTarget::makeRename(const ObjectId& id, std::string name) const {
260 const UiThemeAsset* current = findTheme(id);
261 if (!current)
262 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui-theme.missing"),
263 "Theme asset does not exist");
264 if (name.empty())
265 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.empty-name"),
266 "Theme name must not be empty");
267 if (std::any_of(themes_.begin(), themes_.end(),
268 [&](const UiThemeAsset& asset) { return asset.id != id && asset.name == name; }))
269 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict, RuleId("editor.ui-theme.name"),
270 "Theme name already exists");
271 auto candidate = *this;
272 candidate.mutableTheme(id)->name = std::move(name);
273 return replacement(candidate.contentValue(), "theme.name");
274}
275
277 if (!findTheme(id))
278 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui-theme.missing"),
279 "Theme asset does not exist");
280 if (themes_.size() <= 1)
281 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.last"),
282 "The last theme asset cannot be deleted");
283 auto candidate = *this;
284 std::erase_if(candidate.themes_, [&](const UiThemeAsset& asset) { return asset.id == id; });
285 if (candidate.activeId_ == id) candidate.activeId_ = candidate.themes_.front().id;
286 if (hasError(candidate.validate()))
287 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.invalid"),
288 "Deleting the theme would invalidate the catalog");
289 return replacement(candidate.contentValue());
290}
291
293 if (!findTheme(id))
294 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui-theme.missing"),
295 "Theme asset does not exist");
296 auto candidate = *this;
297 candidate.activeId_ = id;
298 return replacement(candidate.contentValue(), "catalog.active");
299}
300
302 const UiThemeAsset* current = findTheme(id);
303 if (!current)
304 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.ui-theme.missing"),
305 "Theme asset does not exist");
306 if (current->basePreset == UiThemeBasePreset::Custom)
307 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.ui-theme.reset"),
308 "Custom themes have no built-in base preset to restore");
309 auto candidate = *this;
310 candidate.mutableTheme(id)->tokens = themeFromPreset(current->basePreset);
311 return replacement(candidate.contentValue(), "theme.tokens");
312}
313
315 return EditorValue::Object{{"schemaVersion", int64_t{1}}, {"content", contentValue()}};
316}
317
319 const EditorValue* versionValue = field(snapshot, "schemaVersion");
320 const EditorValue* content = field(snapshot, "content");
321 const auto* version = versionValue ? versionValue->getIf<int64_t>() : nullptr;
322 if (!version || *version != 1 || !content)
323 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.ui-theme.snapshot"),
324 "Theme snapshot requires schemaVersion 1 and content");
325 auto candidate = *this;
326 auto parsed = parseCatalog(*content, candidate.themes_, candidate.activeId_);
327 if (!parsed.ok()) return parsed;
328 if (hasError(candidate.validate()))
329 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.ui-theme.snapshot-invalid"),
330 "Theme snapshot failed validation");
331 themes_ = std::move(candidate.themes_);
332 activeId_ = candidate.activeId_;
333 bumpRevision();
335 return eve::editing::applied<void>();
336}
337
338} // namespace eve::ui_editing
ActionParameterOperation operation
double value
bool & active
std::string message
std::unordered_map< const Graphics *, std::shared_ptr< Lifetime > > tokens
std::uint32_t key
glm::uvec4 ids
std::string name
std::string id
Definition PlayHost.cpp:108
bool found
double current
Light3D::Data * light
const UnitySourceAsset & source
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
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.
virtual void clearDirtyRegion()=0
Clears dirty region.
Optional capability exposing a deterministic, persistence-safe editing snapshot.
static CapabilityId editingCapabilityId()
Stable capability identity shared by every editable domain target.
UI-independent property access for an immutable selection snapshot.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
Serializable catalog of named UI themes with one active publication slot.
Definition UiTheme.h:60
void * queryCapability(const CapabilityId &capability) override
Query Inspector and snapshot capabilities.
Definition UiTheme.cpp:104
Result< DomainOperation > makeDelete(const ObjectId &id) const
Make delete.
Definition UiTheme.cpp:276
Result< DomainOperation > makeSetActive(const ObjectId &id) const
Make set active.
Definition UiTheme.cpp:292
Result< DomainOperation > makeResetToBase(const ObjectId &id) const
Make reset to base.
Definition UiTheme.cpp:301
std::string runtimeName(const ObjectId &id) const
Runtime name written to ui::globalThemeName(): dark, light, or custom.
Definition UiTheme.cpp:151
Result< DomainOperation > makeRename(const ObjectId &id, std::string name) const
Make rename.
Definition UiTheme.cpp:259
EditorValue snapshotValue() const override
Snapshot value.
Definition UiTheme.cpp:314
Result< void > loadSnapshot(const EditorValue &snapshot)
Loads snapshot.
Definition UiTheme.cpp:318
std::vector< EditorDiagnostic > validate() const
Validate.
Definition UiTheme.cpp:156
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
Definition UiTheme.cpp:203
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
Definition UiTheme.cpp:179
UiThemeCatalogTarget(std::string id)
Construct a catalog seeded with built-in dark and light assets.
Definition UiTheme.cpp:80
Result< UiThemeAsset > theme(const ObjectId &id) const
Copy one named theme, or NotFound.
Definition UiTheme.cpp:143
Result< DomainOperation > makeCreateFromPreset(const ObjectId &id, std::string name, UiThemeBasePreset preset) const
Make create from preset.
Definition UiTheme.cpp:212
static CapabilityId propertyCapabilityId()
Inspector capability published by describe().
Definition UiTheme.h:69
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
Definition UiTheme.cpp:199
TargetDescriptor describe() const override
Describe.
Definition UiTheme.cpp:96
const std::vector< UiThemeAsset > & themes() const noexcept
Borrow authored themes in catalog order.
Definition UiTheme.h:91
Result< DomainOperation > makeDuplicate(const ObjectId &source, const ObjectId &id, std::string name) const
Make duplicate.
Definition UiTheme.cpp:237
editing::DomainOperation DomainOperation
editing::ObjectId ObjectId
editing::TargetId TargetId
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
ui_editing::UiThemeAsset UiThemeAsset
const EditorValue * field(const EditorValue &value, const char *name)
ui::Theme themeFromPreset(UiThemeBasePreset preset)
Theme from preset.
editing::Diagnostic EditorDiagnostic
Definition UiDocument.h:45
std::vector< EditorDiagnostic > validateThemeTokens(const ui::Theme &theme)
Validate theme tokens.
std::string presetName(UiThemeBasePreset preset)
Preset name.
Result< UiThemeBasePreset > parsePreset(const std::string &name)
Parse preset.
UiThemeBasePreset
Base preset used when resetting a named theme asset.
Definition UiTheme.h:44
editing::Value EditorValue
Definition UiDocument.h:44
Result< ui::Theme > parseThemeTokens(const EditorValue &value)
Parse theme tokens.
EditorValue themeTokensValue(const ui::Theme &theme)
Theme tokens value.
WidgetDesc list(std::string listId, const std::vector< std::string > &items, const std::function< WidgetDesc(const std::string &, int)> &itemFn)
Expand a string list into a Group of item widgets. itemFn(label, index) builds each row; keys default...
Definition Widget.cpp:715
DomainOperation public API.
Stable discovery metadata exposed by every editable target.
static Theme light()
Light.
Definition Theme.cpp:180
static Theme dark()
Built-in presets with matching geometry; only colors differ.
Definition Theme.cpp:174
One named, fully snapshotted Theme asset.
Definition UiTheme.h:47
UiThemeBasePreset basePreset
Definition UiTheme.h:50