载入中...
搜索中...
未找到
UISystem.cpp
浏览该文件的文档.
1#include "ui/UISystem.h"
2
3#include "ui/Layout.h"
4#include "ui/Icons.h"
5#include "ui/Theme.h"
7#include "ui/UIBackend.h"
9
10#include "common/Module.h"
11#include "common/Profile.h"
12#include "graphics/Graphics.h"
13#include "graphics/ClipSpace.h"
15
16#include <imgui.h>
17#include <imgui_internal.h>
18
19#include <algorithm>
20#include <chrono>
21#include <cmath>
22#include <cstdio>
23#include <cstring>
24#include <iterator>
25#include <map>
26#include <string>
27#include <vector>
28
29namespace eve::ui {
30namespace {
31
32std::vector<UIEvent> g_pending;
33std::vector<UIClick> g_clicks;
34std::vector<UIChange> g_changes;
35std::vector<UIDrop> g_drops;
36std::vector<std::string> g_pendingFileDrops;
37UIBackend *g_backend = nullptr;
38UIStats g_stats;
39std::map<std::string, ViewportState> g_viewports;
40
41struct HostThemeScope {
42 bool active = false;
43 ImGuiStyle previousStyle;
44 float previousFontScale = 1.f;
45 ImGuiConfigFlags previousConfigFlags = ImGuiConfigFlags_None;
46
47 explicit HostThemeScope(UIHost *host) {
48 if (!host || !host->hasThemeOverride() || !host->themeOverride()) return;
49 previousStyle = ImGui::GetStyle();
50 previousFontScale = ImGui::GetIO().FontGlobalScale;
51 previousConfigFlags = ImGui::GetIO().ConfigFlags;
52 applyThemeToImGui(*host->themeOverride());
53 active = true;
54 }
55
56 ~HostThemeScope() {
57 if (!active) return;
58 ImGui::GetStyle() = previousStyle;
59 ImGui::GetIO().FontGlobalScale = previousFontScale;
60 ImGui::GetIO().ConfigFlags = previousConfigFlags;
61 }
62
63 HostThemeScope(const HostThemeScope &) = delete;
64 HostThemeScope &operator=(const HostThemeScope &) = delete;
65};
66
67struct ActiveDragPayload {
68 uint64_t serial = 0;
69 std::string sourceHostName;
70 std::string sourceNodeId;
71 std::string payloadType;
72 std::string payloadText;
73};
74
75std::optional<ActiveDragPayload> g_activeDrag;
76uint64_t g_nextDragSerial = 1;
77
78bool acceptsDropType(const UINode &node, const std::string &type) {
79 return node.dropTarget && (node.acceptedDropType == "*" || node.acceptedDropType == type);
80}
81
82void renderDragDrop(UIHost *host, const UINode &node) {
84 !node.enabled)
85 return;
86 const std::string hostName = host->meta()->name;
87 if (node.dragSource && !node.dragPayloadType.empty() && ImGui::BeginDragDropSource()) {
88 if (!g_activeDrag || g_activeDrag->sourceHostName != hostName ||
89 g_activeDrag->sourceNodeId != node.id ||
90 g_activeDrag->payloadType != node.dragPayloadType ||
91 g_activeDrag->payloadText != node.dragPayloadText) {
92 g_activeDrag = ActiveDragPayload{g_nextDragSerial++, hostName, node.id,
93 node.dragPayloadType, node.dragPayloadText};
94 }
95 const uint64_t serial = g_activeDrag->serial;
96 ImGui::SetDragDropPayload("EVE_UI_DND", &serial, sizeof(serial), ImGuiCond_Once);
97 ImGui::TextUnformatted(node.dragPayloadText.empty() ? node.dragPayloadType.c_str()
98 : node.dragPayloadText.c_str());
99 ImGui::EndDragDropSource();
100 }
101
102 if (node.dropTarget && ImGui::BeginDragDropTarget()) {
103 const bool acceptsActive = g_activeDrag &&
104 acceptsDropType(node, g_activeDrag->payloadType);
105 if (const ImGuiPayload *payload =
106 acceptsActive ? ImGui::AcceptDragDropPayload("EVE_UI_DND") : nullptr) {
107 if (payload->IsDelivery() && payload->DataSize == int(sizeof(uint64_t)) &&
108 g_activeDrag) {
109 const uint64_t serial = *static_cast<const uint64_t *>(payload->Data);
110 if (serial == g_activeDrag->serial) {
111 g_drops.push_back({DragDropOrigin::Internal,
112 g_activeDrag->sourceHostName,
113 g_activeDrag->sourceNodeId,
114 hostName,
115 node.id,
116 g_activeDrag->payloadType,
117 g_activeDrag->payloadText});
118 g_activeDrag.reset();
119 }
120 }
121 }
122 ImGui::EndDragDropTarget();
123 }
124
125 if (!g_pendingFileDrops.empty() && acceptsDropType(node, "file") &&
126 ImGui::IsItemHovered()) {
127 for (auto &path : g_pendingFileDrops) {
128 g_drops.push_back({DragDropOrigin::OperatingSystemFile, "", "", hostName,
129 node.id, "file", std::move(path)});
130 }
131 g_pendingFileDrops.clear();
132 }
133}
134
135graphics::Camera3D *activeCamera() {
136 if (ecs::current()->getManager<graphics::Camera3D>() == nullptr) return nullptr;
137 auto cameras = ecs::View<graphics::Camera3D, graphics::Camera3D::Data>();
138 for (auto it = cameras.begin(); it != cameras.end(); ++it) {
139 auto [data] = *it;
140 if (data->active) return data->entity;
141 }
142 return nullptr;
143}
144
145bool updateWorldAnchor(UIHost &host, graphics::Camera3D *camera, float width, float height) {
146 auto anchor = host.worldAnchor();
147 if (!anchor->enabled) {
149 return true;
150 }
151 if (!camera || width <= 0.f || height <= 0.f) {
153 return false;
154 }
155 const auto data = camera->data();
156 const glm::vec3 eye(data->eyeX, data->eyeY, data->eyeZ);
157 const glm::vec3 target(data->targetX, data->targetY, data->targetZ);
158 const glm::vec3 up(data->upX, data->upY, data->upZ);
159 const glm::mat4 view = glm::lookAtRH(eye, target, up);
160 const glm::mat4 projection = graphics::cameraProjectionVulkanRH_ZO(
161 data->orthographic, glm::radians(data->fovYDeg), data->orthoHeight,
162 width / height, data->nearZ, data->farZ);
163 const auto projected = projectWorldAnchor(*anchor, projection * view, data->eyeX,
164 data->eyeY, data->eyeZ, width, height);
165 anchor->state = projected.state;
166 anchor->screenX = projected.screenX;
167 anchor->screenY = projected.screenY;
168 anchor->depth = projected.depth;
169 anchor->scale = projected.scale;
170 anchor->displacementX = 0.f;
171 anchor->displacementY = 0.f;
172 return projected.render;
173}
174
175std::string viewportKey(UIHost *host, const UINode &n) {
176 const std::string hostName =
177 host && !host->meta()->name.empty() ? host->meta()->name : "?";
178 return hostName + "/" + (n.id.empty() ? "viewport" : n.id);
179}
180
181void pushPending(UIHost *host, const UINode &n, const char *kind, uint32_t handlerIndex,
182 bool toggleValue = false, float floatValue = 0.f, std::string textValue = {}) {
183 if (!n.enabled || n.mouseFilter == MouseFilter::Ignore) return;
184 UIEvent ev;
185 if (!host) return;
186 ev.host = host->handle();
187 ev.hostName = host ? host->meta()->name : "";
188 ev.nodeId = n.id;
189 if (host) {
190 auto tree = host->tree();
191 if (!tree->nodes.empty()) ev.nodeIndex = int(&n - tree->nodes.data());
192 }
193 ev.kind = kind;
194 ev.handlerIndex = handlerIndex;
195 ev.toggleValue = toggleValue;
196 ev.floatValue = floatValue;
197 ev.textValue = std::move(textValue);
198 g_pending.push_back(std::move(ev));
199}
200
201std::string nodeLabel(const UINode &node, const char *fallback) {
202 std::string label = node.text.empty() ? fallback : node.text;
203 if (!node.id.empty()) label += "###" + node.id;
204 return label;
205}
206
207bool privateUseGlyph(const std::string &text, ImWchar *out) {
208 if (text.size() != 3) return false;
209 const auto b0 = static_cast<unsigned char>(text[0]);
210 const auto b1 = static_cast<unsigned char>(text[1]);
211 const auto b2 = static_cast<unsigned char>(text[2]);
212 if ((b0 & 0xf0u) != 0xe0u || (b1 & 0xc0u) != 0x80u || (b2 & 0xc0u) != 0x80u)
213 return false;
214 const unsigned int codepoint =
215 ((b0 & 0x0fu) << 12u) | ((b1 & 0x3fu) << 6u) | (b2 & 0x3fu);
216 if (codepoint < 0xe000u || codepoint > 0xf8ffu) return false;
217 if (out) *out = static_cast<ImWchar>(codepoint);
218 return true;
219}
220
221bool isInteractive(NodeType type) {
222 switch (type) {
223 case NodeType::Button:
225 case NodeType::Slider:
228 case NodeType::Combo:
231 case NodeType::Switch:
233 case NodeType::MenuItem: return true;
234 default: return false;
235 }
236}
237
238struct PaletteSwatch {
239 float r = 1.f;
240 float g = 1.f;
241 float b = 1.f;
242 float a = 1.f;
243};
244
245void appendDefaultPaletteSwatches(std::vector<PaletteSwatch> &out) {
246 static const PaletteSwatch kDefaults[] = {
247 {0.00f, 0.00f, 0.00f, 1.f}, {0.20f, 0.20f, 0.22f, 1.f},
248 {0.45f, 0.45f, 0.48f, 1.f}, {0.92f, 0.92f, 0.94f, 1.f},
249 {0.82f, 0.18f, 0.18f, 1.f}, {0.90f, 0.45f, 0.12f, 1.f},
250 {0.92f, 0.78f, 0.16f, 1.f}, {0.22f, 0.68f, 0.28f, 1.f},
251 {0.14f, 0.70f, 0.72f, 1.f}, {0.18f, 0.38f, 0.86f, 1.f},
252 {0.48f, 0.28f, 0.82f, 1.f}, {0.82f, 0.22f, 0.62f, 1.f},
253 {0.55f, 0.35f, 0.22f, 1.f}, {0.95f, 0.55f, 0.70f, 1.f},
254 {0.42f, 0.52f, 0.22f, 1.f}, {0.12f, 0.18f, 0.32f, 1.f},
255 };
256 out.insert(out.end(), std::begin(kDefaults), std::end(kDefaults));
257}
258
259bool parsePaletteSwatch(std::string token, PaletteSwatch &out) {
260 while (!token.empty() && (token.back() == ' ' || token.back() == '\r' || token.back() == '\t'))
261 token.pop_back();
262 size_t start = 0;
263 while (start < token.size() && (token[start] == ' ' || token[start] == '\t')) ++start;
264 token = token.substr(start);
265 if (token.empty()) return false;
266 if (token[0] == '#') {
267 unsigned value = 0;
268 if (token.size() == 7 && std::sscanf(token.c_str() + 1, "%06x", &value) == 1) {
269 out.r = float((value >> 16) & 0xff) / 255.f;
270 out.g = float((value >> 8) & 0xff) / 255.f;
271 out.b = float(value & 0xff) / 255.f;
272 out.a = 1.f;
273 return true;
274 }
275 if (token.size() == 9 && std::sscanf(token.c_str() + 1, "%08x", &value) == 1) {
276 out.r = float((value >> 24) & 0xff) / 255.f;
277 out.g = float((value >> 16) & 0xff) / 255.f;
278 out.b = float((value >> 8) & 0xff) / 255.f;
279 out.a = float(value & 0xff) / 255.f;
280 return true;
281 }
282 return false;
283 }
284 float r = 0.f, g = 0.f, b = 0.f, a = 1.f;
285 const int n = std::sscanf(token.c_str(), "%f,%f,%f,%f", &r, &g, &b, &a);
286 if (n < 3) return false;
287 out.r = r;
288 out.g = g;
289 out.b = b;
290 if (n >= 4) out.a = a;
291 return true;
292}
293
294std::vector<PaletteSwatch> paletteSwatches(const UINode &n) {
295 std::vector<PaletteSwatch> swatches;
296 const std::string &src = n.valueText;
297 size_t i = 0;
298 while (i < src.size()) {
299 size_t end = src.find_first_of(";\n", i);
300 if (end == std::string::npos) end = src.size();
301 PaletteSwatch swatch;
302 if (parsePaletteSwatch(src.substr(i, end - i), swatch)) swatches.push_back(swatch);
303 i = end + 1;
304 }
305 if (swatches.empty()) appendDefaultPaletteSwatches(swatches);
306 return swatches;
307}
308
309void commitPaletteColor(UIHost *host, UINode &n, const float col[4]) {
310 n.tintR = col[0];
311 n.tintG = col[1];
312 n.tintB = col[2];
313 n.tintA = col[3];
314 n.value = col[0];
315 pushPending(host, n, "value", n.handlerValue, false, col[0]);
316}
317
318bool keyPressed(ImGuiKey key) {
319 const int index = ImGui::GetKeyIndex(key);
320 return index >= 0 && ImGui::IsKeyPressed(index, false);
321}
322
323void routeExplicitFocusInput(UIHost *host, const UINode &node) {
324 if (!host || !node.focused || node.focusMode != FocusMode::All) return;
325 if (keyPressed(ImGuiKey_Tab)) {
326 const bool backwards = ImGui::GetIO().KeyShift;
327 const std::string &target = backwards ? node.focusPrevious : node.focusNext;
328 if (!target.empty())
329 host->moveFocus(backwards ? FocusDirection::Previous : FocusDirection::Next);
330 return;
331 }
332 if (ImGui::IsItemActive()) return;
333 if (!node.focusLeft.empty() && keyPressed(ImGuiKey_LeftArrow))
334 host->moveFocus(FocusDirection::Left);
335 else if (!node.focusRight.empty() && keyPressed(ImGuiKey_RightArrow))
336 host->moveFocus(FocusDirection::Right);
337 else if (!node.focusUp.empty() && keyPressed(ImGuiKey_UpArrow))
338 host->moveFocus(FocusDirection::Up);
339 else if (!node.focusDown.empty() && keyPressed(ImGuiKey_DownArrow))
340 host->moveFocus(FocusDirection::Down);
341}
342
343void walkNode(UIHost *host, UIHost::Tree *tree, int index);
344
345void walkSiblings(UIHost *host, UIHost::Tree *tree, int index) {
346 while (index >= 0 && index < int(tree->nodes.size())) {
347 const int next = tree->nodes[size_t(index)].nextSibling;
348 UINode &node = tree->nodes[size_t(index)];
349 // Desktop composition behaves like CSS grid rows: menu/toolbar and
350 // status bar keep their intrinsic height while the workspace consumes
351 // the remaining block size. An explicit split height still wins.
352 if (node.type == NodeType::SplitPane && node.sizeY <= 0.f && next >= 0 &&
353 next < int(tree->nodes.size()) &&
354 tree->nodes[size_t(next)].type == NodeType::StatusBar) {
355 const float oldY = node.sizeY;
356 const float reserve = tree->nodes[size_t(next)].measuredH +
357 ImGui::GetStyle().ItemSpacing.y;
358 node.sizeY = std::max(1.f, ImGui::GetContentRegionAvail().y - reserve);
359 walkNode(host, tree, index);
360 node.sizeY = oldY;
361 } else {
362 walkNode(host, tree, index);
363 }
364 index = next;
365 }
366}
367
368void walkPaneChild(UIHost *host, UIHost::Tree *tree, int index) {
369 if (index < 0 || index >= int(tree->nodes.size())) return;
370 UINode &child = tree->nodes[size_t(index)];
371 const ImVec2 avail = ImGui::GetContentRegionAvail();
372 const float oldX = child.sizeX;
373 const float oldY = child.sizeY;
374 // A direct SplitPane child fills its allocated pane. Descendants can still
375 // specialize their own dimensions through normal widget layout props.
376 child.sizeX = std::max(1.f, avail.x);
377 child.sizeY = std::max(1.f, avail.y);
378 child.measuredW = child.sizeX;
379 child.measuredH = child.sizeY;
380 walkNode(host, tree, index);
381 child.sizeX = oldX;
382 child.sizeY = oldY;
383}
384
385bool hasDirectChildType(const UIHost::Tree &tree, const UINode &parent, NodeType type) {
386 for (int child = parent.firstChild; child >= 0;
387 child = tree.nodes[size_t(child)].nextSibling) {
388 if (child >= int(tree.nodes.size())) break;
389 if (tree.nodes[size_t(child)].type == type) return true;
390 }
391 return false;
392}
393
394void emitSpacer(float main, float cross, bool row) {
395 if (main < 0.f) main = 0.f;
396 if (cross < 0.f) cross = 0.f;
397 if (row) {
398 if (main <= 0.f && cross <= 0.f) {
399 ImGui::Dummy(ImVec2(0.f, 0.f));
400 } else {
401 ImGui::Dummy(ImVec2(main, cross > 0.f ? cross : 1.f));
402 }
403 } else {
404 if (main <= 0.f && cross <= 0.f) {
405 ImGui::Dummy(ImVec2(0.f, 0.f));
406 } else {
407 ImGui::Dummy(ImVec2(cross > 0.f ? cross : 1.f, main));
408 }
409 }
410}
411
413void drawNinePatch(void *handle, const ImVec2 &pos, const ImVec2 &size, const ImVec2 &uv0,
414 const ImVec2 &uv1, float bL, float bT, float bR, float bB, int texW, int texH,
415 ImU32 tint) {
416 ImDrawList *dl = ImGui::GetWindowDrawList();
417 const auto draw = [&](float x0, float y0, float x1, float y1, float u0, float v0, float u1,
418 float v1) {
419 dl->AddImage(static_cast<ImTextureID>(handle), ImVec2(pos.x + x0, pos.y + y0),
420 ImVec2(pos.x + x1, pos.y + y1), ImVec2(u0, v0), ImVec2(u1, v1), tint);
421 };
422 if (bL <= 0.f && bT <= 0.f && bR <= 0.f && bB <= 0.f) {
423 draw(0.f, 0.f, size.x, size.y, uv0.x, uv0.y, uv1.x, uv1.y);
424 return;
425 }
426 if (texW <= 0 || texH <= 0) {
427 draw(0.f, 0.f, size.x, size.y, uv0.x, uv0.y, uv1.x, uv1.y);
428 return;
429 }
430 const float uPerPx = (uv1.x - uv0.x) / float(texW);
431 const float vPerPx = (uv1.y - uv0.y) / float(texH);
432 const float uL = uv0.x + std::min(bL, size.x * 0.5f) * uPerPx;
433 const float uR = uv1.x - std::min(bR, size.x * 0.5f) * uPerPx;
434 const float vT = uv0.y + std::min(bT, size.y * 0.5f) * vPerPx;
435 const float vB = uv1.y - std::min(bB, size.y * 0.5f) * vPerPx;
436 const float bl = std::min(bL, size.x * 0.5f);
437 const float br = std::min(bR, size.x * 0.5f);
438 const float bt = std::min(bT, size.y * 0.5f);
439 const float bb = std::min(bB, size.y * 0.5f);
440 // Corners
441 draw(0.f, 0.f, bl, bt, uv0.x, uv0.y, uL, vT);
442 draw(size.x - br, 0.f, size.x, bt, uR, uv0.y, uv1.x, vT);
443 draw(0.f, size.y - bb, bl, size.y, uv0.x, vB, uL, uv1.y);
444 draw(size.x - br, size.y - bb, size.x, size.y, uR, vB, uv1.x, uv1.y);
445 // Edges
446 draw(bl, 0.f, size.x - br, bt, uL, uv0.y, uR, vT);
447 draw(bl, size.y - bb, size.x - br, size.y, uL, vB, uR, uv1.y);
448 draw(0.f, bt, bl, size.y - bb, uv0.x, vT, uL, vB);
449 draw(size.x - br, bt, size.x, size.y - bb, uR, vT, uv1.x, vB);
450 // Center
451 draw(bl, bt, size.x - br, size.y - bb, uL, vT, uR, vB);
452}
453
455void queueNinePatch(uint64_t textureId, const ImVec2 &pos, const ImVec2 &size,
456 const ImVec2 &uv0, const ImVec2 &uv1, float bL, float bT, float bR,
457 float bB, int texW, int texH, float r, float g, float b, float a,
458 bool opaque = false) {
459 if (!g_backend) return;
460 const auto draw = [&](float x0, float y0, float x1, float y1, float u0, float v0,
461 float u1, float v1) {
462 if (x1 <= x0 || y1 <= y0) return;
463 g_backend->queueTextureDraw(textureId, pos.x + x0, pos.y + y0, x1 - x0, y1 - y0,
464 u0, v0, u1, v1, r, g, b, a, opaque);
465 };
466 if ((bL <= 0.f && bT <= 0.f && bR <= 0.f && bB <= 0.f) || texW <= 0 || texH <= 0) {
467 draw(0.f, 0.f, size.x, size.y, uv0.x, uv0.y, uv1.x, uv1.y);
468 return;
469 }
470 const float bl = std::min(bL, size.x * 0.5f);
471 const float br = std::min(bR, size.x * 0.5f);
472 const float bt = std::min(bT, size.y * 0.5f);
473 const float bb = std::min(bB, size.y * 0.5f);
474 const float uPerPx = (uv1.x - uv0.x) / float(texW);
475 const float vPerPx = (uv1.y - uv0.y) / float(texH);
476 const float uL = uv0.x + bl * uPerPx;
477 const float uR = uv1.x - br * uPerPx;
478 const float vT = uv0.y + bt * vPerPx;
479 const float vB = uv1.y - bb * vPerPx;
480 draw(0.f, 0.f, bl, bt, uv0.x, uv0.y, uL, vT);
481 draw(size.x - br, 0.f, size.x, bt, uR, uv0.y, uv1.x, vT);
482 draw(0.f, size.y - bb, bl, size.y, uv0.x, vB, uL, uv1.y);
483 draw(size.x - br, size.y - bb, size.x, size.y, uR, vB, uv1.x, uv1.y);
484 draw(bl, 0.f, size.x - br, bt, uL, uv0.y, uR, vT);
485 draw(bl, size.y - bb, size.x - br, size.y, uL, vB, uR, uv1.y);
486 draw(0.f, bt, bl, size.y - bb, uv0.x, vT, uL, vB);
487 draw(size.x - br, bt, size.x, size.y - bb, uR, vT, uv1.x, vB);
488 draw(bl, bt, size.x - br, size.y - bb, uL, vT, uR, vB);
489}
490
491void walkFlex(UIHost *host, UIHost::Tree *tree, UINode &flex) {
493 const ImGuiStyle &style = ImGui::GetStyle();
494 const float legacyGap =
495 flex.gap >= 0.f ? flex.gap : (row ? style.ItemSpacing.x : style.ItemSpacing.y);
496 const float gap = row ? (flex.columnGap >= 0.f ? flex.columnGap : legacyGap)
497 : (flex.rowGap >= 0.f ? flex.rowGap : legacyGap);
498 const float crossGap = row ? (flex.rowGap >= 0.f ? flex.rowGap : style.ItemSpacing.y)
499 : (flex.columnGap >= 0.f ? flex.columnGap : style.ItemSpacing.x);
500
501 // Own content box: an explicit size (set by the parent's arrange) wins;
502 // otherwise use the available region (root flex inside a window/child).
503 const ImVec2 avail = ImGui::GetContentRegionAvail();
504 const bool fillsContainer =
506 const float flexW = flex.sizeX > 0.f ? flex.sizeX : avail.x;
507 const float flexH =
508 flex.sizeY > 0.f ? flex.sizeY : (fillsContainer ? avail.y : flex.measuredH);
509 const float padMain = row ? flex.paddingL + flex.paddingR : flex.paddingT + flex.paddingB;
510 const float padCross = row ? flex.paddingT + flex.paddingB : flex.paddingL + flex.paddingR;
511 const float availMain = (row ? flexW : flexH) - padMain;
512 const float availCross = (row ? flexH : flexW) - padCross;
513
514 std::vector<int> kids;
515 std::vector<int> absKids;
516 std::vector<FlexItemSpec> specs;
517 for (int c = flex.firstChild; c >= 0; c = tree->nodes[size_t(c)].nextSibling) {
518 if (c >= int(tree->nodes.size())) break;
519 UINode &child = tree->nodes[size_t(c)];
520 if (!child.visible) continue;
521 if (child.absolute) {
522 absKids.push_back(c);
523 continue;
524 }
525 kids.push_back(c);
526 FlexItemSpec s;
527 s.basisMain = row ? child.measuredW : child.measuredH;
528 s.basisCross = row ? child.measuredH : child.measuredW;
529 s.marginBefore = row ? child.marginL : child.marginT;
530 s.marginAfter = row ? child.marginR : child.marginB;
531 s.marginCrossBefore = row ? child.marginT : child.marginL;
532 s.marginCrossAfter = row ? child.marginB : child.marginR;
533 s.flexGrow = child.flexGrow;
534 s.flexShrink = child.flexShrink;
535 s.flexBasis = child.flexBasis;
536 s.alignSelf = child.alignSelf;
537 s.aspectRatio = child.aspectRatio;
538 s.isSpacer = child.type == NodeType::Spacer;
539 s.minMain = row ? child.minSizeX : child.minSizeY;
540 s.maxMain = row ? child.maxSizeX : child.maxSizeY;
541 s.minCross = row ? child.minSizeY : child.minSizeX;
542 s.maxCross = row ? child.maxSizeY : child.maxSizeX;
543 s.percentMain = row ? child.percentW : child.percentH;
544 s.percentCross = row ? child.percentH : child.percentW;
545 s.explicitMain = row ? child.sizeX : child.sizeY;
546 s.explicitCross = row ? child.sizeY : child.sizeX;
547 specs.push_back(s);
548 }
549
550 const FlexResult res =
551 flexArrange(row, gap, std::max(0.f, availMain), std::max(0.f, availCross),
553 flex.flexWrap == FlexWrap::Wrap, crossGap);
554 flex.layoutOverflowX = std::max(
555 0.f, res.contentW - std::max(0.f, row ? availMain : availCross));
556 flex.layoutOverflowY = std::max(
557 0.f, res.contentH - std::max(0.f, row ? availCross : availMain));
558 if (row) flex.layoutOverflowX = std::max(flex.layoutOverflowX, res.overflowMain);
559 else flex.layoutOverflowY = std::max(flex.layoutOverflowY, res.overflowMain);
560 const bool scrolling = flex.overflow == OverflowMode::Scroll;
561 if (scrolling) {
562 ImGui::PushID(&flex);
563 ImGui::BeginChild("layout-scroll", ImVec2(std::max(0.f, flexW), std::max(0.f, flexH)),
564 false, ImGuiWindowFlags_HorizontalScrollbar);
565 }
566 const ImVec2 cursor = ImGui::GetCursorScreenPos();
567 const float dummyW = scrolling ? std::max(flexW, res.contentW + flex.paddingL + flex.paddingR)
568 : flexW;
569 const float dummyH = scrolling ? std::max(flexH, res.contentH + flex.paddingT + flex.paddingB)
570 : flexH;
571 ImGui::Dummy(ImVec2(std::max(0.f, dummyW), std::max(0.f, dummyH)));
572 const ImVec2 flowEnd = ImGui::GetCursorScreenPos();
573 const ImVec2 origin = ImVec2(cursor.x + flex.paddingL, cursor.y + flex.paddingT);
574 const bool clipped = flex.overflow == OverflowMode::Clip;
575 if (clipped)
576 ImGui::PushClipRect(cursor, ImVec2(cursor.x + flexW, cursor.y + flexH), true);
577
578 for (size_t i = 0; i < kids.size(); ++i) {
579 UINode &child = tree->nodes[size_t(kids[i])];
580 const FlexRect &r = res.items[i];
581 ImGui::SetCursorScreenPos(ImVec2(origin.x + r.x, origin.y + r.y));
582 const float oldX = child.sizeX;
583 const float oldY = child.sizeY;
584 bool pushedWidth = false;
585 if (r.w > 0.f) child.sizeX = r.w;
586 if (r.h > 0.f) child.sizeY = r.h;
587 if (row || r.w > 0.f) {
588 ImGui::PushItemWidth(r.w > 0.f ? r.w : 1.f);
589 pushedWidth = true;
590 }
591 walkNode(host, tree, kids[i]);
592 child.sizeX = oldX;
593 child.sizeY = oldY;
594 if (pushedWidth) ImGui::PopItemWidth();
595 }
596
597 // Absolutely placed children are excluded from flow; draw on top of it.
598 for (int c : absKids) {
599 UINode &child = tree->nodes[size_t(c)];
600 float w = child.percentW > 0.f ? child.percentW * flexW : child.measuredW;
601 float h = child.percentH > 0.f ? child.percentH * flexH : child.measuredH;
602 if (child.sizeX > 0.f) w = child.sizeX;
603 if (child.sizeY > 0.f) h = child.sizeY;
604 if (child.minSizeX > 0.f) w = std::max(w, child.minSizeX);
605 if (child.minSizeY > 0.f) h = std::max(h, child.minSizeY);
606 if (child.maxSizeX > 0.f) w = std::min(w, child.maxSizeX);
607 if (child.maxSizeY > 0.f) h = std::min(h, child.maxSizeY);
608 const float x = child.anchorX * flexW + child.posX - child.anchorX * w;
609 const float y = child.anchorY * flexH + child.posY - child.anchorY * h;
610 ImGui::SetCursorScreenPos(ImVec2(origin.x + x, origin.y + y));
611 const float oldX = child.sizeX;
612 const float oldY = child.sizeY;
613 child.sizeX = w;
614 child.sizeY = h;
615 if (w > 0.f) ImGui::PushItemWidth(w);
616 walkNode(host, tree, c);
617 child.sizeX = oldX;
618 child.sizeY = oldY;
619 if (w > 0.f) ImGui::PopItemWidth();
620 }
621 if (clipped) ImGui::PopClipRect();
622 ImGui::SetCursorScreenPos(flowEnd);
623 if (scrolling) {
624 ImGui::EndChild();
625 ImGui::PopID();
626 }
627}
628
629void walkGrid(UIHost *host, UIHost::Tree *tree, UINode &grid) {
630 const ImGuiStyle &style = ImGui::GetStyle();
631 const float columnGap = grid.columnGap >= 0.f
633 : (grid.gap >= 0.f ? grid.gap : style.ItemSpacing.x);
634 const float rowGap = grid.rowGap >= 0.f
635 ? grid.rowGap
636 : (grid.gap >= 0.f ? grid.gap : style.ItemSpacing.y);
637 const ImVec2 avail = ImGui::GetContentRegionAvail();
638 const float gridW = grid.sizeX > 0.f ? grid.sizeX : avail.x;
639 const float contentW = std::max(0.f, gridW - grid.paddingL - grid.paddingR);
640 std::vector<int> kids;
641 std::vector<int> absKids;
642 std::vector<GridItemSpec> specs;
643 for (int c = grid.firstChild; c >= 0; c = tree->nodes[size_t(c)].nextSibling) {
644 if (c >= int(tree->nodes.size())) break;
645 UINode &child = tree->nodes[size_t(c)];
646 if (!child.visible) continue;
647 if (child.absolute) {
648 absKids.push_back(c);
649 continue;
650 }
651 kids.push_back(c);
652 GridItemSpec spec;
653 spec.basisW = child.measuredW;
654 spec.basisH = child.measuredH;
655 spec.marginL = child.marginL;
656 spec.marginT = child.marginT;
657 spec.marginR = child.marginR;
658 spec.marginB = child.marginB;
659 spec.aspectRatio = child.aspectRatio;
660 spec.columnSpan = child.gridColumnSpan;
661 specs.push_back(spec);
662 }
663 const GridResult result =
664 gridArrange(grid.gridColumns, columnGap, rowGap, contentW, specs);
665 const float naturalH = result.contentH + grid.paddingT + grid.paddingB;
666 const float gridH = grid.sizeY > 0.f ? grid.sizeY : naturalH;
667 grid.layoutOverflowX = result.overflowX;
668 grid.layoutOverflowY = std::max(0.f, naturalH - gridH);
669 const bool scrolling = grid.overflow == OverflowMode::Scroll;
670 if (scrolling) {
671 ImGui::PushID(&grid);
672 ImGui::BeginChild("grid-scroll", ImVec2(std::max(0.f, gridW), std::max(0.f, gridH)),
673 false, ImGuiWindowFlags_HorizontalScrollbar);
674 }
675 const ImVec2 cursor = ImGui::GetCursorScreenPos();
676 ImGui::Dummy(ImVec2(std::max(0.f, gridW),
677 std::max(0.f, scrolling ? std::max(gridH, naturalH) : gridH)));
678 const ImVec2 flowEnd = ImGui::GetCursorScreenPos();
679 const ImVec2 origin(cursor.x + grid.paddingL, cursor.y + grid.paddingT);
680 const bool clipped = grid.overflow == OverflowMode::Clip;
681 if (clipped)
682 ImGui::PushClipRect(cursor, ImVec2(cursor.x + gridW, cursor.y + gridH), true);
683 for (size_t i = 0; i < kids.size(); ++i) {
684 UINode &child = tree->nodes[size_t(kids[i])];
685 const FlexRect &rect = result.items[i];
686 ImGui::SetCursorScreenPos(ImVec2(origin.x + rect.x, origin.y + rect.y));
687 const float oldX = child.sizeX;
688 const float oldY = child.sizeY;
689 child.sizeX = rect.w;
690 child.sizeY = rect.h;
691 if (rect.w > 0.f) ImGui::PushItemWidth(rect.w);
692 walkNode(host, tree, kids[i]);
693 child.sizeX = oldX;
694 child.sizeY = oldY;
695 if (rect.w > 0.f) ImGui::PopItemWidth();
696 }
697 // Absolutely placed children are excluded from grid flow; draw on top (Flex parity).
698 for (int c : absKids) {
699 UINode &child = tree->nodes[size_t(c)];
700 float w = child.percentW > 0.f ? child.percentW * gridW : child.measuredW;
701 float h = child.percentH > 0.f ? child.percentH * gridH : child.measuredH;
702 if (child.sizeX > 0.f) w = child.sizeX;
703 if (child.sizeY > 0.f) h = child.sizeY;
704 if (child.minSizeX > 0.f) w = std::max(w, child.minSizeX);
705 if (child.minSizeY > 0.f) h = std::max(h, child.minSizeY);
706 if (child.maxSizeX > 0.f) w = std::min(w, child.maxSizeX);
707 if (child.maxSizeY > 0.f) h = std::min(h, child.maxSizeY);
708 const float x = child.anchorX * gridW + child.posX - child.anchorX * w;
709 const float y = child.anchorY * gridH + child.posY - child.anchorY * h;
710 ImGui::SetCursorScreenPos(ImVec2(origin.x + x, origin.y + y));
711 const float oldX = child.sizeX;
712 const float oldY = child.sizeY;
713 child.sizeX = w;
714 child.sizeY = h;
715 if (w > 0.f) ImGui::PushItemWidth(w);
716 walkNode(host, tree, c);
717 child.sizeX = oldX;
718 child.sizeY = oldY;
719 if (w > 0.f) ImGui::PopItemWidth();
720 }
721 if (clipped) ImGui::PopClipRect();
722 ImGui::SetCursorScreenPos(flowEnd);
723 if (scrolling) {
724 ImGui::EndChild();
725 ImGui::PopID();
726 }
727}
728
729void walkNode(UIHost *host, UIHost::Tree *tree, int index) {
730 if (index < 0 || index >= int(tree->nodes.size())) return;
731 UINode &n = tree->nodes[size_t(index)];
732 if (!n.visible) return;
733
734 const bool scopedTheme = n.themePreset != ThemePreset::Inherit;
735 ImGuiStyle previousStyle;
736 float previousFontScale = 1.f;
737 ImGuiConfigFlags previousConfigFlags = ImGuiConfigFlags_None;
738 if (scopedTheme) {
739 previousStyle = ImGui::GetStyle();
740 previousFontScale = ImGui::GetIO().FontGlobalScale;
741 previousConfigFlags = ImGui::GetIO().ConfigFlags;
742 applyThemeToImGui(n.themePreset == ThemePreset::Dark ? Theme::dark() : Theme::light());
743 }
744
745 int styleColorCount = 0;
746 int styleVarCount = 0;
747 if (n.hasResolvedStyle) {
748 const StyleClass &style = n.resolvedStyle;
749 const auto pushColor = [&](ImGuiCol slot, const float color[4]) {
750 ImGui::PushStyleColor(slot, ImVec4(color[0], color[1], color[2], color[3]));
751 ++styleColorCount;
752 };
753 if (style.hasTextColor) pushColor(ImGuiCol_Text, style.textColor);
754 if (style.hasBackgroundColor) {
755 pushColor(ImGuiCol_Button, style.backgroundColor);
756 pushColor(ImGuiCol_FrameBg, style.backgroundColor);
757 pushColor(ImGuiCol_Header, style.backgroundColor);
758 pushColor(ImGuiCol_ChildBg, style.backgroundColor);
759 }
760 if (style.hasBorderColor) pushColor(ImGuiCol_Border, style.borderColor);
761 if (style.hasAccentColor) {
762 pushColor(ImGuiCol_ButtonHovered, style.accentColor);
763 pushColor(ImGuiCol_ButtonActive, style.accentColor);
764 pushColor(ImGuiCol_HeaderHovered, style.accentColor);
765 pushColor(ImGuiCol_HeaderActive, style.accentColor);
766 pushColor(ImGuiCol_CheckMark, style.accentColor);
767 pushColor(ImGuiCol_SliderGrab, style.accentColor);
768 pushColor(ImGuiCol_SliderGrabActive, style.accentColor);
769 }
770 const float scale = themeUiScale();
771 if (style.hasPadding) {
772 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
773 ImVec2(style.paddingX * scale, style.paddingY * scale));
774 ++styleVarCount;
775 }
776 if (style.hasRounding) {
777 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, style.rounding * scale);
778 ++styleVarCount;
779 }
780 if (style.hasAlpha) {
781 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * style.alpha);
782 ++styleVarCount;
783 }
784 }
785 if (n.opacity < 0.9999f) {
786 ImGui::PushStyleVar(ImGuiStyleVar_Alpha,
787 ImGui::GetStyle().Alpha * std::clamp(n.opacity, 0.f, 1.f));
788 ++styleVarCount;
789 }
790
791 const bool interactive = isInteractive(n.type);
792 const bool disableItem = interactive && !n.enabled;
793 const bool disableNavigation = interactive && n.focusMode != FocusMode::All;
794 if (disableItem) {
795 ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
796 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.6f);
797 }
798 if (disableNavigation) ImGui::PushItemFlag(ImGuiItemFlags_NoNav, true);
799 if (interactive && n.focusRequested && n.enabled && n.focusMode != FocusMode::None)
800 ImGui::SetKeyboardFocusHere();
801
802 switch (n.type) {
803 case NodeType::Window: {
804 const std::string visibleTitle = n.text.empty() ? "Window" : n.text;
805 std::string title = visibleTitle;
806 // Keep the host name in ImGui's stable ID without exposing internal
807 // paths such as "inventory/Inspector" in the visible title bar.
808 if (host && !host->meta()->name.empty())
809 title += "###" + host->meta()->name + "/" + visibleTitle;
810 const bool modal = host && host->meta()->modal;
811 ImGuiWindowFlags flags = 0;
812 if (hasDirectChildType(*tree, n, NodeType::MenuBar)) flags |= ImGuiWindowFlags_MenuBar;
813 const bool desktopChrome = hasDirectChildType(*tree, n, NodeType::SplitPane) &&
814 (hasDirectChildType(*tree, n, NodeType::Toolbar) ||
815 hasDirectChildType(*tree, n, NodeType::StatusBar));
816 if (desktopChrome)
817 flags |= ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
818 float winW = 0.f;
819 float winH = 0.f;
820 if (host && host->meta()->percentW > 0.f)
821 winW = host->meta()->percentW * ImGui::GetIO().DisplaySize.x;
822 else if (host && host->meta()->hasSize)
823 winW = host->meta()->sizeX;
824 if (host && host->meta()->percentH > 0.f)
825 winH = host->meta()->percentH * ImGui::GetIO().DisplaySize.y;
826 else if (host && host->meta()->hasSize)
827 winH = host->meta()->sizeY;
828 const auto worldAnchor = host ? host->worldAnchor() : nullptr;
829 if (worldAnchor->enabled) {
830 ImGui::SetNextWindowPos(ImVec2(worldAnchor->screenX, worldAnchor->screenY),
831 ImGuiCond_Always,
832 ImVec2(host->meta()->pivotX, host->meta()->pivotY));
833 flags |= ImGuiWindowFlags_NoMove;
834 if (!host->meta()->hasSize && winW <= 0.f && winH <= 0.f)
835 flags |= ImGuiWindowFlags_AlwaysAutoResize;
836 } else if (host && host->meta()->hasPos) {
837 const ImVec2 display = ImGui::GetIO().DisplaySize;
838 // SetNextWindowPos applies the pivot itself. Passing an already
839 // pivot-adjusted top-left position shifts the window by its pivot
840 // a second time (a centered window moves half its width left).
841 const float x = host->meta()->anchorX * display.x + host->meta()->posX;
842 const float y = host->meta()->anchorY * display.y + host->meta()->posY;
843 const bool drivePos = host->meta()->lockPos || host->meta()->animDrivePos;
844 ImGui::SetNextWindowPos(ImVec2(x, y), drivePos ? ImGuiCond_Always : ImGuiCond_FirstUseEver,
845 ImVec2(host->meta()->pivotX, host->meta()->pivotY));
846 if (host->meta()->lockPos) flags |= ImGuiWindowFlags_NoMove;
847 if (winW <= 0.f && winH <= 0.f) flags |= ImGuiWindowFlags_AlwaysAutoResize;
848 }
849 if (winW > 0.f || winH > 0.f) {
850 const bool driveSize = host && (host->meta()->lockSize || host->meta()->animDriveSize);
851 ImGui::SetNextWindowSize(ImVec2(winW > 0.f ? winW : -1.f, winH > 0.f ? winH : -1.f),
852 driveSize ? ImGuiCond_Always : ImGuiCond_FirstUseEver);
853 } else if (n.measuredW > 0.f || n.measuredH > 0.f) {
854 // Real measured size → stable auto-resize instead of ImGui's guess.
855 ImGui::SetNextWindowContentSize(ImVec2(n.measuredW, n.measuredH));
856 }
857 if (host && host->meta()->overlay) {
858 flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
859 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse;
860 if (!host->meta()->hasSize && host->meta()->percentW <= 0.f &&
861 host->meta()->percentH <= 0.f)
862 flags |= ImGuiWindowFlags_AlwaysAutoResize;
863 ImGui::SetNextWindowBgAlpha(host->meta()->overlayBgAlpha);
864 }
865 // A transparent overlay delegates its surface to its content (for example
866 // a nine-patch skin). Do not leave desktop chrome around that surface.
867 const bool transparentOverlay = host && host->meta()->overlay && host->meta()->overlayBgAlpha == 0.f;
868 const bool flushOverlay = host && host->meta()->overlay && (host->meta()->overlayFlush || transparentOverlay);
869 if (flushOverlay) ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
870 if (transparentOverlay) ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
871 if (modal) {
872 ImGui::OpenPopup(title.c_str());
873 if (ImGui::BeginPopupModal(title.c_str(), nullptr,
874 ImGuiWindowFlags_AlwaysAutoResize | flags)) {
875 if (worldAnchor->enabled)
876 ImGui::SetWindowFontScale(worldAnchor->scale);
877 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
878 ImGui::EndPopup();
879 }
880 } else {
881 ImGui::Begin(title.c_str(), nullptr, flags);
882 if (worldAnchor->enabled)
883 ImGui::SetWindowFontScale(worldAnchor->scale);
884 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
885 ImGui::End();
886 }
887 if (transparentOverlay) ImGui::PopStyleVar();
888 if (flushOverlay) ImGui::PopStyleVar();
889 break;
890 }
891 case NodeType::Text:
892 if (n.wrapWidth > 0.f) {
893 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + n.wrapWidth);
894 ImGui::TextUnformatted(n.text.c_str());
895 ImGui::PopTextWrapPos();
896 } else {
897 ImGui::TextUnformatted(n.text.c_str());
898 }
899 break;
900 case NodeType::Button: {
901 ImWchar iconCodepoint = 0;
902 const bool iconOnly = privateUseGlyph(n.text, &iconCodepoint);
903 const std::string label =
904 iconOnly ? "##iconButton" + (n.id.empty() ? std::string("button") : n.id)
905 : nodeLabel(n, "Button");
906 const bool sized = n.sizeX > 0.f || n.sizeY > 0.f;
907 const float defaultSide = ImGui::GetFrameHeight();
908 const ImVec2 buttonSize(iconOnly ? (n.sizeX > 0.f ? n.sizeX : defaultSide)
909 : (n.sizeX > 0.f ? n.sizeX : 0.f),
910 iconOnly ? (n.sizeY > 0.f ? n.sizeY : defaultSide)
911 : (n.sizeY > 0.f ? n.sizeY : 0.f));
912 if (n.checked) {
913 ImGui::PushStyleColor(ImGuiCol_Button,
914 ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
915 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
916 ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
917 }
918 const bool clicked = controls::button(label.c_str(),
919 (sized || iconOnly) ? buttonSize.x : 0.f,
920 (sized || iconOnly) ? buttonSize.y : 0.f) ==
921 controls::ControlEdit::Changed;
922 if (n.checked) ImGui::PopStyleColor(2);
923 if (iconOnly) {
924 const ImVec2 rectMin = ImGui::GetItemRectMin();
925 const ImVec2 rectMax = ImGui::GetItemRectMax();
926 ImFont *font = ImGui::GetFont();
927 const ImFontGlyph *glyph = font ? font->FindGlyph(iconCodepoint) : nullptr;
928 if (glyph) {
929 const float fontSize = ImGui::GetFontSize();
930 const float fontScale = fontSize / font->FontSize;
931 const float glyphW = (glyph->X1 - glyph->X0) * fontScale;
932 const float glyphH = (glyph->Y1 - glyph->Y0) * fontScale;
933 const ImVec2 glyphOrigin(
934 (rectMin.x + rectMax.x - glyphW) * 0.5f - glyph->X0 * fontScale,
935 (rectMin.y + rectMax.y - glyphH) * 0.5f - glyph->Y0 * fontScale);
936 ImGui::GetWindowDrawList()->AddText(font, fontSize, glyphOrigin,
937 ImGui::GetColorU32(ImGuiCol_Text),
938 n.text.c_str());
939 }
940 }
941 if (clicked) pushPending(host, n, "click", n.handlerClick);
942 break;
943 }
945 ImGui::SameLine();
946 break;
947 case NodeType::Group:
948 ImGui::BeginGroup();
949 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
950 ImGui::EndGroup();
951 break;
953 ImGui::Separator();
954 break;
955 case NodeType::Checkbox: {
956 bool checked = n.checked;
957 if (controls::checkbox(n.text.empty() ? "Check" : n.text.c_str(), checked) ==
958 controls::ControlEdit::Changed) {
959 n.checked = checked;
960 pushPending(host, n, "toggle", n.handlerToggle, checked);
961 }
962 break;
963 }
964 case NodeType::Slider: {
965 float v = n.value;
966 const char *label = n.text.empty() ? "Slider" : n.text.c_str();
967 if (controls::slider(label, v, n.minValue, n.maxValue) ==
969 n.value = v;
970 pushPending(host, n, "value", n.handlerValue, false, v);
971 }
972 break;
973 }
975 const float swatch = ImGui::GetFrameHeight();
976 const float gap = 4.f;
977 const int cols = 8;
978 float col[4] = {n.tintR, n.tintG, n.tintB, n.tintA};
979 const std::string label = n.text.empty() ? "Color" : n.text;
980 const std::string editId = label + "###palette-edit-" + n.id;
981 ImGui::BeginGroup();
982 ImGui::PushID(n.id.empty() ? "color-palette" : n.id.c_str());
983 ImGui::SetNextItemWidth(std::max(80.f, n.measuredW > 0.f ? n.measuredW : 220.f));
984 const ImGuiColorEditFlags flags =
985 ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreviewHalf |
986 ImGuiColorEditFlags_PickerHueBar | ImGuiColorEditFlags_DisplayRGB;
987 if (ImGui::ColorEdit4(editId.c_str(), col, flags)) commitPaletteColor(host, n, col);
988 const std::vector<PaletteSwatch> swatches = paletteSwatches(n);
989 for (size_t i = 0; i < swatches.size(); ++i) {
990 if (i % static_cast<size_t>(cols) != 0) ImGui::SameLine(0.f, gap);
991 const PaletteSwatch &s = swatches[i];
992 const ImVec4 color(s.r, s.g, s.b, s.a);
993 const bool selected = std::fabs(n.tintR - s.r) < 0.004f &&
994 std::fabs(n.tintG - s.g) < 0.004f &&
995 std::fabs(n.tintB - s.b) < 0.004f &&
996 std::fabs(n.tintA - s.a) < 0.004f;
997 ImGui::PushID(static_cast<int>(i));
998 if (ImGui::ColorButton("##swatch", color, ImGuiColorEditFlags_NoTooltip |
999 ImGuiColorEditFlags_AlphaPreview,
1000 ImVec2(swatch, swatch))) {
1001 float picked[4] = {s.r, s.g, s.b, s.a};
1002 commitPaletteColor(host, n, picked);
1003 }
1004 if (selected) {
1005 const ImVec2 p0 = ImGui::GetItemRectMin();
1006 const ImVec2 p1 = ImGui::GetItemRectMax();
1007 ImGui::GetWindowDrawList()->AddRect(
1008 p0, p1, ImGui::GetColorU32(ImGuiCol_Text), 0.f, 0, 1.5f);
1009 }
1010 ImGui::PopID();
1011 }
1012 ImGui::PopID();
1013 ImGui::EndGroup();
1014 break;
1015 }
1016 case NodeType::Progress: {
1017 const char *overlay = n.text.empty() ? nullptr : n.text.c_str();
1018 ImGui::ProgressBar(n.value, ImVec2(n.sizeX > 0.f ? n.sizeX : -1.f, n.sizeY > 0.f ? n.sizeY : 0.f), overlay);
1019 break;
1020 }
1021 case NodeType::Image: {
1022 const float w = n.sizeX > 0.f ? n.sizeX : 32.f;
1023 const float h = n.sizeY > 0.f ? n.sizeY : 32.f;
1024 const ImVec2 pos = ImGui::GetCursorScreenPos();
1025 const ImVec2 size(w, h);
1026 const ImU32 tint =
1027 ImGui::ColorConvertFloat4ToU32(ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
1028 if (n.textureId != 0 && g_backend) {
1029 int tw = 0, th = 0;
1030 g_backend->textureSize(n.textureId, &tw, &th);
1031 if (g_backend->usesQueuedTextureDraws()) {
1032 queueNinePatch(n.textureId, pos, size, ImVec2(n.uv0x, n.uv0y),
1033 ImVec2(n.uv1x, n.uv1y), n.borderL, n.borderT, n.borderR,
1034 n.borderB, tw, th, n.tintR, n.tintG, n.tintB, n.tintA);
1035 } else {
1036 void *handle = g_backend->textureHandle(n.textureId);
1037 if (handle) {
1038 drawNinePatch(handle, pos, size, ImVec2(n.uv0x, n.uv0y),
1039 ImVec2(n.uv1x, n.uv1y), n.borderL, n.borderT,
1040 n.borderR, n.borderB, tw, th, tint);
1041 } else {
1042 ImGui::GetWindowDrawList()->AddRectFilled(
1043 pos, ImVec2(pos.x + w, pos.y + h), tint, n.cornerRadius);
1044 }
1045 }
1046 } else {
1047 ImGui::GetWindowDrawList()->AddRectFilled(pos, ImVec2(pos.x + w, pos.y + h), tint,
1048 n.cornerRadius);
1049 }
1050 ImGui::Dummy(size);
1051 break;
1052 }
1053 case NodeType::ImageButton: {
1054 const float w = n.sizeX > 0.f ? n.sizeX : 32.f;
1055 const float h = n.sizeY > 0.f ? n.sizeY : 32.f;
1056 const ImVec2 size(w, h);
1057 const std::string sid = n.id.empty() ? "imgbtn" : n.id;
1058 ImGui::PushID(sid.c_str());
1059 bool clicked = false;
1060 bool itemCreated = false;
1061 if (n.textureId != 0 && g_backend) {
1062 if (g_backend->usesQueuedTextureDraws()) {
1063 clicked = ImGui::InvisibleButton(sid.c_str(), size);
1064 itemCreated = true;
1065 const ImVec2 pmin = ImGui::GetItemRectMin();
1066 const float hover = ImGui::IsItemHovered() ? 1.12f : 1.f;
1067 g_backend->queueTextureDraw(n.textureId, pmin.x, pmin.y, size.x, size.y,
1068 n.uv0x, n.uv0y, n.uv1x, n.uv1y,
1069 std::min(1.f, n.tintR * hover),
1070 std::min(1.f, n.tintG * hover),
1071 std::min(1.f, n.tintB * hover), n.tintA, false);
1072 } else {
1073 void *handle = g_backend->textureHandle(n.textureId);
1074 if (handle) {
1075 clicked = ImGui::ImageButton(
1076 static_cast<ImTextureID>(handle), size, ImVec2(n.uv0x, n.uv0y),
1077 ImVec2(n.uv1x, n.uv1y), -1, ImVec4(0.f, 0.f, 0.f, 0.f),
1078 ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
1079 itemCreated = true;
1080 }
1081 }
1082 }
1083 if (!itemCreated) {
1084 clicked = ImGui::InvisibleButton(sid.c_str(), size);
1085 const ImVec2 pmin = ImGui::GetItemRectMin();
1086 const ImVec2 pmax = ImGui::GetItemRectMax();
1087 ImU32 col =
1088 ImGui::ColorConvertFloat4ToU32(ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
1089 if (ImGui::IsItemHovered()) col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
1090 ImGui::GetWindowDrawList()->AddRectFilled(pmin, pmax, col, n.cornerRadius);
1091 }
1092 ImGui::PopID();
1093 if (clicked) pushPending(host, n, "click", n.handlerClick);
1094 break;
1095 }
1096 case NodeType::Combo: {
1097 // Options are newline-separated in valueText; selected index in value.
1098 std::vector<std::string> storage;
1099 size_t start = 0;
1100 while (start <= n.valueText.size()) {
1101 const size_t end = n.valueText.find('\n', start);
1102 storage.push_back(n.valueText.substr(
1103 start, end == std::string::npos ? std::string::npos : end - start));
1104 if (end == std::string::npos) break;
1105 start = end + 1;
1106 }
1107 std::vector<const char *> items;
1108 items.reserve(storage.size());
1109 for (const auto &s : storage) items.push_back(s.c_str());
1110 int idx = int(n.value);
1111 const char *label = n.text.empty() ? "Combo" : n.text.c_str();
1113 n.value = float(idx);
1114 pushPending(host, n, "value", n.handlerValue, false, float(idx));
1115 }
1116 break;
1117 }
1118 case NodeType::InputText: {
1119 const char *label = n.text.empty() ? "Input" : n.text.c_str();
1120 std::string edited = n.valueText;
1122 n.valueText = std::move(edited);
1123 pushPending(host, n, "text", n.handlerText, false, 0.f, n.valueText);
1124 }
1125 break;
1126 }
1127 case NodeType::SearchField: {
1128 std::string edited = n.valueText;
1129 const std::string label = "##search" + (n.id.empty() ? std::string() : "###" + n.id);
1130 const ImGuiStyle &style = ImGui::GetStyle();
1131 const std::string hint = n.text.empty() ? "Search" : n.text;
1132 const char *glyph = iconGlyph(Icon::Search);
1133 const ImVec2 iconSize = ImGui::CalcTextSize(glyph);
1134 const float gap = globalTheme().layout.searchIconGap * themeUiScale();
1135 const float width = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
1136 n.measuredW = std::max(1.f, width);
1137 ImGui::PushItemWidth(std::max(1.f, width));
1138 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
1139 ImVec2(style.FramePadding.x + iconSize.x + gap,
1140 style.FramePadding.y));
1141 if (controls::inputTextWithHint(label.c_str(), hint.c_str(), edited) ==
1143 n.valueText = std::move(edited);
1144 pushPending(host, n, "text", n.handlerText, false, 0.f, n.valueText);
1145 }
1146 ImGui::PopStyleVar();
1147 ImGui::PopItemWidth();
1148 const ImVec2 rectMin = ImGui::GetItemRectMin();
1149 const ImVec2 rectMax = ImGui::GetItemRectMax();
1150 ImGui::GetWindowDrawList()->AddText(
1151 ImVec2(rectMin.x + style.FramePadding.x,
1152 rectMin.y + (rectMax.y - rectMin.y - iconSize.y) * 0.5f),
1153 ImGui::GetColorU32(ImGuiCol_TextDisabled), glyph);
1154 n.measuredH = rectMax.y - rectMin.y;
1155 break;
1156 }
1157 case NodeType::Switch: {
1158 const float height = ImGui::GetFrameHeight();
1159 const float trackH = std::max(16.f, height * 0.68f);
1160 const float trackW = height * 1.7f;
1161 const float radius = trackH * 0.5f;
1162 const std::string id = "##switch" + (n.id.empty() ? std::string() : "###" + n.id);
1163 ImGui::BeginGroup();
1164 const ImVec2 pos = ImGui::GetCursorScreenPos();
1165 if (ImGui::InvisibleButton(id.c_str(), ImVec2(trackW, height))) {
1166 n.checked = !n.checked;
1167 pushPending(host, n, "toggle", n.handlerToggle, n.checked);
1168 }
1169 const bool hovered = ImGui::IsItemHovered();
1170 const ImU32 bg = ImGui::GetColorU32(
1171 n.checked ? ImGuiCol_CheckMark
1172 : (hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg));
1173 const float y = pos.y + (height - trackH) * 0.5f;
1174 ImDrawList *draw = ImGui::GetWindowDrawList();
1175 draw->AddRectFilled(ImVec2(pos.x, y), ImVec2(pos.x + trackW, y + trackH), bg, radius);
1176 const float knobR = radius - 2.f;
1177 const float knobX = n.checked ? pos.x + trackW - radius : pos.x + radius;
1178 draw->AddCircleFilled(ImVec2(knobX, y + radius), knobR,
1179 ImGui::GetColorU32(ImGuiCol_Text));
1180 if (!n.text.empty()) {
1181 ImGui::SameLine(0.f, ImGui::GetStyle().ItemInnerSpacing.x);
1182 ImGui::AlignTextToFramePadding();
1183 ImGui::TextUnformatted(n.text.c_str());
1184 }
1185 ImGui::EndGroup();
1186 break;
1187 }
1188 case NodeType::Badge: {
1189 const ImVec2 textSize = ImGui::CalcTextSize(n.text.c_str());
1190 const ImVec2 padding(ImGui::GetStyle().FramePadding.x, 3.f);
1191 const ImVec2 pos = ImGui::GetCursorScreenPos();
1192 const ImVec2 size(textSize.x + padding.x * 2.f, textSize.y + padding.y * 2.f);
1193 ImDrawList *draw = ImGui::GetWindowDrawList();
1194 const ImU32 bg = ImGui::ColorConvertFloat4ToU32(
1195 ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
1196 const ImU32 border = ImGui::ColorConvertFloat4ToU32(
1197 ImVec4(n.tintR, n.tintG, n.tintB, std::min(1.f, n.tintA + 0.35f)));
1198 draw->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), bg, size.y * 0.5f);
1199 draw->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), border, size.y * 0.5f);
1200 draw->AddText(ImVec2(pos.x + padding.x, pos.y + padding.y),
1201 ImGui::GetColorU32(ImGuiCol_Text), n.text.c_str());
1202 ImGui::Dummy(size);
1203 break;
1204 }
1206 ImGuiTreeNodeFlags flags = n.open ? ImGuiTreeNodeFlags_DefaultOpen : 0;
1207 const char *label = n.text.empty() ? "Section" : n.text.c_str();
1208 if (ImGui::CollapsingHeader(label, flags)) {
1209 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
1210 }
1211 break;
1212 }
1213 case NodeType::Child: {
1214 ImVec2 size(n.sizeX, n.sizeY);
1215 const char *cid = n.id.empty() ? "child" : n.id.c_str();
1216 if (ImGui::BeginChild(cid, size, true)) {
1217 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
1218 }
1219 ImGui::EndChild();
1220 break;
1221 }
1222 case NodeType::Card: {
1223 const ThemeLayout &layout = globalTheme().layout;
1224 const ImVec2 padding(layout.cardPaddingX * themeUiScale(),
1225 layout.cardPaddingY * themeUiScale());
1226 const float height = n.sizeY > 0.f
1227 ? n.sizeY
1228 : std::max(ImGui::GetFrameHeight(),
1229 n.measuredH + padding.y * 2.f);
1230 const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f, height);
1231 const std::string id = n.id.empty() ? "card" : n.id;
1232 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
1233 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg));
1234 if (ImGui::BeginChild(id.c_str(), size, true, ImGuiWindowFlags_AlwaysUseWindowPadding)) {
1235 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
1236 }
1237 ImGui::EndChild();
1238 ImGui::PopStyleColor();
1239 ImGui::PopStyleVar();
1240 break;
1241 }
1243 const float width = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
1244 const float height = n.sizeY > 0.f
1245 ? n.sizeY
1246 : std::max(n.minSizeY,
1247 n.measuredH > 0.f ? n.measuredH
1248 : ImGui::GetFrameHeight());
1249 const ImVec2 pos = ImGui::GetCursorScreenPos();
1250 const ImVec2 size(std::max(n.minSizeX, width), height);
1251 if (n.textureId != 0 && g_backend) {
1252 int tw = 0, th = 0;
1253 g_backend->textureSize(n.textureId, &tw, &th);
1254 if (g_backend->usesQueuedTextureDraws()) {
1255 queueNinePatch(n.textureId, pos, size, ImVec2(n.uv0x, n.uv0y),
1256 ImVec2(n.uv1x, n.uv1y), n.borderL, n.borderT, n.borderR,
1257 n.borderB, tw, th, n.tintR, n.tintG, n.tintB, n.tintA);
1258 } else if (void *handle = g_backend->textureHandle(n.textureId)) {
1259 const ImU32 tint = ImGui::ColorConvertFloat4ToU32(
1260 ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
1261 drawNinePatch(handle, pos, size, ImVec2(n.uv0x, n.uv0y),
1262 ImVec2(n.uv1x, n.uv1y), n.borderL, n.borderT, n.borderR,
1263 n.borderB, tw, th, tint);
1264 }
1265 }
1266 const std::string id = n.id.empty() ? "ninePatchPanel" : n.id;
1267 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
1268 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
1269 if (ImGui::BeginChild(id.c_str(), size, false,
1270 ImGuiWindowFlags_AlwaysUseWindowPadding)) {
1271 ImGui::SetCursorPos(ImVec2(n.paddingL, n.paddingT));
1272 const ImVec2 contentSize(std::max(1.f, size.x - n.paddingL - n.paddingR),
1273 std::max(1.f, size.y - n.paddingT - n.paddingB));
1274 const std::string contentId = id + "##content";
1275 if (ImGui::BeginChild(contentId.c_str(), contentSize, false)) {
1276 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
1277 }
1278 ImGui::EndChild();
1279 }
1280 ImGui::EndChild();
1281 ImGui::PopStyleColor();
1282 ImGui::PopStyleVar();
1283 break;
1284 }
1286 const float sectionGap = globalTheme().layout.sectionSpacingY * themeUiScale();
1287 const ImVec2 pos = ImGui::GetCursorScreenPos();
1288 const float height = ImGui::GetFrameHeight();
1289 const float totalHeight = sectionGap + height;
1290 const float width = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
1291 const float headerY = pos.y + sectionGap;
1292 ImDrawList *draw = ImGui::GetWindowDrawList();
1293 draw->AddRectFilled(ImVec2(pos.x, headerY + 4.f),
1294 ImVec2(pos.x + 3.f, headerY + height - 4.f),
1295 ImGui::GetColorU32(ImGuiCol_CheckMark), 1.f);
1296 draw->AddText(ImVec2(pos.x + 10.f,
1297 headerY + (height - ImGui::GetFontSize()) * 0.5f),
1298 ImGui::GetColorU32(ImGuiCol_Text), n.text.c_str());
1299 const float separatorY = pos.y + totalHeight - 1.f;
1300 draw->AddLine(ImVec2(pos.x, separatorY), ImVec2(pos.x + width, separatorY),
1301 ImGui::GetColorU32(ImGuiCol_Separator));
1302 ImGui::Dummy(ImVec2(width, totalHeight));
1303 break;
1304 }
1305 case NodeType::MenuBar:
1306 if (ImGui::BeginMenuBar()) {
1307 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
1308 ImGui::EndMenuBar();
1309 }
1310 break;
1311 case NodeType::Menu: {
1312 const std::string label = nodeLabel(n, "Menu");
1313 if (ImGui::BeginMenu(label.c_str())) {
1314 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
1315 ImGui::EndMenu();
1316 }
1317 break;
1318 }
1319 case NodeType::MenuItem: {
1320 const std::string label = nodeLabel(n, "Command");
1321 const char *shortcut = n.valueText.empty() ? nullptr : n.valueText.c_str();
1322 if (ImGui::MenuItem(label.c_str(), shortcut, n.checked, true))
1323 pushPending(host, n, "click", n.handlerClick);
1324 break;
1325 }
1326 case NodeType::Toolbar:
1327 case NodeType::StatusBar: {
1328 const bool status = n.type == NodeType::StatusBar;
1329 const ThemeLayout &layout = globalTheme().layout;
1330 const float scale = themeUiScale();
1331 const float defaultHeight =
1332 (status ? layout.statusBarHeight : layout.toolbarHeight) * scale;
1333 const float height = n.sizeY > 0.f ? n.sizeY : defaultHeight;
1334 const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f, height);
1335 const std::string id = n.id.empty() ? (status ? "statusbar" : "toolbar") : n.id;
1336 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
1337 ImVec2(layout.barPaddingX * scale, layout.barPaddingY * scale));
1338 ImGui::PushStyleColor(ImGuiCol_ChildBg,
1339 ImGui::GetStyleColorVec4(status ? ImGuiCol_WindowBg
1340 : ImGuiCol_MenuBarBg));
1341 if (ImGui::BeginChild(id.c_str(), size, false,
1342 ImGuiWindowFlags_AlwaysUseWindowPadding |
1343 ImGuiWindowFlags_NoScrollbar)) {
1344 walkFlex(host, tree, n);
1345 }
1346 ImGui::EndChild();
1347 ImGui::PopStyleColor();
1348 ImGui::PopStyleVar();
1349 break;
1350 }
1351 case NodeType::Toolbox: {
1352 const ThemeLayout &layout = globalTheme().layout;
1353 const float scale = themeUiScale();
1354 const float cell = n.itemHeight > 0.f ? n.itemHeight : layout.toolboxCellSize * scale;
1355 const ImVec2 padding(layout.panelPaddingX * scale, layout.panelPaddingY * scale);
1356 const float outerWidth = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
1357 const float contentWidth = std::max(1.f, outerWidth - padding.x * 2.f);
1358 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1359 const int capacity = std::max(1, int((contentWidth + spacing) / (cell + spacing)));
1360 const int requested = int(n.value) > 0 ? int(n.value) : capacity;
1361 const int columns = std::max(1, std::min(requested, capacity));
1362 int count = 0;
1363 for (int childIndex = n.firstChild; childIndex >= 0;
1364 childIndex = tree->nodes[size_t(childIndex)].nextSibling) {
1365 if (tree->nodes[size_t(childIndex)].visible) ++count;
1366 }
1367 const int rows = (count + columns - 1) / columns;
1368 const float autoHeight = std::max(
1369 cell, float(rows) * cell + float(std::max(0, rows - 1)) *
1370 ImGui::GetStyle().ItemSpacing.y + padding.y * 2.f);
1371 const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f, n.sizeY > 0.f ? n.sizeY : autoHeight);
1372 const std::string id = n.id.empty() ? "toolbox" : n.id;
1373 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
1374 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg));
1375 if (ImGui::BeginChild(id.c_str(), size, false, ImGuiWindowFlags_AlwaysUseWindowPadding)) {
1376 int visibleIndex = 0;
1377 for (int childIndex = n.firstChild; childIndex >= 0;
1378 childIndex = tree->nodes[size_t(childIndex)].nextSibling) {
1379 UINode &child = tree->nodes[size_t(childIndex)];
1380 if (!child.visible) continue;
1381 const float oldX = child.sizeX;
1382 const float oldY = child.sizeY;
1383 child.sizeX = cell;
1384 child.sizeY = cell;
1385 walkNode(host, tree, childIndex);
1386 child.sizeX = oldX;
1387 child.sizeY = oldY;
1388 ++visibleIndex;
1389 if (visibleIndex % columns != 0) ImGui::SameLine();
1390 }
1391 }
1392 ImGui::EndChild();
1393 ImGui::PopStyleColor();
1394 ImGui::PopStyleVar();
1395 break;
1396 }
1397 case NodeType::Sidebar: {
1398 const ThemeLayout &layout = globalTheme().layout;
1399 const float scale = themeUiScale();
1400 const ImVec2 size(n.sizeX > 0.f ? n.sizeX : layout.sidebarWidth * scale,
1401 n.sizeY > 0.f ? n.sizeY : 0.f);
1402 const std::string id = n.id.empty() ? "sidebar" : n.id;
1403 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
1404 ImVec2(layout.panelPaddingX * scale, layout.panelPaddingY * scale));
1405 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg));
1406 if (ImGui::BeginChild(id.c_str(), size, true, ImGuiWindowFlags_AlwaysUseWindowPadding)) {
1407 if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
1408 }
1409 ImGui::EndChild();
1410 ImGui::PopStyleColor();
1411 ImGui::PopStyleVar();
1412 break;
1413 }
1414 case NodeType::SplitPane: {
1415 const bool row = n.flexDirection == FlexDirection::Row;
1416 const ImVec2 avail = ImGui::GetContentRegionAvail();
1417 const ImVec2 size(n.sizeX > 0.f ? n.sizeX : avail.x, n.sizeY > 0.f ? n.sizeY : avail.y);
1418 n.measuredW = size.x;
1419 n.measuredH = size.y;
1420 const ThemeLayout &layout = globalTheme().layout;
1421 const float splitter = layout.splitterSize * themeUiScale();
1422 const float mainSize = std::max(1.f, (row ? size.x : size.y) - splitter);
1423 const float minPane = std::min(layout.minPaneSize * themeUiScale(), mainSize * 0.5f);
1424 const float ratioLo = std::max(n.minValue, minPane / mainSize);
1425 const float ratioHi = std::min(n.maxValue, 1.f - minPane / mainSize);
1426 const std::string id = n.id.empty() ? "splitpane" : n.id;
1427 ImGui::PushID(id.c_str());
1428 const ImGuiID ratioStateId = ImGui::GetID("ratio");
1429 const float storedRatio = ImGui::GetStateStorage()->GetFloat(ratioStateId, n.value);
1430 const float ratio = std::max(ratioLo, std::min(ratioHi, storedRatio));
1431 ImGui::GetStateStorage()->SetFloat(ratioStateId, ratio);
1432 n.value = ratio;
1433 const float firstMain = std::floor(mainSize * ratio);
1434 const float secondMain = std::max(1.f, mainSize - firstMain);
1435 const int first = n.firstChild;
1436 const int second = first >= 0 ? tree->nodes[size_t(first)].nextSibling : -1;
1437 ImGui::BeginGroup();
1438 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
1439 if (first >= 0) {
1440 const ImVec2 paneSize(row ? firstMain : size.x, row ? size.y : firstMain);
1441 if (ImGui::BeginChild("first", paneSize, false,
1442 ImGuiWindowFlags_NoScrollbar |
1443 ImGuiWindowFlags_NoScrollWithMouse))
1444 walkPaneChild(host, tree, first);
1445 ImGui::EndChild();
1446 }
1447 if (row) ImGui::SameLine(0.f, 0.f);
1448 const ImVec2 handleSize(row ? splitter : size.x, row ? size.y : splitter);
1449 ImGui::InvisibleButton("splitter", handleSize);
1450 const bool hovered = ImGui::IsItemHovered();
1451 const bool active = ImGui::IsItemActive();
1452 if (hovered || active)
1453 ImGui::SetMouseCursor(row ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS);
1454 ImGui::GetWindowDrawList()->AddRectFilled(
1455 ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
1456 ImGui::GetColorU32(active ? ImGuiCol_CheckMark
1457 : (hovered ? ImGuiCol_SeparatorHovered
1458 : ImGuiCol_Separator)));
1459 if (active) {
1460 const float delta = row ? ImGui::GetIO().MouseDelta.x : ImGui::GetIO().MouseDelta.y;
1461 const float next = std::max(ratioLo, std::min(ratioHi, ratio + delta / mainSize));
1462 if (next != ratio) {
1463 n.value = next;
1464 ImGui::GetStateStorage()->SetFloat(ratioStateId, next);
1465 pushPending(host, n, "value", n.handlerValue, false, next);
1466 }
1467 }
1468 if (row) ImGui::SameLine(0.f, 0.f);
1469 if (second >= 0) {
1470 const ImVec2 paneSize(row ? secondMain : size.x, row ? size.y : secondMain);
1471 if (ImGui::BeginChild("second", paneSize, false,
1472 ImGuiWindowFlags_NoScrollbar |
1473 ImGuiWindowFlags_NoScrollWithMouse))
1474 walkPaneChild(host, tree, second);
1475 ImGui::EndChild();
1476 }
1477 ImGui::PopStyleVar();
1478 ImGui::EndGroup();
1479 ImGui::PopID();
1480 break;
1481 }
1482 case NodeType::ScrollList: {
1483 // Virtualized list: only the rows intersecting the viewport are drawn.
1484 std::vector<int> kids;
1485 for (int c = n.firstChild; c >= 0; c = tree->nodes[size_t(c)].nextSibling) {
1486 if (c >= int(tree->nodes.size())) break;
1487 if (tree->nodes[size_t(c)].visible) kids.push_back(c);
1488 }
1489 const int total = int(kids.size());
1490 const char *cid = n.id.empty() ? "scrolllist" : n.id.c_str();
1491 const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f,
1492 n.sizeY > 0.f ? n.sizeY : 120.f);
1493 ImGui::BeginChild(cid, size, true);
1494 const float itemH = n.itemHeight > 0.f ? n.itemHeight : ImGui::GetFrameHeight();
1495 const float contentW = ImGui::GetContentRegionAvail().x;
1496 const float scrollY = ImGui::GetScrollY();
1497 const float viewH = ImGui::GetWindowHeight();
1498 if (total > 0 && itemH > 0.f) {
1499 int first = int(scrollY / itemH);
1500 if (first < 0) first = 0;
1501 if (first > total) first = total;
1502 int last = int(std::ceil((scrollY + viewH) / itemH));
1503 if (last < first) last = first;
1504 if (last > total) last = total;
1505 for (int i = first; i < last; ++i) {
1506 UINode &child = tree->nodes[size_t(kids[size_t(i)])];
1507 ImGui::SetCursorPos(ImVec2(0.f, float(i) * itemH));
1508 const float oldX = child.sizeX;
1509 const float oldY = child.sizeY;
1510 if (contentW > 0.f) child.sizeX = contentW;
1511 if (contentW > 0.f) ImGui::PushItemWidth(contentW);
1512 walkNode(host, tree, kids[size_t(i)]);
1513 child.sizeX = oldX;
1514 child.sizeY = oldY;
1515 if (contentW > 0.f) ImGui::PopItemWidth();
1516 // Force the row to occupy exactly itemH so rows stay uniform.
1517 ImGui::SetCursorPosY(float(i) * itemH + itemH);
1518 }
1519 }
1520 // Establish the full scrollable extent (scrollbar reflects total rows).
1521 ImGui::SetCursorPosY(float(total) * itemH);
1522 ImGui::EndChild();
1523 break;
1524 }
1525 case NodeType::Viewport: {
1526 const float w = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
1527 const float h = n.sizeY > 0.f ? n.sizeY : ImGui::GetContentRegionAvail().y;
1528 const ImVec2 size(std::max(1.f, w), std::max(1.f, h));
1529 const std::string key = viewportKey(host, n);
1530 auto viewport = UISystem::ensureViewport(key, int(size.x), int(size.y));
1531
1532 const ImVec2 rectMin = ImGui::GetCursorScreenPos();
1533 const std::string label = "##vp" + (n.id.empty() ? std::string("viewport") : n.id);
1534 ImGui::InvisibleButton(label.c_str(), size);
1535 const bool hovered = ImGui::IsItemHovered();
1536 const bool active = ImGui::IsItemActive();
1537 const ImVec2 mouse = ImGui::GetMousePos();
1538 if (viewport.ok()) {
1539 ViewportState &vs = viewport.value().get();
1540 vs.hovered = hovered;
1541 vs.active = active;
1542 vs.mouseX = mouse.x - rectMin.x;
1543 vs.mouseY = mouse.y - rectMin.y;
1544 if (active) {
1545 vs.dragDX = ImGui::GetIO().MouseDelta.x;
1546 vs.dragDY = ImGui::GetIO().MouseDelta.y;
1547 } else {
1548 vs.dragDX = 0.f;
1549 vs.dragDY = 0.f;
1550 }
1551 vs.wheel = hovered ? ImGui::GetIO().MouseWheel : 0.f;
1552
1553 if (vs.textureId && g_backend && g_backend->usesQueuedTextureDraws()) {
1554 g_backend->queueTextureDraw(vs.textureId, rectMin.x, rectMin.y, size.x, size.y, 0.f, 0.f, 1.f, 1.f, 1.f,
1555 1.f, 1.f, 1.f, true);
1556 } else if (vs.textureId && g_backend) {
1557 void *handle = g_backend->textureHandle(vs.textureId);
1558 if (handle) {
1559 ImGui::SetCursorScreenPos(rectMin);
1560 ImGui::Image(static_cast<ImTextureID>(handle), size);
1561 } else {
1562 ImGui::GetWindowDrawList()->AddRectFilled(rectMin, ImVec2(rectMin.x + size.x, rectMin.y + size.y),
1563 IM_COL32(10, 12, 16, 255));
1564 }
1565 } else {
1566 ImGui::GetWindowDrawList()->AddRectFilled(
1567 rectMin, ImVec2(rectMin.x + size.x, rectMin.y + size.y),
1568 IM_COL32(10, 12, 16, 255));
1569 }
1570 } else {
1571 ImGui::GetWindowDrawList()->AddRectFilled(
1572 rectMin, ImVec2(rectMin.x + size.x, rectMin.y + size.y), IM_COL32(10, 12, 16, 255));
1573 }
1574 break;
1575 }
1576 case NodeType::Flex:
1577 walkFlex(host, tree, n);
1578 break;
1579 case NodeType::Grid:
1580 walkGrid(host, tree, n);
1581 break;
1582 case NodeType::Spacer:
1583 // Outside Flex: honor explicit size, otherwise a tiny dummy.
1584 emitSpacer(n.sizeX > 0.f ? n.sizeX : 0.f, n.sizeY > 0.f ? n.sizeY : 0.f, true);
1585 break;
1586 }
1587
1588 if (interactive) {
1589 n.focused = ImGui::IsItemFocused();
1590 routeExplicitFocusInput(host, n);
1591 n.focusRequested = false;
1592 }
1593 if (disableNavigation) ImGui::PopItemFlag();
1594 if (disableItem) {
1595 ImGui::PopStyleVar();
1596 ImGui::PopItemFlag();
1597 }
1598
1599 renderDragDrop(host, n);
1600 if (!n.tooltip.empty() && ImGui::IsItemHovered(ImGuiHoveredFlags_None))
1601 ImGui::SetTooltip("%s", n.tooltip.c_str());
1602
1603 if (styleVarCount > 0) ImGui::PopStyleVar(styleVarCount);
1604 if (styleColorCount > 0) ImGui::PopStyleColor(styleColorCount);
1605
1606 if (scopedTheme) {
1607 ImGui::GetStyle() = previousStyle;
1608 ImGui::GetIO().FontGlobalScale = previousFontScale;
1609 ImGui::GetIO().ConfigFlags = previousConfigFlags;
1610 }
1611}
1612
1613void walk(UIHost *host, UIHost::Tree *tree, int index) { walkSiblings(host, tree, index); }
1614
1615} // namespace
1616
1617std::vector<UIEvent> &UISystem::pendingEvents() { return g_pending; }
1618std::vector<UIClick> &UISystem::clickQueue() { return g_clicks; }
1619std::vector<UIChange> &UISystem::changeQueue() { return g_changes; }
1620std::vector<UIDrop> &UISystem::dropQueue() { return g_drops; }
1621
1623#if (defined(_WIN32) || defined(__APPLE__) || defined(__linux__)) && \
1624 !defined(__ANDROID__) && !defined(__EMSCRIPTEN__)
1626#else
1628#endif
1629}
1630
1631void UISystem::enqueuePlatformFileDrop(const std::string &path) {
1632 if (dragDropSupport() == DragDropSupport::Supported && !path.empty())
1633 g_pendingFileDrops.push_back(path);
1634}
1635
1636std::optional<UIDrop> UISystem::consumeDrop() {
1637 if (g_drops.empty()) return std::nullopt;
1638 UIDrop result = std::move(g_drops.front());
1639 g_drops.erase(g_drops.begin());
1640 return result;
1641}
1642
1643void UISystem::setBackend(UIBackend &backend) { g_backend = &backend; }
1644void UISystem::clearBackend() noexcept { g_backend = nullptr; }
1645std::optional<std::reference_wrapper<UIBackend>> UISystem::backend() {
1646 if (!g_backend) return std::nullopt;
1647 return std::ref(*g_backend);
1648}
1649const UIStats &UISystem::stats() { return g_stats; }
1650
1653 if (key.empty() || w <= 0 || h <= 0) {
1654 return Return::failure(eve::Status::failure(
1656 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "viewport key and dimensions must be valid")));
1657 }
1658 auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>("Graphics");
1659 if (!gfx) {
1660 return Return::failure(eve::Status::failure(
1662 eve::Diagnostic::error(eve::DiagnosticCode::Unsupported, "viewport requires the Graphics module")));
1663 }
1664 ViewportState &vs = g_viewports[key];
1665 vs.key = key;
1666 if (!vs.canvas || vs.width != w || vs.height != h) {
1667 graphics::Canvas *canvas = gfx->newCanvas(w, h);
1668 if (!canvas) {
1669 return Return::failure(eve::Status::failure(
1671 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "Graphics could not create the viewport canvas")));
1672 }
1673 if (vs.textureId && g_backend) g_backend->unregisterTexture(vs.textureId);
1674 vs.textureId = 0;
1675 vs.canvas = canvas;
1676 vs.width = w;
1677 vs.height = h;
1678 if (vs.canvas && g_backend)
1679 vs.textureId = g_backend->registerTexture(vs.canvas->getTexture());
1680 }
1681 return Return::success(std::ref(vs));
1682}
1683
1684std::optional<std::reference_wrapper<ViewportState>> UISystem::viewportState(const std::string &hostName,
1685 const std::string &nodeId) {
1686 auto it = g_viewports.find(hostName + "/" + nodeId);
1687 if (it == g_viewports.end()) return std::nullopt;
1688 return std::ref(it->second);
1689}
1690
1692 if (name.empty()) return {};
1693 if (ecs::current()->getManager<UIHost>() == nullptr) return {};
1694 auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
1695 for (auto it = view.begin(); it != view.end(); ++it) {
1696 auto [meta, tree] = *it;
1697 (void)tree;
1698 if (meta->name == name && UIHost::resolve(meta->entity)) return meta->entity;
1699 }
1700 return {};
1701}
1702
1704 if (ownerId == 0) return {};
1705 if (ecs::current()->getManager<UIHost>() == nullptr) return {};
1706 auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
1707 for (auto it = view.begin(); it != view.end(); ++it) {
1708 auto [meta, tree] = *it;
1709 (void)tree;
1710 if (meta->ownerId == ownerId && UIHost::resolve(meta->entity)) return meta->entity;
1711 }
1712 return {};
1713}
1714
1716 EV_PROFILE_MODULE("ui", "UISystem::render");
1717 if (ecs::current()->getManager<UIHost>() == nullptr) return;
1718
1720
1721 struct Item {
1722 UIHost *host;
1723 UIHost::Meta *meta;
1724 UIHost::Tree *tree;
1725 bool render = true;
1726 };
1727 std::vector<Item> items;
1728
1729 graphics::Camera3D *camera = activeCamera();
1730 const ImVec2 display = ImGui::GetIO().DisplaySize;
1731 auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
1732 for (auto it = view.begin(); it != view.end(); ++it) {
1733 auto [meta, tree] = *it;
1734 if (!meta->visible) continue;
1735 auto host = UIHost::resolve(meta->entity);
1736 if (!host) continue;
1737 if (!updateWorldAnchor(host->get(), camera, display.x, display.y)) continue;
1738 items.push_back(Item{&host->get(), meta, tree, true});
1739 }
1740
1741 std::stable_sort(items.begin(), items.end(),
1742 [](const Item &a, const Item &b) { return a.meta->layer < b.meta->layer; });
1743
1744 g_stats.nodeCount = 0;
1745 g_stats.measureMs = 0.0;
1746 g_stats.walkMs = 0.0;
1747 for (auto &item : items) {
1748 item.tree->dirty = false;
1749 HostThemeScope theme(item.host);
1750 const auto m0 = std::chrono::steady_clock::now();
1751 measureTree(*item.tree);
1752 const auto m1 = std::chrono::steady_clock::now();
1753 g_stats.nodeCount += int(item.tree->nodes.size());
1754 g_stats.measureMs +=
1755 std::chrono::duration<double, std::milli>(m1 - m0).count();
1756 }
1757
1758 std::vector<WorldAnchorLayoutItem> anchorItems;
1759 anchorItems.reserve(items.size());
1760 const ImGuiStyle &style = ImGui::GetStyle();
1761 for (std::size_t i = 0; i < items.size(); ++i) {
1762 auto &item = items[i];
1763 auto anchor = item.host->worldAnchor();
1764 if (!anchor->enabled) continue;
1765 float width = item.meta->hasSize ? item.meta->sizeX : 0.f;
1766 float height = item.meta->hasSize ? item.meta->sizeY : 0.f;
1767 if (item.meta->percentW > 0.f) width = item.meta->percentW * display.x;
1768 if (item.meta->percentH > 0.f) height = item.meta->percentH * display.y;
1769 if (item.tree->root >= 0) {
1770 const auto &root = item.tree->nodes[std::size_t(item.tree->root)];
1771 if (width <= 0.f) width = root.measuredW + style.WindowPadding.x * 2.f;
1772 if (height <= 0.f) height = root.measuredH + style.WindowPadding.y * 2.f;
1773 }
1774 width *= anchor->scale;
1775 height *= anchor->scale;
1776 anchorItems.push_back({i,
1777 anchor->screenX,
1778 anchor->screenY,
1779 width,
1780 height,
1781 item.meta->pivotX,
1782 item.meta->pivotY,
1783 anchor->depth,
1784 anchor->safeMargin,
1785 anchor->overlapPadding,
1786 anchor->maxDisplacement,
1787 anchor->overlapPriority,
1788 anchor->overlapPolicy == WorldAnchorOverlapPolicy::Avoid});
1789 }
1790 for (const auto &resolved :
1791 resolveWorldAnchorOverlaps(std::move(anchorItems), display.x, display.y)) {
1792 auto &item = items[resolved.stableIndex];
1793 auto anchor = item.host->worldAnchor();
1794 anchor->screenX = resolved.screenX;
1795 anchor->screenY = resolved.screenY;
1796 anchor->displacementX = resolved.displacementX;
1797 anchor->displacementY = resolved.displacementY;
1798 item.render = resolved.render;
1799 if (!resolved.render) anchor->state = WorldAnchorState::Crowded;
1800 }
1801
1802 g_stats.hostCount = int(std::count_if(items.begin(), items.end(),
1803 [](const Item &item) { return item.render; }));
1804 for (auto &item : items) {
1805 if (!item.render) continue;
1806 HostThemeScope theme(item.host);
1807 const auto w0 = std::chrono::steady_clock::now();
1808 if (item.tree->root >= 0) walk(item.host, item.tree, item.tree->root);
1809 const auto w1 = std::chrono::steady_clock::now();
1810 g_stats.walkMs += std::chrono::duration<double, std::milli>(w1 - w0).count();
1811 }
1812 // Animation overrides are one-frame: Motion/tweens re-assert them before the
1813 // next render. Clearing here restores movable/resizable hosts after the sample.
1814 for (auto &item : items) {
1815 item.meta->animDrivePos = false;
1816 item.meta->animDriveSize = false;
1817 }
1818}
1819
1821 auto events = std::move(g_pending);
1822 g_pending.clear();
1823 for (auto &ev : events) {
1824 auto resolvedHost = UIHost::resolve(ev.host);
1825 if (!resolvedHost) continue;
1826 UIHost *host = &resolvedHost->get();
1827
1828 if (ev.kind == "click" && !ev.nodeId.empty()) {
1829 UIClick c;
1830 c.hostName = ev.hostName.empty() ? host->meta()->name : ev.hostName;
1831 c.nodeId = ev.nodeId;
1832 g_clicks.push_back(std::move(c));
1833 }
1834
1835 if ((ev.kind == "toggle" || ev.kind == "value" || ev.kind == "text") && !ev.nodeId.empty()) {
1836 UIChange ch;
1837 ch.hostName = ev.hostName.empty() ? host->meta()->name : ev.hostName;
1838 ch.nodeId = ev.nodeId;
1839 ch.kind = ev.kind;
1840 g_changes.push_back(std::move(ch));
1841 }
1842
1843 if (ev.kind == "click") {
1844 auto tree = host->tree();
1845 int index = ev.nodeIndex;
1846 if (index < 0 || index >= int(tree->nodes.size()) ||
1847 tree->nodes[size_t(index)].id != ev.nodeId) {
1848 auto target = host->findById(ev.nodeId);
1849 index = target && !tree->nodes.empty() ? int(&target->get() - tree->nodes.data()) : -1;
1850 }
1851
1852 bool first = true;
1853 while (index >= 0 && index < int(tree->nodes.size())) {
1854 const UINode &node = tree->nodes[size_t(index)];
1855 if (!node.visible || !node.enabled) break;
1856 const uint32_t handler = first ? ev.handlerIndex : node.handlerClick;
1857 if (node.mouseFilter != MouseFilter::Ignore && handler != 0) {
1858 const size_t handlerSlot = size_t(handler - 1);
1859 if (handlerSlot < tree->clickHandlers.size() &&
1860 tree->clickHandlers[handlerSlot])
1861 tree->clickHandlers[handlerSlot]();
1862 }
1863 if (node.mouseFilter == MouseFilter::Stop) break;
1864 index = node.parent;
1865 first = false;
1866 }
1867 continue;
1868 }
1869
1870 if (ev.kind == "toggle" && ev.handlerIndex != 0) {
1871 auto t = host->tree();
1872 size_t idx = size_t(ev.handlerIndex - 1);
1873 if (idx < t->toggleHandlers.size() && t->toggleHandlers[idx])
1874 t->toggleHandlers[idx](ev.toggleValue);
1875 continue;
1876 }
1877 if (ev.kind == "value" && ev.handlerIndex != 0) {
1878 auto t = host->tree();
1879 size_t idx = size_t(ev.handlerIndex - 1);
1880 if (idx < t->valueHandlers.size() && t->valueHandlers[idx])
1881 t->valueHandlers[idx](ev.floatValue);
1882 continue;
1883 }
1884 if (ev.kind == "text" && ev.handlerIndex != 0) {
1885 auto t = host->tree();
1886 size_t idx = size_t(ev.handlerIndex - 1);
1887 if (idx < t->textHandlers.size() && t->textHandlers[idx])
1888 t->textHandlers[idx](ev.textValue);
1889 continue;
1890 }
1891 }
1892}
1893
1895 if (g_clicks.empty()) return {};
1896 UIClick c = std::move(g_clicks.front());
1897 g_clicks.erase(g_clicks.begin());
1898 if (c.hostName.empty()) return c.nodeId;
1899 return c.hostName + "/" + c.nodeId;
1900}
1901
1902std::string UISystem::consumeClickFor(const std::string &hostName) {
1903 for (auto it = g_clicks.begin(); it != g_clicks.end(); ++it) {
1904 if (it->hostName != hostName) continue;
1905 std::string id = std::move(it->nodeId);
1906 g_clicks.erase(it);
1907 return id;
1908 }
1909 return {};
1910}
1911
1913 if (g_changes.empty()) return {};
1914 UIChange c = std::move(g_changes.front());
1915 g_changes.erase(g_changes.begin());
1916 if (c.hostName.empty()) return c.nodeId;
1917 return c.hostName + "/" + c.nodeId;
1918}
1919
1920} // namespace eve::ui
LogicalId target
double value
Value::Object payload
Duration start
bool & active
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
const std::string & s
float gap
Vec3 projected
Definition CaveMesh.cpp:122
Vec3 anchor
Definition CaveMesh.cpp:90
std::string title
std::string label
std::string nodeId
std::map< std::string, ImVec2 > rectMin
Object::Ptr theme
std::map< std::string, ImVec2 > rectMax
int rows
int cols
std::string layout
tensor::Graph g
Definition GpuGraph.cpp:7
float camera[2]
std::uint32_t capacity
std::uint32_t key
wgpu::PopErrorScopeStatus status
glm::vec4 tint
glm::vec3 n
Definition Grass.cpp:63
double r
bool border
float v
HexVec3 up
std::int32_t second
std::int32_t c
std::int32_t first
const char * glyph
Definition Icons.cpp:13
int h
std::uint32_t height
std::uint32_t width
std::string text
TokenKind kind
std::array< float, 3 > scale
std::int32_t parent
HSQOBJECT handler
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
MeshVfxBatchedDraw draw
int idx
float radius
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
PrimitiveHandle handle
#define EV_PROFILE_MODULE(module, name)
Profile the enclosing scope, tagged with a module for grouping.
Definition Profile.h:140
float t
glm::vec3 eye
uint64_t token
glm::mat4 view
V3 origin
Definition RoadBake.cpp:138
const RoadNode * node
Light3D::Data * light
std::uint32_t count
Cell cell
Battle::Events events
std::size_t cursor
int spacing
int columns
float size
Definition TreeMesh.cpp:156
UIHostHandle host
uint64_t serial
Definition UISystem.cpp:68
ImGuiConfigFlags previousConfigFlags
Definition UISystem.cpp:45
std::string payloadType
Definition UISystem.cpp:71
std::string payloadText
Definition UISystem.cpp:72
float previousFontScale
Definition UISystem.cpp:44
ImGuiStyle previousStyle
Definition UISystem.cpp:43
std::string sourceNodeId
Definition UISystem.cpp:70
std::string sourceHostName
Definition UISystem.cpp:69
uint32_t index
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 Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
Definition Status.h:84
EVENGINE_API_BACKENDS public API.
Canvas public API.
Definition Canvas.h:17
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:20
ECS mount point for one UI panel/screen. Subclass to attach UI to game entities, e....
Definition UIHost.h:243
static eve::OptionalRef< UIHost > resolve(UIHostHandle handle) noexcept
Resolves a UI host handle with ECS generation checking.
Definition UIHost.cpp:61
static std::vector< UIDrop > & dropQueue()
Returns the owning completed-drop queue.
static const UIStats & stats()
static void setBackend(UIBackend &backend)
Sets the borrowed backend used for the current UI/render lifetime.
static void clearBackend() noexcept
Clears the backend association before the backend is shut down.
static eve::OptionalRef< ViewportState > viewportState(const std::string &hostName, const std::string &nodeId)
Returns viewport state for a host/node key when it exists.
static void enqueuePlatformFileDrop(const std::string &path)
Copies an OS file-drop path into UI-owned pending state.
static std::vector< UIClick > & clickQueue()
static std::string consumeChange()
Pop next change as "name/node"; empty if none.
static eve::OptionalRef< UIBackend > backend()
Returns the configured backend for the current synchronous operation.
static UIHostHandle findHost(const std::string &name)
Lookup by Meta.name across the UIHost View.
static std::vector< UIChange > & changeQueue()
Change queue.
static void dispatchEvents()
Dispatches events.
static eve::ResultRef< ViewportState > ensureViewport(const std::string &key, int w, int h)
Creates or refreshes offscreen state for a viewport key.
static DragDropSupport dragDropSupport() noexcept
Returns retained drag-and-drop availability for this build target.
static std::string consumeClick()
Pop next click as "name/node"; empty if none.
static void render()
Walk all UIHost (+ subclasses) via ECS View.
static UIHostHandle findHostByOwner(uint32_t ownerId)
Find the first host with Meta.ownerId equal to ownerId, or null.
static std::optional< UIDrop > consumeDrop()
Pops the oldest completed drop, or returns empty when none exists.
static std::string consumeClickFor(const std::string &hostName)
Pop next click for a specific host; returns node id only (or "").
static std::vector< UIEvent > & pendingEvents()
Pending events.
int main(int argc, char **argv)
Definition main.cpp:46
glm::mat4 cameraProjectionVulkanRH_ZO(bool orthographic, float fovyRad, float orthoHeight, float aspect, float zNear, float zFar)
Camera projection shared by rendering, picking, and diagnostic passes.
Definition ClipSpace.h:38
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
double cross(const Vec2 &a, const Vec2 &b)
Cross.
Definition UrbanTypes.h:36
ControlEdit slider(const char *label, float &value, float minimum, float maximum, const char *format="%.3f")
Shared immediate renderer for scalar sliders.
ControlEdit button(const char *label, float width=0.f, float height=0.f)
Shared immediate renderer for retained and EditorHost buttons.
ControlEdit combo(const char *label, int &selected, const std::vector< const char * > &items)
Shared immediate renderer for indexed combo boxes.
ControlEdit inputText(const char *label, std::string &value, bool multiline=false, ImVec2 multilineSize=ImVec2(0.f, 96.f))
UTF-8 owning text editor with automatic buffer growth.
ControlEdit inputTextWithHint(const char *label, const char *hint, std::string &value)
UTF-8 owning single-line editor with placeholder text and automatic growth.
ControlEdit checkbox(const char *label, bool &value)
Shared immediate renderer for retained and EditorHost checkboxes.
ControlEdit
Result of drawing one immediate control in the current frame.
const char * iconGlyph(Icon icon)
Returns the UTF-8 glyph for an icon, or an empty string for None.
Definition Icons.cpp:86
DragDropSupport
Build-target availability of retained drag-and-drop.
Definition UISystem.h:53
GridResult gridArrange(int columns, float columnGap, float rowGap, float availWidth, const std::vector< GridItemSpec > &items)
Places source-ordered items into equal-width fixed columns.
Definition Layout.cpp:276
void measureTree(UIHost::Tree &tree)
Measure tree.
Definition Layout.cpp:667
std::vector< WorldAnchorLayoutResult > resolveWorldAnchorOverlaps(std::vector< WorldAnchorLayoutItem > items, float viewportWidth, float viewportHeight)
Resolves projected host overlap with bounded deterministic vertical displacement.
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
FocusDirection
Logical focus movement understood by keyboard, gamepad and automation.
Definition UIHost.h:82
WidgetDesc grid(int columns, std::vector< WidgetDesc > children, std::string id)
Fixed-column grid; children are placed in source order.
Definition Widget.cpp:687
WidgetDesc viewport(std::string id, float width, float height)
Viewport.
Definition Widget.cpp:443
WidgetDesc flex(FlexDirection direction, std::vector< WidgetDesc > children, std::string id)
Elastic layout container (row/column). Prefer row / column shorthands.
Definition Widget.cpp:669
ecs::EntityHandle UIHostHandle
Definition UIHost.h:16
WorldAnchorProjection projectWorldAnchor(const UIHost::WorldAnchor &anchor, const glm::mat4 &viewProjection, float cameraX, float cameraY, float cameraZ, float viewportWidth, float viewportHeight)
Projects one world-space UI anchor using a Vulkan RH-ZO view-projection matrix.
Theme & globalTheme()
Global theme.
Definition Theme.cpp:277
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:679
FlexResult flexArrange(bool row, float gap, float availMain, float availCross, FlexAlign containerAlign, FlexJustify justify, const std::vector< FlexItemSpec > &items, bool wrap, float crossGap)
Flex arrange.
Definition Layout.cpp:201
void applyThemeToImGui(const Theme &theme)
Push tokens into ImGui style. Metrics are multiplied by uiScale (default: themeUiScale()).
Definition Theme.cpp:313
float themeUiScale()
Theme ui scale.
Definition Theme.cpp:305
float searchIconGap
Definition Theme.h:31
float sectionSpacingY
Definition Theme.h:29
ThemeLayout layout
Definition Theme.h:41
static Theme dark()
Built-in presets with matching geometry; only colors differ.
Definition Theme.cpp:174
Value/text/toggle change for script: "hostName/nodeId".
Definition UISystem.h:43
std::string kind
Definition UISystem.h:46
std::string nodeId
Definition UISystem.h:45
std::string hostName
Definition UISystem.h:44
One script-facing click: "hostName/nodeId".
Definition UISystem.h:37
std::string hostName
Definition UISystem.h:38
One completed desktop drop with no borrowed node or platform pointers.
Definition UISystem.h:56
Meta public API.
Definition UIHost.h:252
Retained UI widget node (arena tree). Conceptual counterpart of eve::scene::SceneNode; built declarat...
Definition UIHost.h:128
uint32_t handlerClick
Definition UIHost.h:230
UIStats public API.
Definition UISystem.h:68
ViewportState public API.
Definition UISystem.h:81
graphics::Canvas * canvas
Definition UISystem.h:83
NodeType type
Definition Widget.h:16
FlexWrap flexWrap
Definition Widget.h:99
FlexDirection flexDirection
Definition Widget.h:89
FlexJustify justifyContent
Definition Widget.h:91
FlexAlign alignItems
Definition Widget.h:90
OverflowMode overflow
Definition Widget.h:100
glm::vec4 color