载入中...
搜索中...
未找到
UISystem.cpp
浏览该文件的文档.
1#include "ui/UISystem.h"
2
3#include "ui/Layout.h"
4#include "ui/Theme.h"
5#include "ui/UIBackend.h"
6
7#include "common/Module.h"
8#include "graphics/Graphics.h"
9
10#include <imgui.h>
11
12#include <algorithm>
13#include <chrono>
14#include <cmath>
15#include <cstring>
16#include <map>
17#include <vector>
18
19namespace eve::ui {
20namespace {
21
22std::vector<UIEvent> g_pending;
23std::vector<UIClick> g_clicks;
24std::vector<UIChange> g_changes;
25UIBackend *g_backend = nullptr;
26UIStats g_stats;
27std::map<std::string, ViewportState> g_viewports;
28
29std::string viewportKey(UIHost *host, const UINode &n) {
30 const std::string hostName =
31 host && !host->meta()->name.empty() ? host->meta()->name : "?";
32 return hostName + "/" + (n.id.empty() ? "viewport" : n.id);
33}
34
35void pushPending(UIHost *host, const UINode &n, const char *kind, uint32_t handlerIndex,
36 bool toggleValue = false, float floatValue = 0.f, std::string textValue = {}) {
37 UIEvent ev;
38 ev.host = host;
39 ev.hostName = host ? host->meta()->name : "";
40 ev.nodeId = n.id;
41 ev.kind = kind;
42 ev.handlerIndex = handlerIndex;
43 ev.toggleValue = toggleValue;
44 ev.floatValue = floatValue;
45 ev.textValue = std::move(textValue);
46 g_pending.push_back(std::move(ev));
47}
48
49void walkNode(UIHost *host, UIHost::Tree *tree, int index);
50
51void walkSiblings(UIHost *host, UIHost::Tree *tree, int index) {
52 while (index >= 0 && index < int(tree->nodes.size())) {
53 const int next = tree->nodes[size_t(index)].nextSibling;
54 walkNode(host, tree, index);
55 index = next;
56 }
57}
58
59void emitSpacer(float main, float cross, bool row) {
60 if (main < 0.f) main = 0.f;
61 if (cross < 0.f) cross = 0.f;
62 if (row) {
63 if (main <= 0.f && cross <= 0.f) {
64 ImGui::Dummy(ImVec2(0.f, 0.f));
65 } else {
66 ImGui::Dummy(ImVec2(main, cross > 0.f ? cross : 1.f));
67 }
68 } else {
69 if (main <= 0.f && cross <= 0.f) {
70 ImGui::Dummy(ImVec2(0.f, 0.f));
71 } else {
72 ImGui::Dummy(ImVec2(cross > 0.f ? cross : 1.f, main));
73 }
74 }
75}
76
78void drawNinePatch(void *handle, const ImVec2 &pos, const ImVec2 &size, const ImVec2 &uv0,
79 const ImVec2 &uv1, float bL, float bT, float bR, float bB, int texW, int texH,
80 ImU32 tint) {
81 ImDrawList *dl = ImGui::GetWindowDrawList();
82 const auto draw = [&](float x0, float y0, float x1, float y1, float u0, float v0, float u1,
83 float v1) {
84 dl->AddImage(static_cast<ImTextureID>(handle), ImVec2(pos.x + x0, pos.y + y0),
85 ImVec2(pos.x + x1, pos.y + y1), ImVec2(u0, v0), ImVec2(u1, v1), tint);
86 };
87 if (bL <= 0.f && bT <= 0.f && bR <= 0.f && bB <= 0.f) {
88 draw(0.f, 0.f, size.x, size.y, uv0.x, uv0.y, uv1.x, uv1.y);
89 return;
90 }
91 if (texW <= 0 || texH <= 0) {
92 draw(0.f, 0.f, size.x, size.y, uv0.x, uv0.y, uv1.x, uv1.y);
93 return;
94 }
95 const float uPerPx = (uv1.x - uv0.x) / float(texW);
96 const float vPerPx = (uv1.y - uv0.y) / float(texH);
97 const float uL = uv0.x + std::min(bL, size.x * 0.5f) * uPerPx;
98 const float uR = uv1.x - std::min(bR, size.x * 0.5f) * uPerPx;
99 const float vT = uv0.y + std::min(bT, size.y * 0.5f) * vPerPx;
100 const float vB = uv1.y - std::min(bB, size.y * 0.5f) * vPerPx;
101 const float bl = std::min(bL, size.x * 0.5f);
102 const float br = std::min(bR, size.x * 0.5f);
103 const float bt = std::min(bT, size.y * 0.5f);
104 const float bb = std::min(bB, size.y * 0.5f);
105 // Corners
106 draw(0.f, 0.f, bl, bt, uv0.x, uv0.y, uL, vT);
107 draw(size.x - br, 0.f, size.x, bt, uR, uv0.y, uv1.x, vT);
108 draw(0.f, size.y - bb, bl, size.y, uv0.x, vB, uL, uv1.y);
109 draw(size.x - br, size.y - bb, size.x, size.y, uR, vB, uv1.x, uv1.y);
110 // Edges
111 draw(bl, 0.f, size.x - br, bt, uL, uv0.y, uR, vT);
112 draw(bl, size.y - bb, size.x - br, size.y, uL, vB, uR, uv1.y);
113 draw(0.f, bt, bl, size.y - bb, uv0.x, vT, uL, vB);
114 draw(size.x - br, bt, size.x, size.y - bb, uR, vT, uv1.x, vB);
115 // Center
116 draw(bl, bt, size.x - br, size.y - bb, uL, vT, uR, vB);
117}
118
119void walkFlex(UIHost *host, UIHost::Tree *tree, UINode &flex) {
121 const ImGuiStyle &style = ImGui::GetStyle();
122 const float gap = flex.gap >= 0.f ? flex.gap : (row ? style.ItemSpacing.x : style.ItemSpacing.y);
123
124 // Own content box: an explicit size (set by the parent's arrange) wins;
125 // otherwise use the available region (root flex inside a window/child).
126 const ImVec2 avail = ImGui::GetContentRegionAvail();
127 const float flexW = flex.sizeX > 0.f ? flex.sizeX : avail.x;
128 const float flexH = flex.sizeY > 0.f ? flex.sizeY : avail.y;
129 const float padMain = row ? flex.paddingL + flex.paddingR : flex.paddingT + flex.paddingB;
130 const float padCross = row ? flex.paddingT + flex.paddingB : flex.paddingL + flex.paddingR;
131 const float availMain = (row ? flexW : flexH) - padMain;
132 const float availCross = (row ? flexH : flexW) - padCross;
133
134 std::vector<int> kids;
135 std::vector<int> absKids;
136 std::vector<FlexItemSpec> specs;
137 for (int c = flex.firstChild; c >= 0; c = tree->nodes[size_t(c)].nextSibling) {
138 if (c >= int(tree->nodes.size())) break;
139 UINode &child = tree->nodes[size_t(c)];
140 if (!child.visible) continue;
141 if (child.absolute) {
142 absKids.push_back(c);
143 continue;
144 }
145 kids.push_back(c);
146 FlexItemSpec s;
147 s.basisMain = row ? child.measuredW : child.measuredH;
148 s.basisCross = row ? child.measuredH : child.measuredW;
149 s.marginBefore = row ? child.marginL : child.marginT;
150 s.marginAfter = row ? child.marginR : child.marginB;
151 s.marginCrossBefore = row ? child.marginT : child.marginL;
152 s.marginCrossAfter = row ? child.marginB : child.marginR;
153 s.flexGrow = child.flexGrow;
154 s.isSpacer = child.type == NodeType::Spacer;
155 s.minMain = row ? child.minSizeX : child.minSizeY;
156 s.maxMain = row ? child.maxSizeX : child.maxSizeY;
157 s.minCross = row ? child.minSizeY : child.minSizeX;
158 s.maxCross = row ? child.maxSizeY : child.maxSizeX;
159 s.percentMain = row ? child.percentW : child.percentH;
160 s.percentCross = row ? child.percentH : child.percentW;
161 s.explicitMain = row ? child.sizeX : child.sizeY;
162 s.explicitCross = row ? child.sizeY : child.sizeX;
163 specs.push_back(s);
164 }
165
166 const FlexResult res =
167 flexArrange(row, gap, std::max(0.f, availMain), std::max(0.f, availCross),
169 const ImVec2 cursor = ImGui::GetCursorScreenPos();
170 const ImVec2 origin = ImVec2(cursor.x + flex.paddingL, cursor.y + flex.paddingT);
171
172 for (size_t i = 0; i < kids.size(); ++i) {
173 UINode &child = tree->nodes[size_t(kids[i])];
174 const FlexRect &r = res.items[i];
175 ImGui::SetCursorScreenPos(ImVec2(origin.x + r.x, origin.y + r.y));
176 const float oldX = child.sizeX;
177 const float oldY = child.sizeY;
178 bool pushedWidth = false;
179 if (r.w > 0.f) child.sizeX = r.w;
180 if (r.h > 0.f) child.sizeY = r.h;
181 if (row || r.w > 0.f) {
182 ImGui::PushItemWidth(r.w > 0.f ? r.w : 1.f);
183 pushedWidth = true;
184 }
185 walkNode(host, tree, kids[i]);
186 child.sizeX = oldX;
187 child.sizeY = oldY;
188 if (pushedWidth) ImGui::PopItemWidth();
189 }
190
191 // Absolutely placed children are excluded from flow; draw on top of it.
192 for (int c : absKids) {
193 UINode &child = tree->nodes[size_t(c)];
194 float w = child.percentW > 0.f ? child.percentW * flexW : child.measuredW;
195 float h = child.percentH > 0.f ? child.percentH * flexH : child.measuredH;
196 if (child.sizeX > 0.f) w = child.sizeX;
197 if (child.sizeY > 0.f) h = child.sizeY;
198 if (child.minSizeX > 0.f) w = std::max(w, child.minSizeX);
199 if (child.minSizeY > 0.f) h = std::max(h, child.minSizeY);
200 if (child.maxSizeX > 0.f) w = std::min(w, child.maxSizeX);
201 if (child.maxSizeY > 0.f) h = std::min(h, child.maxSizeY);
202 const float x = child.anchorX * flexW + child.posX - child.anchorX * w;
203 const float y = child.anchorY * flexH + child.posY - child.anchorY * h;
204 ImGui::SetCursorScreenPos(ImVec2(origin.x + x, origin.y + y));
205 const float oldX = child.sizeX;
206 const float oldY = child.sizeY;
207 child.sizeX = w;
208 child.sizeY = h;
209 if (w > 0.f) ImGui::PushItemWidth(w);
210 walkNode(host, tree, c);
211 child.sizeX = oldX;
212 child.sizeY = oldY;
213 if (w > 0.f) ImGui::PopItemWidth();
214 }
215}
216
217void walkNode(UIHost *host, UIHost::Tree *tree, int index) {
218 if (index < 0 || index >= int(tree->nodes.size())) return;
219 UINode &n = tree->nodes[size_t(index)];
220 if (!n.visible) return;
221
222 switch (n.type) {
223 case NodeType::Window: {
224 std::string title = n.text.empty() ? "Window" : n.text;
225 if (host && !host->meta()->name.empty()) title = host->meta()->name + "/" + title;
226 const bool modal = host && host->meta()->modal;
227 ImGuiWindowFlags flags = 0;
228 float winW = 0.f;
229 float winH = 0.f;
230 if (host && host->meta()->percentW > 0.f)
231 winW = host->meta()->percentW * ImGui::GetIO().DisplaySize.x;
232 else if (host && host->meta()->hasSize)
233 winW = host->meta()->sizeX;
234 if (host && host->meta()->percentH > 0.f)
235 winH = host->meta()->percentH * ImGui::GetIO().DisplaySize.y;
236 else if (host && host->meta()->hasSize)
237 winH = host->meta()->sizeY;
238 if (host && host->meta()->hasPos) {
239 const ImVec2 display = ImGui::GetIO().DisplaySize;
240 const float px = host->meta()->pivotX;
241 const float py = host->meta()->pivotY;
242 const float x = host->meta()->anchorX * display.x + host->meta()->posX - px * winW;
243 const float y = host->meta()->anchorY * display.y + host->meta()->posY - py * winH;
244 ImGui::SetNextWindowPos(ImVec2(x, y), ImGuiCond_Always,
245 ImVec2(host->meta()->pivotX, host->meta()->pivotY));
246 flags |= ImGuiWindowFlags_NoMove;
247 if (winW <= 0.f && winH <= 0.f) flags |= ImGuiWindowFlags_AlwaysAutoResize;
248 }
249 if (winW > 0.f || winH > 0.f) {
250 ImGui::SetNextWindowSize(
251 ImVec2(winW > 0.f ? winW : -1.f, winH > 0.f ? winH : -1.f), ImGuiCond_Always);
252 } else if (n.measuredW > 0.f || n.measuredH > 0.f) {
253 // Real measured size → stable auto-resize instead of ImGui's guess.
254 ImGui::SetNextWindowContentSize(ImVec2(n.measuredW, n.measuredH));
255 }
256 if (host && host->meta()->overlay) {
257 flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
258 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse |
259 ImGuiWindowFlags_AlwaysAutoResize;
260 ImGui::SetNextWindowBgAlpha(0.4f);
261 }
262 if (modal) {
263 ImGui::OpenPopup(title.c_str());
264 if (ImGui::BeginPopupModal(title.c_str(), nullptr,
265 ImGuiWindowFlags_AlwaysAutoResize | flags)) {
266 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
267 ImGui::EndPopup();
268 }
269 } else {
270 ImGui::Begin(title.c_str(), nullptr, flags);
271 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
272 ImGui::End();
273 }
274 break;
275 }
276 case NodeType::Text:
277 if (n.wrapWidth > 0.f) {
278 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + n.wrapWidth);
279 ImGui::TextUnformatted(n.text.c_str());
280 ImGui::PopTextWrapPos();
281 } else {
282 ImGui::TextUnformatted(n.text.c_str());
283 }
284 break;
285 case NodeType::Button: {
286 const char *label = n.text.empty() ? "Button" : n.text.c_str();
287 const bool sized = n.sizeX > 0.f || n.sizeY > 0.f;
288 const bool clicked =
289 sized ? ImGui::Button(label, ImVec2(n.sizeX > 0.f ? n.sizeX : 0.f,
290 n.sizeY > 0.f ? n.sizeY : 0.f))
291 : ImGui::Button(label);
292 if (clicked) pushPending(host, n, "click", n.handlerClick);
293 break;
294 }
296 ImGui::SameLine();
297 break;
298 case NodeType::Group:
299 ImGui::BeginGroup();
300 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
301 ImGui::EndGroup();
302 break;
304 ImGui::Separator();
305 break;
306 case NodeType::Checkbox: {
307 bool checked = n.checked;
308 if (ImGui::Checkbox(n.text.empty() ? "Check" : n.text.c_str(), &checked)) {
309 n.checked = checked;
310 pushPending(host, n, "toggle", n.handlerToggle, checked);
311 }
312 break;
313 }
314 case NodeType::Slider: {
315 float v = n.value;
316 const char *label = n.text.empty() ? "Slider" : n.text.c_str();
317 if (ImGui::SliderFloat(label, &v, n.minValue, n.maxValue)) {
318 n.value = v;
319 pushPending(host, n, "value", n.handlerValue, false, v);
320 }
321 break;
322 }
323 case NodeType::Progress: {
324 const char *overlay = n.text.empty() ? nullptr : n.text.c_str();
325 ImGui::ProgressBar(n.value, ImVec2(-1.f, 0.f), overlay);
326 break;
327 }
328 case NodeType::Image: {
329 const float w = n.sizeX > 0.f ? n.sizeX : 32.f;
330 const float h = n.sizeY > 0.f ? n.sizeY : 32.f;
331 const ImVec2 pos = ImGui::GetCursorScreenPos();
332 const ImVec2 size(w, h);
333 const ImU32 tint =
334 ImGui::ColorConvertFloat4ToU32(ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
335 if (n.textureId != 0 && g_backend) {
336 void *handle = g_backend->textureHandle(n.textureId);
337 int tw = 0, th = 0;
338 g_backend->textureSize(n.textureId, &tw, &th);
339 drawNinePatch(handle, pos, size, ImVec2(n.uv0x, n.uv0y), ImVec2(n.uv1x, n.uv1y),
340 n.borderL, n.borderT, n.borderR, n.borderB, tw, th, tint);
341 } else {
342 ImGui::GetWindowDrawList()->AddRectFilled(pos, ImVec2(pos.x + w, pos.y + h), tint,
343 n.cornerRadius);
344 }
345 ImGui::Dummy(size);
346 break;
347 }
349 const float w = n.sizeX > 0.f ? n.sizeX : 32.f;
350 const float h = n.sizeY > 0.f ? n.sizeY : 32.f;
351 const ImVec2 size(w, h);
352 const std::string sid = n.id.empty() ? "imgbtn" : n.id;
353 ImGui::PushID(sid.c_str());
354 bool clicked = false;
355 if (n.textureId != 0 && g_backend) {
356 void *handle = g_backend->textureHandle(n.textureId);
357 if (handle) {
358 clicked = ImGui::ImageButton(static_cast<ImTextureID>(handle), size,
359 ImVec2(n.uv0x, n.uv0y), ImVec2(n.uv1x, n.uv1y), -1,
360 ImVec4(0.f, 0.f, 0.f, 0.f),
361 ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
362 }
363 }
364 if (!clicked && n.textureId == 0) {
365 clicked = ImGui::InvisibleButton(sid.c_str(), size);
366 const ImVec2 pmin = ImGui::GetItemRectMin();
367 const ImVec2 pmax = ImGui::GetItemRectMax();
368 ImU32 col =
369 ImGui::ColorConvertFloat4ToU32(ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
370 if (ImGui::IsItemHovered()) col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
371 ImGui::GetWindowDrawList()->AddRectFilled(pmin, pmax, col, n.cornerRadius);
372 }
373 ImGui::PopID();
374 if (clicked) pushPending(host, n, "click", n.handlerClick);
375 break;
376 }
377 case NodeType::Combo: {
378 // Options are newline-separated in valueText; selected index in value.
379 std::vector<std::string> storage;
380 size_t start = 0;
381 while (start <= n.valueText.size()) {
382 const size_t end = n.valueText.find('\n', start);
383 storage.push_back(n.valueText.substr(
384 start, end == std::string::npos ? std::string::npos : end - start));
385 if (end == std::string::npos) break;
386 start = end + 1;
387 }
388 std::vector<const char *> items;
389 items.reserve(storage.size());
390 for (const auto &s : storage) items.push_back(s.c_str());
391 int idx = int(n.value);
392 const char *label = n.text.empty() ? "Combo" : n.text.c_str();
393 if (ImGui::Combo(label, &idx, items.data(), int(items.size()))) {
394 n.value = float(idx);
395 pushPending(host, n, "value", n.handlerValue, false, float(idx));
396 }
397 break;
398 }
399 case NodeType::InputText: {
400 char buf[1024];
401 std::memset(buf, 0, sizeof(buf));
402 if (!n.valueText.empty()) {
403 std::strncpy(buf, n.valueText.c_str(), sizeof(buf) - 1);
404 }
405 const char *label = n.text.empty() ? "Input" : n.text.c_str();
406 if (ImGui::InputText(label, buf, sizeof(buf))) {
407 n.valueText = buf;
408 pushPending(host, n, "text", n.handlerText, false, 0.f, n.valueText);
409 }
410 break;
411 }
413 ImGuiTreeNodeFlags flags = n.open ? ImGuiTreeNodeFlags_DefaultOpen : 0;
414 const char *label = n.text.empty() ? "Section" : n.text.c_str();
415 if (ImGui::CollapsingHeader(label, flags)) {
416 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
417 }
418 break;
419 }
420 case NodeType::Child: {
421 ImVec2 size(n.sizeX, n.sizeY);
422 const char *cid = n.id.empty() ? "child" : n.id.c_str();
423 if (ImGui::BeginChild(cid, size, true)) {
424 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
425 }
426 ImGui::EndChild();
427 break;
428 }
430 // Virtualized list: only the rows intersecting the viewport are drawn.
431 std::vector<int> kids;
432 for (int c = n.firstChild; c >= 0; c = tree->nodes[size_t(c)].nextSibling) {
433 if (c >= int(tree->nodes.size())) break;
434 if (tree->nodes[size_t(c)].visible) kids.push_back(c);
435 }
436 const int total = int(kids.size());
437 const char *cid = n.id.empty() ? "scrolllist" : n.id.c_str();
438 const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f,
439 n.sizeY > 0.f ? n.sizeY : 120.f);
440 ImGui::BeginChild(cid, size, true);
441 const float itemH = n.itemHeight > 0.f ? n.itemHeight : ImGui::GetFrameHeight();
442 const float contentW = ImGui::GetContentRegionAvail().x;
443 const float scrollY = ImGui::GetScrollY();
444 const float viewH = ImGui::GetWindowHeight();
445 if (total > 0 && itemH > 0.f) {
446 int first = int(scrollY / itemH);
447 if (first < 0) first = 0;
448 if (first > total) first = total;
449 int last = int(std::ceil((scrollY + viewH) / itemH));
450 if (last < first) last = first;
451 if (last > total) last = total;
452 for (int i = first; i < last; ++i) {
453 UINode &child = tree->nodes[size_t(kids[size_t(i)])];
454 ImGui::SetCursorPos(ImVec2(0.f, float(i) * itemH));
455 const float oldX = child.sizeX;
456 const float oldY = child.sizeY;
457 if (contentW > 0.f) child.sizeX = contentW;
458 if (contentW > 0.f) ImGui::PushItemWidth(contentW);
459 walkNode(host, tree, kids[size_t(i)]);
460 child.sizeX = oldX;
461 child.sizeY = oldY;
462 if (contentW > 0.f) ImGui::PopItemWidth();
463 // Force the row to occupy exactly itemH so rows stay uniform.
464 ImGui::SetCursorPosY(float(i) * itemH + itemH);
465 }
466 }
467 // Establish the full scrollable extent (scrollbar reflects total rows).
468 ImGui::SetCursorPosY(float(total) * itemH);
469 ImGui::EndChild();
470 break;
471 }
472 case NodeType::Viewport: {
473 const float w = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
474 const float h = n.sizeY > 0.f ? n.sizeY : ImGui::GetContentRegionAvail().y;
475 const ImVec2 size(std::max(1.f, w), std::max(1.f, h));
476 const std::string key = viewportKey(host, n);
477 ViewportState *vs = UISystem::ensureViewport(key, int(size.x), int(size.y));
478
479 const ImVec2 rectMin = ImGui::GetCursorScreenPos();
480 const std::string label = "##vp" + (n.id.empty() ? std::string("viewport") : n.id);
481 ImGui::InvisibleButton(label.c_str(), size);
482 const bool hovered = ImGui::IsItemHovered();
483 const bool active = ImGui::IsItemActive();
484 const ImVec2 mouse = ImGui::GetMousePos();
485 if (vs) {
486 vs->hovered = hovered;
487 vs->active = active;
488 vs->mouseX = mouse.x - rectMin.x;
489 vs->mouseY = mouse.y - rectMin.y;
490 if (active) {
491 vs->dragDX = ImGui::GetIO().MouseDelta.x;
492 vs->dragDY = ImGui::GetIO().MouseDelta.y;
493 } else {
494 vs->dragDX = 0.f;
495 vs->dragDY = 0.f;
496 }
497 vs->wheel = hovered ? ImGui::GetIO().MouseWheel : 0.f;
498 }
499
500 if (vs && vs->textureId && g_backend) {
501 void *handle = g_backend->textureHandle(vs->textureId);
502 if (handle) {
503 ImGui::SetCursorScreenPos(rectMin);
504 ImGui::Image(static_cast<ImTextureID>(handle), size);
505 } else {
506 ImGui::GetWindowDrawList()->AddRectFilled(
507 rectMin, ImVec2(rectMin.x + size.x, rectMin.y + size.y),
508 IM_COL32(10, 12, 16, 255));
509 }
510 } else {
511 ImGui::GetWindowDrawList()->AddRectFilled(
512 rectMin, ImVec2(rectMin.x + size.x, rectMin.y + size.y), IM_COL32(10, 12, 16, 255));
513 }
514 break;
515 }
516 case NodeType::Flex:
517 walkFlex(host, tree, n);
518 break;
519 case NodeType::Spacer:
520 // Outside Flex: honor explicit size, otherwise a tiny dummy.
521 emitSpacer(n.sizeX > 0.f ? n.sizeX : 0.f, n.sizeY > 0.f ? n.sizeY : 0.f, true);
522 break;
523 }
524}
525
526void walk(UIHost *host, UIHost::Tree *tree, int index) { walkSiblings(host, tree, index); }
527
528} // namespace
529
530std::vector<UIEvent> &UISystem::pendingEvents() { return g_pending; }
531std::vector<UIClick> &UISystem::clickQueue() { return g_clicks; }
532std::vector<UIChange> &UISystem::changeQueue() { return g_changes; }
533
534void UISystem::setBackend(UIBackend *backend) { g_backend = backend; }
535UIBackend *UISystem::backend() { return g_backend; }
536const UIStats &UISystem::stats() { return g_stats; }
537
538ViewportState *UISystem::ensureViewport(const std::string &key, int w, int h) {
539 if (key.empty() || w <= 0 || h <= 0) return nullptr;
540 ViewportState &vs = g_viewports[key];
541 vs.key = key;
542 auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>("Graphics");
543 if (!gfx) return &vs;
544 if (!vs.canvas || vs.width != w || vs.height != h) {
545 if (vs.textureId && g_backend) g_backend->unregisterTexture(vs.textureId);
546 vs.textureId = 0;
547 vs.canvas = gfx->newCanvas(w, h);
548 vs.width = w;
549 vs.height = h;
550 if (vs.canvas && g_backend)
551 vs.textureId = g_backend->registerTexture(vs.canvas->getTexture());
552 }
553 return &vs;
554}
555
556ViewportState *UISystem::viewportState(const std::string &hostName, const std::string &nodeId) {
557 auto it = g_viewports.find(hostName + "/" + nodeId);
558 return it == g_viewports.end() ? nullptr : &it->second;
559}
560
561UIHost *UISystem::findHost(const std::string &name) {
562 if (name.empty()) return nullptr;
563 if (ecs::current()->getManager<UIHost>() == nullptr) return nullptr;
564 auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
565 for (auto it = view.begin(); it != view.end(); ++it) {
566 auto [meta, tree] = *it;
567 (void)tree;
568 if (meta->name == name) return meta->entity;
569 }
570 return nullptr;
571}
572
574 if (ownerId == 0) return nullptr;
575 if (ecs::current()->getManager<UIHost>() == nullptr) return nullptr;
576 auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
577 for (auto it = view.begin(); it != view.end(); ++it) {
578 auto [meta, tree] = *it;
579 (void)tree;
580 if (meta->ownerId == ownerId) return meta->entity;
581 }
582 return nullptr;
583}
584
586 if (ecs::current()->getManager<UIHost>() == nullptr) return;
587
589
590 struct Item {
591 UIHost *host;
592 UIHost::Meta *meta;
593 UIHost::Tree *tree;
594 };
595 std::vector<Item> items;
596
597 auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
598 for (auto it = view.begin(); it != view.end(); ++it) {
599 auto [meta, tree] = *it;
600 if (!meta->visible) continue;
601 UIHost *host = meta->entity;
602 if (!host) continue;
603 items.push_back(Item{host, meta, tree});
604 }
605
606 std::stable_sort(items.begin(), items.end(),
607 [](const Item &a, const Item &b) { return a.meta->layer < b.meta->layer; });
608
609 g_stats.hostCount = int(items.size());
610 g_stats.nodeCount = 0;
611 g_stats.measureMs = 0.0;
612 g_stats.walkMs = 0.0;
613 for (auto &item : items) {
614 item.tree->dirty = false;
615 // Real measure pass: nested containers, text metrics, margins/min/max.
616 const auto m0 = std::chrono::steady_clock::now();
617 measureTree(*item.tree);
618 const auto m1 = std::chrono::steady_clock::now();
619 g_stats.nodeCount += int(item.tree->nodes.size());
620 if (item.tree->root >= 0) walk(item.host, item.tree, item.tree->root);
621 const auto m2 = std::chrono::steady_clock::now();
622 g_stats.measureMs +=
623 std::chrono::duration<double, std::milli>(m1 - m0).count();
624 g_stats.walkMs += std::chrono::duration<double, std::milli>(m2 - m1).count();
625 }
626}
627
629 auto events = std::move(g_pending);
630 g_pending.clear();
631 for (auto &ev : events) {
632 if (!ev.host) continue;
633
634 if (ev.kind == "click" && !ev.nodeId.empty()) {
635 UIClick c;
636 c.hostName = ev.hostName.empty() ? ev.host->meta()->name : ev.hostName;
637 c.nodeId = ev.nodeId;
638 g_clicks.push_back(std::move(c));
639 }
640
641 if ((ev.kind == "toggle" || ev.kind == "value" || ev.kind == "text") && !ev.nodeId.empty()) {
642 UIChange ch;
643 ch.hostName = ev.hostName.empty() ? ev.host->meta()->name : ev.hostName;
644 ch.nodeId = ev.nodeId;
645 ch.kind = ev.kind;
646 g_changes.push_back(std::move(ch));
647 }
648
649 if (ev.kind == "toggle" && ev.handlerIndex != 0) {
650 auto t = ev.host->tree();
651 size_t idx = size_t(ev.handlerIndex - 1);
652 if (idx < t->toggleHandlers.size() && t->toggleHandlers[idx])
653 t->toggleHandlers[idx](ev.toggleValue);
654 continue;
655 }
656 if (ev.kind == "value" && ev.handlerIndex != 0) {
657 auto t = ev.host->tree();
658 size_t idx = size_t(ev.handlerIndex - 1);
659 if (idx < t->valueHandlers.size() && t->valueHandlers[idx])
660 t->valueHandlers[idx](ev.floatValue);
661 continue;
662 }
663 if (ev.kind == "text" && ev.handlerIndex != 0) {
664 auto t = ev.host->tree();
665 size_t idx = size_t(ev.handlerIndex - 1);
666 if (idx < t->textHandlers.size() && t->textHandlers[idx])
667 t->textHandlers[idx](ev.textValue);
668 continue;
669 }
670 if (ev.handlerIndex == 0) continue;
671 auto t = ev.host->tree();
672 size_t idx = size_t(ev.handlerIndex - 1);
673 if (idx >= t->clickHandlers.size()) continue;
674 if (t->clickHandlers[idx]) t->clickHandlers[idx]();
675 }
676}
677
679 if (g_clicks.empty()) return {};
680 UIClick c = std::move(g_clicks.front());
681 g_clicks.erase(g_clicks.begin());
682 if (c.hostName.empty()) return c.nodeId;
683 return c.hostName + "/" + c.nodeId;
684}
685
686std::string UISystem::consumeClickFor(const std::string &hostName) {
687 for (auto it = g_clicks.begin(); it != g_clicks.end(); ++it) {
688 if (it->hostName != hostName) continue;
689 std::string id = std::move(it->nodeId);
690 g_clicks.erase(it);
691 return id;
692 }
693 return {};
694}
695
697 if (g_changes.empty()) return {};
698 UIChange c = std::move(g_changes.front());
699 g_changes.erase(g_changes.begin());
700 if (c.hostName.empty()) return c.nodeId;
701 return c.hostName + "/" + c.nodeId;
702}
703
704} // namespace eve::ui
bool active
Definition CardTypes.cpp:34
Tok kind
std::map< std::string, ImVec2 > rectMin
std::string title
std::vector< HostEvent > events
std::string id
int y
Definition Grass.cpp:135
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
std::vector< Colorf > px
uint32_t a
uint32_t b
uint32_t c
int idx
glm::mat4 view
const char * name
Definition RockMesh.cpp:21
int v
uint32_t s
Definition Weather.cpp:28
virtual Texture * getTexture()=0
Sampleable color buffer; screen Canvas returns nullptr.
Platform / renderer backend for declarative UI. Concrete implementations live under ui/<backend>/ (e....
Definition UIBackend.h:18
ECS mount point for one UI panel/screen. Subclass to attach UI to game entities, e....
Definition UIHost.h:130
static ViewportState * ensureViewport(const std::string &key, int w, int h)
Definition UISystem.cpp:538
static UIBackend * backend()
Definition UISystem.cpp:535
static const UIStats & stats()
Definition UISystem.cpp:536
static std::vector< UIClick > & clickQueue()
Definition UISystem.cpp:531
static void setBackend(UIBackend *backend)
Definition UISystem.cpp:534
static std::string consumeChange()
Pop next change as "name/node"; empty if none.
Definition UISystem.cpp:696
static std::vector< UIChange > & changeQueue()
Definition UISystem.cpp:532
static UIHost * findHost(const std::string &name)
Lookup by Meta.name across the UIHost View.
Definition UISystem.cpp:561
static void dispatchEvents()
Definition UISystem.cpp:628
static UIHost * findHostByOwner(uint32_t ownerId)
First host with Meta.ownerId == ownerId, or nullptr.
Definition UISystem.cpp:573
static std::string consumeClick()
Pop next click as "name/node"; empty if none.
Definition UISystem.cpp:678
static void render()
Walk all UIHost (+ subclasses) via ECS View.
Definition UISystem.cpp:585
static std::string consumeClickFor(const std::string &hostName)
Pop next click for a specific host; returns node id only (or "").
Definition UISystem.cpp:686
static std::vector< UIEvent > & pendingEvents()
Definition UISystem.cpp:530
static ViewportState * viewportState(const std::string &hostName, const std::string &nodeId)
Definition UISystem.cpp:556
int main(int argc, char **argv)
Definition main.cpp:56
void applyThemeToImGui(const Theme &theme)
Push tokens into ImGui style. Metrics are multiplied by uiScale (default: themeUiScale()).
Definition Theme.cpp:135
WidgetDesc text(std::string content, std::string id)
Static text label.
Definition Widget.cpp:235
void measureTree(UIHost::Tree &tree)
Definition Layout.cpp:348
WidgetDesc flex(FlexDirection direction, std::vector< WidgetDesc > children, std::string id)
Elastic layout container (row/column). Prefer row / column shorthands.
Definition Widget.cpp:421
FlexResult flexArrange(bool row, float gap, float availMain, float availCross, FlexAlign containerAlign, FlexJustify justify, const std::vector< FlexItemSpec > &items)
Definition Layout.cpp:24
Theme & globalTheme()
Definition Theme.cpp:99
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:387
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:431
Value/text/toggle change for script: "hostName/nodeId".
Definition UISystem.h:36
std::string kind
Definition UISystem.h:39
std::string nodeId
Definition UISystem.h:38
std::string hostName
Definition UISystem.h:37
One script-facing click: "hostName/nodeId".
Definition UISystem.h:30
std::string hostName
Definition UISystem.h:31
graphics::Canvas * canvas
Definition UISystem.h:57
NodeType type
Definition Widget.h:14
FlexDirection flexDirection
Definition Widget.h:65
FlexJustify justifyContent
Definition Widget.h:67
FlexAlign alignItems
Definition Widget.h:66