载入中...
搜索中...
未找到
UIAutomationCapabilities.cpp
浏览该文件的文档.
2
3#include "common/config.h"
4
5#if defined(EVENGINE_WEBGPU) && defined(__EMSCRIPTEN__)
6
7namespace eve::ui {
8
10
11} // namespace eve::ui
12
13#else
14
15#include "common/Capability.h"
16#include "common/ECS.h"
17#include "common/UIAutomation.h"
18#include "ui/UIHost.h"
19#include "ui/UISystem.h"
20
21#include <Poco/Dynamic/Var.h>
22#include <Poco/JSON/Array.h>
23#include <Poco/JSON/Object.h>
24#include <Poco/JSON/Stringifier.h>
25
26#include <sstream>
27#include <string>
28#include <vector>
29
30namespace eve::ui {
31namespace {
32
33const char* nodeTypeName(NodeType type) {
34 switch (type) {
35 case NodeType::Window: return "window";
36 case NodeType::Text: return "text";
37 case NodeType::Button: return "button";
38 case NodeType::SameLine: return "sameLine";
39 case NodeType::Group: return "group";
40 case NodeType::Separator: return "separator";
41 case NodeType::Checkbox: return "checkbox";
42 case NodeType::Slider: return "slider";
43 case NodeType::Progress: return "progress";
44 case NodeType::InputText: return "inputText";
45 case NodeType::CollapsingHeader: return "collapsingHeader";
46 case NodeType::Child: return "child";
47 case NodeType::Flex: return "flex";
48 case NodeType::Grid: return "grid";
49 case NodeType::Spacer: return "spacer";
50 case NodeType::Image: return "image";
51 case NodeType::ImageButton: return "imageButton";
52 case NodeType::Combo: return "combo";
53 case NodeType::ScrollList: return "scrollList";
54 case NodeType::Viewport: return "viewport";
55 case NodeType::SearchField: return "searchField";
56 case NodeType::Switch: return "switch";
57 case NodeType::Badge: return "badge";
58 case NodeType::Card: return "card";
59 case NodeType::NinePatchPanel: return "ninePatchPanel";
60 case NodeType::ColorPalette: return "colorPalette";
61 case NodeType::SectionHeader: return "sectionHeader";
62 case NodeType::MenuBar: return "menuBar";
63 case NodeType::Menu: return "menu";
64 case NodeType::MenuItem: return "menuItem";
65 case NodeType::Toolbar: return "toolbar";
66 case NodeType::Toolbox: return "toolbox";
67 case NodeType::Sidebar: return "sidebar";
68 case NodeType::StatusBar: return "statusBar";
69 case NodeType::SplitPane: return "splitPane";
70 }
71 return "unknown";
72}
73
74bool isClickable(const UINode& node) {
75 if (!node.enabled || node.mouseFilter == MouseFilter::Ignore) return false;
76 return node.type == NodeType::Button || node.type == NodeType::ImageButton ||
77 node.type == NodeType::Switch || node.type == NodeType::MenuItem ||
78 node.handlerClick != 0;
79}
80
81const char* focusModeName(FocusMode mode) {
82 switch (mode) {
83 case FocusMode::None: return "none";
84 case FocusMode::Click: return "click";
85 case FocusMode::All: return "all";
86 }
87 return "none";
88}
89
90const char* mouseFilterName(MouseFilter filter) {
91 switch (filter) {
92 case MouseFilter::Stop: return "stop";
93 case MouseFilter::Pass: return "pass";
94 case MouseFilter::Ignore: return "ignore";
95 }
96 return "ignore";
97}
98
99const char* themePresetName(ThemePreset preset) {
100 switch (preset) {
101 case ThemePreset::Inherit: return "inherit";
102 case ThemePreset::Dark: return "dark";
103 case ThemePreset::Light: return "light";
104 }
105 return "inherit";
106}
107
108const char* accessibilityRoleName(AccessibilityRole role) {
109 switch (role) {
110 case AccessibilityRole::Auto: return "auto";
111 case AccessibilityRole::Button: return "button";
112 case AccessibilityRole::Checkbox: return "checkbox";
113 case AccessibilityRole::Slider: return "slider";
114 case AccessibilityRole::Text: return "text";
115 case AccessibilityRole::TextInput: return "textInput";
116 case AccessibilityRole::List: return "list";
117 case AccessibilityRole::ListItem: return "listItem";
118 case AccessibilityRole::Menu: return "menu";
119 case AccessibilityRole::MenuItem: return "menuItem";
120 case AccessibilityRole::Progress: return "progress";
121 case AccessibilityRole::Region: return "region";
122 case AccessibilityRole::Tab: return "tab";
123 case AccessibilityRole::Window: return "window";
124 }
125 return "auto";
126}
127
128std::string stringify(const Poco::Dynamic::Var& value) {
129 std::ostringstream out;
130 Poco::JSON::Stringifier::stringify(value, out, 0, 0);
131 return out.str();
132}
133
134Poco::JSON::Object::Ptr nodeJson(const UIHost::Tree& tree, int index) {
135 Poco::JSON::Object::Ptr out(new Poco::JSON::Object());
136 if (index < 0 || index >= static_cast<int>(tree.nodes.size())) return out;
137
138 const UINode& node = tree.nodes[static_cast<size_t>(index)];
139 out->set("type", nodeTypeName(node.type));
140 out->set("id", node.id);
141 if (!node.key.empty()) out->set("key", node.key);
142 out->set("text", node.text);
143 out->set("visible", node.visible);
144 out->set("enabled", node.enabled);
145 out->set("clickable", isClickable(node));
146 out->set("focusMode", focusModeName(node.focusMode));
147 out->set("focused", node.focused);
148 out->set("tabIndex", node.tabIndex);
149 out->set("mouseFilter", mouseFilterName(node.mouseFilter));
150 out->set("theme", themePresetName(node.themePreset));
151 if (!node.focusPrevious.empty()) out->set("focusPrevious", node.focusPrevious);
152 if (!node.focusNext.empty()) out->set("focusNext", node.focusNext);
153 if (!node.focusLeft.empty()) out->set("focusLeft", node.focusLeft);
154 if (!node.focusRight.empty()) out->set("focusRight", node.focusRight);
155 if (!node.focusUp.empty()) out->set("focusUp", node.focusUp);
156 if (!node.focusDown.empty()) out->set("focusDown", node.focusDown);
157 Poco::JSON::Object::Ptr accessibility(new Poco::JSON::Object());
158 accessibility->set("role", accessibilityRoleName(node.accessibilityRole));
159 accessibility->set("name", node.accessibilityName.empty() ? node.text
160 : node.accessibilityName);
161 if (!node.accessibilityDescription.empty())
162 accessibility->set("description", node.accessibilityDescription);
163 out->set("accessibility", accessibility);
164 if (node.type == NodeType::Checkbox || node.type == NodeType::Switch ||
165 node.type == NodeType::MenuItem)
166 out->set("checked", node.checked);
167 if (node.type == NodeType::Slider || node.type == NodeType::Progress ||
168 node.type == NodeType::Combo || node.type == NodeType::SplitPane) {
169 out->set("value", node.value);
170 out->set("min", node.minValue);
171 out->set("max", node.maxValue);
172 }
173 if (node.type == NodeType::ColorPalette) {
174 Poco::JSON::Array::Ptr color(new Poco::JSON::Array());
175 color->add(static_cast<double>(node.tintR));
176 color->add(static_cast<double>(node.tintG));
177 color->add(static_cast<double>(node.tintB));
178 color->add(static_cast<double>(node.tintA));
179 out->set("color", color);
180 }
181 if (node.type == NodeType::InputText || node.type == NodeType::SearchField ||
182 node.type == NodeType::Combo || node.type == NodeType::MenuItem)
183 out->set("valueText", node.valueText);
184 if (!node.tooltip.empty()) out->set("tooltip", node.tooltip);
185
186 Poco::JSON::Array::Ptr children(new Poco::JSON::Array());
187 for (int child = node.firstChild; child >= 0; child = tree.nodes[static_cast<size_t>(child)].nextSibling) {
188 if (child >= static_cast<int>(tree.nodes.size())) break;
189 children->add(nodeJson(tree, child));
190 }
191 if (children->size() != 0) out->set("children", children);
192 return out;
193}
194
195Poco::JSON::Object::Ptr hostJson(UIHost& host) {
196 Poco::JSON::Object::Ptr out(new Poco::JSON::Object());
197 const auto meta = host.meta();
198 const auto tree = host.tree();
199 out->set("name", meta->name);
200 out->set("visible", meta->visible);
201 out->set("layer", meta->layer);
202 out->set("modal", meta->modal);
203 out->set("overlay", meta->overlay);
204 out->set("nodeCount", static_cast<int>(tree->nodes.size()));
205 if (tree->root >= 0) out->set("root", nodeJson(*tree, tree->root));
206 return out;
207}
208
209std::vector<UIHostHandle> hosts() {
210 std::vector<UIHostHandle> result;
211 if (ecs::current()->getManager<UIHost>() == nullptr) return result;
212 auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
213 for (auto it = view.begin(); it != view.end(); ++it) {
214 auto [meta, tree] = *it;
215 (void)tree;
216 if (UIHost::resolve(meta->entity)) result.push_back(meta->entity);
217 }
218 return result;
219}
220
221struct WidgetMatch {
223 int nodeIndex = -1;
224};
225
226std::vector<WidgetMatch> findWidgets(const std::string& hostName, const std::string& widgetId) {
227 std::vector<WidgetMatch> result;
228 for (UIHostHandle handle : hosts()) {
230 if (!host || (!hostName.empty() && host->get().getName() != hostName)) continue;
231 auto node = host->get().findById(widgetId);
232 if (!node) continue;
233 const auto tree = host->get().tree();
234 result.push_back({handle, static_cast<int>(&node->get() - tree->nodes.data())});
235 }
236 return result;
237}
238
239std::string matchError(const std::string& host, const std::string& widget, size_t matchCount) {
240 if (widget.empty()) return "error: missing widget";
241 if (matchCount == 0) {
242 if (!host.empty()) return "error: widget not found: " + host + "/" + widget;
243 return "error: widget not found: " + widget;
244 }
245 return "error: widget id is ambiguous; specify host: " + widget;
246}
247
248class UIAutomationImpl final : public eve::IUIAutomation {
249public:
250 std::string tree(const std::string& hostName) const override {
251 Poco::JSON::Array::Ptr items(new Poco::JSON::Array());
252 for (UIHostHandle handle : hosts()) {
254 if (!host || (!hostName.empty() && host->get().getName() != hostName)) continue;
255 items->add(hostJson(host->get()));
256 }
257 if (!hostName.empty() && items->size() == 0) return "error: UI host not found: " + hostName;
258
259 Poco::JSON::Object::Ptr out(new Poco::JSON::Object());
260 out->set("hostCount", static_cast<int>(items->size()));
261 out->set("hosts", items);
262 return stringify(Poco::Dynamic::Var(out));
263 }
264
265 std::string get(const std::string& hostName, const std::string& widgetId) const override {
266 const std::vector<WidgetMatch> matches = findWidgets(hostName, widgetId);
267 if (matches.size() != 1) return matchError(hostName, widgetId, matches.size());
268 auto host = UIHost::resolve(matches[0].host);
269 if (!host) return "error: UI host became stale";
270 Poco::JSON::Object::Ptr out = nodeJson(*host->get().tree(), matches[0].nodeIndex);
271 out->set("host", host->get().getName());
272 out->set("hostVisible", host->get().meta()->visible);
273 return stringify(Poco::Dynamic::Var(out));
274 }
275
276 std::string click(const std::string& hostName, const std::string& widgetId) override {
277 const std::vector<WidgetMatch> matches = findWidgets(hostName, widgetId);
278 if (matches.size() != 1) return matchError(hostName, widgetId, matches.size());
279 auto host = UIHost::resolve(matches[0].host);
280 if (!host) return "error: UI host became stale";
281 UIHost& resolvedHost = host->get();
282 const auto tree = resolvedHost.tree();
283 if (matches[0].nodeIndex < 0 || matches[0].nodeIndex >= static_cast<int>(tree->nodes.size()))
284 return "error: UI widget became stale";
285 UINode& node = tree->nodes[static_cast<std::size_t>(matches[0].nodeIndex)];
286 if (!resolvedHost.meta()->visible || !node.visible)
287 return "error: widget is not visible: " + resolvedHost.getName() + "/" + widgetId;
288 if (!isClickable(node)) return "error: widget is not clickable: " + resolvedHost.getName() + "/" + widgetId;
289
290 UIEvent event;
291 event.host = matches[0].host;
292 event.hostName = resolvedHost.getName();
293 event.nodeId = node.id;
294 event.nodeIndex = matches[0].nodeIndex;
295 event.kind = "click";
296 event.handlerIndex = node.handlerClick;
297 UISystem::pendingEvents().push_back(std::move(event));
298
299 Poco::JSON::Object::Ptr out(new Poco::JSON::Object());
300 out->set("queued", true);
301 out->set("host", resolvedHost.getName());
302 out->set("widget", widgetId);
303 out->set("event", "click");
304 return stringify(Poco::Dynamic::Var(out));
305 }
306
307private:
308 static int nodeIndex(const UIHost::Tree& tree, const UINode* node) {
309 if (!node || tree.nodes.empty()) return -1;
310 return static_cast<int>(node - tree.nodes.data());
311 }
312};
313
314} // namespace
315
317 static UIAutomationImpl impl;
318 eve::cap::provide<eve::IUIAutomation>(&impl);
319}
320
321} // namespace eve::ui
322
323#endif
double value
void * impl
std::string widget
PrimitiveHandle handle
glm::mat4 view
const RoadNode * node
std::string filter
int children
Definition TreeMesh.cpp:295
UIHostHandle host
uint32_t index
Semantic retained-UI automation provided by the ui module.
static eve::OptionalRef< UIHost > resolve(UIHostHandle handle) noexcept
Resolves a UI host handle with ECS generation checking.
Definition UIHost.cpp:61
static std::vector< UIEvent > & pendingEvents()
Pending events.
Result< std::string > stringify(const eve::Value &value)
Serialize the canonical owning Value as deterministic compact JSON.
Definition Json.cpp:563
MouseFilter
Pointer event participation and propagation policy.
Definition UIHost.h:85
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
NodeType
Widget node kinds understood by the UI renderer.
Definition UIHost.h:19
FocusMode
Keyboard/gamepad focus policy for an interactive control.
Definition UIHost.h:79
ThemePreset
Built-in theme override inherited by a retained UI subtree.
Definition UIHost.h:88
ecs::EntityHandle UIHostHandle
Definition UIHost.h:16
AccessibilityRole
Platform-neutral accessibility role exposed by controls.
Definition UIHost.h:107
void registerUIAutomationCapabilities()
Registers ui automation capabilities.
glm::vec4 color