#include "ui/UISystem.h" #include "ui/Layout.h" #include "ui/Theme.h" #include "ui/UIBackend.h" #include "common/Module.h" #include "graphics/Graphics.h" #include #include #include #include #include #include #include namespace eve::ui { namespace { std::vector g_pending; std::vector g_clicks; std::vector g_changes; UIBackend *g_backend = nullptr; UIStats g_stats; std::map g_viewports; std::string viewportKey(UIHost *host, const UINode &n) { const std::string hostName = host && !host->meta()->name.empty() ? host->meta()->name : "?"; return hostName + "/" + (n.id.empty() ? "viewport" : n.id); } void pushPending(UIHost *host, const UINode &n, const char *kind, uint32_t handlerIndex, bool toggleValue = false, float floatValue = 0.f, std::string textValue = {}) { UIEvent ev; ev.host = host; ev.hostName = host ? host->meta()->name : ""; ev.nodeId = n.id; ev.kind = kind; ev.handlerIndex = handlerIndex; ev.toggleValue = toggleValue; ev.floatValue = floatValue; ev.textValue = std::move(textValue); g_pending.push_back(std::move(ev)); } void walkNode(UIHost *host, UIHost::Tree *tree, int index); void walkSiblings(UIHost *host, UIHost::Tree *tree, int index) { while (index >= 0 && index < int(tree->nodes.size())) { const int next = tree->nodes[size_t(index)].nextSibling; walkNode(host, tree, index); index = next; } } void emitSpacer(float main, float cross, bool row) { if (main < 0.f) main = 0.f; if (cross < 0.f) cross = 0.f; if (row) { if (main <= 0.f && cross <= 0.f) { ImGui::Dummy(ImVec2(0.f, 0.f)); } else { ImGui::Dummy(ImVec2(main, cross > 0.f ? cross : 1.f)); } } else { if (main <= 0.f && cross <= 0.f) { ImGui::Dummy(ImVec2(0.f, 0.f)); } else { ImGui::Dummy(ImVec2(cross > 0.f ? cross : 1.f, main)); } } } /** Draw a texture with optional nine-patch borders (ImGui 1.83 has no AddImageNineSlice). */ void drawNinePatch(void *handle, const ImVec2 &pos, const ImVec2 &size, const ImVec2 &uv0, const ImVec2 &uv1, float bL, float bT, float bR, float bB, int texW, int texH, ImU32 tint) { ImDrawList *dl = ImGui::GetWindowDrawList(); const auto draw = [&](float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1) { dl->AddImage(static_cast(handle), ImVec2(pos.x + x0, pos.y + y0), ImVec2(pos.x + x1, pos.y + y1), ImVec2(u0, v0), ImVec2(u1, v1), tint); }; if (bL <= 0.f && bT <= 0.f && bR <= 0.f && bB <= 0.f) { draw(0.f, 0.f, size.x, size.y, uv0.x, uv0.y, uv1.x, uv1.y); return; } if (texW <= 0 || texH <= 0) { draw(0.f, 0.f, size.x, size.y, uv0.x, uv0.y, uv1.x, uv1.y); return; } const float uPerPx = (uv1.x - uv0.x) / float(texW); const float vPerPx = (uv1.y - uv0.y) / float(texH); const float uL = uv0.x + std::min(bL, size.x * 0.5f) * uPerPx; const float uR = uv1.x - std::min(bR, size.x * 0.5f) * uPerPx; const float vT = uv0.y + std::min(bT, size.y * 0.5f) * vPerPx; const float vB = uv1.y - std::min(bB, size.y * 0.5f) * vPerPx; const float bl = std::min(bL, size.x * 0.5f); const float br = std::min(bR, size.x * 0.5f); const float bt = std::min(bT, size.y * 0.5f); const float bb = std::min(bB, size.y * 0.5f); // Corners draw(0.f, 0.f, bl, bt, uv0.x, uv0.y, uL, vT); draw(size.x - br, 0.f, size.x, bt, uR, uv0.y, uv1.x, vT); draw(0.f, size.y - bb, bl, size.y, uv0.x, vB, uL, uv1.y); draw(size.x - br, size.y - bb, size.x, size.y, uR, vB, uv1.x, uv1.y); // Edges draw(bl, 0.f, size.x - br, bt, uL, uv0.y, uR, vT); draw(bl, size.y - bb, size.x - br, size.y, uL, vB, uR, uv1.y); draw(0.f, bt, bl, size.y - bb, uv0.x, vT, uL, vB); draw(size.x - br, bt, size.x, size.y - bb, uR, vT, uv1.x, vB); // Center draw(bl, bt, size.x - br, size.y - bb, uL, vT, uR, vB); } void walkFlex(UIHost *host, UIHost::Tree *tree, UINode &flex) { const bool row = flex.flexDirection == FlexDirection::Row; const ImGuiStyle &style = ImGui::GetStyle(); const float gap = flex.gap >= 0.f ? flex.gap : (row ? style.ItemSpacing.x : style.ItemSpacing.y); // Own content box: an explicit size (set by the parent's arrange) wins; // otherwise use the available region (root flex inside a window/child). const ImVec2 avail = ImGui::GetContentRegionAvail(); const float flexW = flex.sizeX > 0.f ? flex.sizeX : avail.x; const float flexH = flex.sizeY > 0.f ? flex.sizeY : avail.y; const float padMain = row ? flex.paddingL + flex.paddingR : flex.paddingT + flex.paddingB; const float padCross = row ? flex.paddingT + flex.paddingB : flex.paddingL + flex.paddingR; const float availMain = (row ? flexW : flexH) - padMain; const float availCross = (row ? flexH : flexW) - padCross; std::vector kids; std::vector absKids; std::vector specs; for (int c = flex.firstChild; c >= 0; c = tree->nodes[size_t(c)].nextSibling) { if (c >= int(tree->nodes.size())) break; UINode &child = tree->nodes[size_t(c)]; if (!child.visible) continue; if (child.absolute) { absKids.push_back(c); continue; } kids.push_back(c); FlexItemSpec s; s.basisMain = row ? child.measuredW : child.measuredH; s.basisCross = row ? child.measuredH : child.measuredW; s.marginBefore = row ? child.marginL : child.marginT; s.marginAfter = row ? child.marginR : child.marginB; s.marginCrossBefore = row ? child.marginT : child.marginL; s.marginCrossAfter = row ? child.marginB : child.marginR; s.flexGrow = child.flexGrow; s.isSpacer = child.type == NodeType::Spacer; s.minMain = row ? child.minSizeX : child.minSizeY; s.maxMain = row ? child.maxSizeX : child.maxSizeY; s.minCross = row ? child.minSizeY : child.minSizeX; s.maxCross = row ? child.maxSizeY : child.maxSizeX; s.percentMain = row ? child.percentW : child.percentH; s.percentCross = row ? child.percentH : child.percentW; s.explicitMain = row ? child.sizeX : child.sizeY; s.explicitCross = row ? child.sizeY : child.sizeX; specs.push_back(s); } const FlexResult res = flexArrange(row, gap, std::max(0.f, availMain), std::max(0.f, availCross), flex.alignItems, flex.justifyContent, specs); const ImVec2 cursor = ImGui::GetCursorScreenPos(); const ImVec2 origin = ImVec2(cursor.x + flex.paddingL, cursor.y + flex.paddingT); for (size_t i = 0; i < kids.size(); ++i) { UINode &child = tree->nodes[size_t(kids[i])]; const FlexRect &r = res.items[i]; ImGui::SetCursorScreenPos(ImVec2(origin.x + r.x, origin.y + r.y)); const float oldX = child.sizeX; const float oldY = child.sizeY; bool pushedWidth = false; if (r.w > 0.f) child.sizeX = r.w; if (r.h > 0.f) child.sizeY = r.h; if (row || r.w > 0.f) { ImGui::PushItemWidth(r.w > 0.f ? r.w : 1.f); pushedWidth = true; } walkNode(host, tree, kids[i]); child.sizeX = oldX; child.sizeY = oldY; if (pushedWidth) ImGui::PopItemWidth(); } // Absolutely placed children are excluded from flow; draw on top of it. for (int c : absKids) { UINode &child = tree->nodes[size_t(c)]; float w = child.percentW > 0.f ? child.percentW * flexW : child.measuredW; float h = child.percentH > 0.f ? child.percentH * flexH : child.measuredH; if (child.sizeX > 0.f) w = child.sizeX; if (child.sizeY > 0.f) h = child.sizeY; if (child.minSizeX > 0.f) w = std::max(w, child.minSizeX); if (child.minSizeY > 0.f) h = std::max(h, child.minSizeY); if (child.maxSizeX > 0.f) w = std::min(w, child.maxSizeX); if (child.maxSizeY > 0.f) h = std::min(h, child.maxSizeY); const float x = child.anchorX * flexW + child.posX - child.anchorX * w; const float y = child.anchorY * flexH + child.posY - child.anchorY * h; ImGui::SetCursorScreenPos(ImVec2(origin.x + x, origin.y + y)); const float oldX = child.sizeX; const float oldY = child.sizeY; child.sizeX = w; child.sizeY = h; if (w > 0.f) ImGui::PushItemWidth(w); walkNode(host, tree, c); child.sizeX = oldX; child.sizeY = oldY; if (w > 0.f) ImGui::PopItemWidth(); } } void walkNode(UIHost *host, UIHost::Tree *tree, int index) { if (index < 0 || index >= int(tree->nodes.size())) return; UINode &n = tree->nodes[size_t(index)]; if (!n.visible) return; switch (n.type) { case NodeType::Window: { std::string title = n.text.empty() ? "Window" : n.text; if (host && !host->meta()->name.empty()) title = host->meta()->name + "/" + title; const bool modal = host && host->meta()->modal; ImGuiWindowFlags flags = 0; float winW = 0.f; float winH = 0.f; if (host && host->meta()->percentW > 0.f) winW = host->meta()->percentW * ImGui::GetIO().DisplaySize.x; else if (host && host->meta()->hasSize) winW = host->meta()->sizeX; if (host && host->meta()->percentH > 0.f) winH = host->meta()->percentH * ImGui::GetIO().DisplaySize.y; else if (host && host->meta()->hasSize) winH = host->meta()->sizeY; if (host && host->meta()->hasPos) { const ImVec2 display = ImGui::GetIO().DisplaySize; const float px = host->meta()->pivotX; const float py = host->meta()->pivotY; const float x = host->meta()->anchorX * display.x + host->meta()->posX - px * winW; const float y = host->meta()->anchorY * display.y + host->meta()->posY - py * winH; ImGui::SetNextWindowPos(ImVec2(x, y), ImGuiCond_Always, ImVec2(host->meta()->pivotX, host->meta()->pivotY)); flags |= ImGuiWindowFlags_NoMove; if (winW <= 0.f && winH <= 0.f) flags |= ImGuiWindowFlags_AlwaysAutoResize; } if (winW > 0.f || winH > 0.f) { ImGui::SetNextWindowSize( ImVec2(winW > 0.f ? winW : -1.f, winH > 0.f ? winH : -1.f), ImGuiCond_Always); } else if (n.measuredW > 0.f || n.measuredH > 0.f) { // Real measured size → stable auto-resize instead of ImGui's guess. ImGui::SetNextWindowContentSize(ImVec2(n.measuredW, n.measuredH)); } if (host && host->meta()->overlay) { flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize; ImGui::SetNextWindowBgAlpha(0.4f); } if (modal) { ImGui::OpenPopup(title.c_str()); if (ImGui::BeginPopupModal(title.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize | flags)) { if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild); ImGui::EndPopup(); } } else { ImGui::Begin(title.c_str(), nullptr, flags); if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild); ImGui::End(); } break; } case NodeType::Text: if (n.wrapWidth > 0.f) { ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + n.wrapWidth); ImGui::TextUnformatted(n.text.c_str()); ImGui::PopTextWrapPos(); } else { ImGui::TextUnformatted(n.text.c_str()); } break; case NodeType::Button: { const char *label = n.text.empty() ? "Button" : n.text.c_str(); const bool sized = n.sizeX > 0.f || n.sizeY > 0.f; const bool clicked = sized ? ImGui::Button(label, ImVec2(n.sizeX > 0.f ? n.sizeX : 0.f, n.sizeY > 0.f ? n.sizeY : 0.f)) : ImGui::Button(label); if (clicked) pushPending(host, n, "click", n.handlerClick); break; } case NodeType::SameLine: ImGui::SameLine(); break; case NodeType::Group: ImGui::BeginGroup(); if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild); ImGui::EndGroup(); break; case NodeType::Separator: ImGui::Separator(); break; case NodeType::Checkbox: { bool checked = n.checked; if (ImGui::Checkbox(n.text.empty() ? "Check" : n.text.c_str(), &checked)) { n.checked = checked; pushPending(host, n, "toggle", n.handlerToggle, checked); } break; } case NodeType::Slider: { float v = n.value; const char *label = n.text.empty() ? "Slider" : n.text.c_str(); if (ImGui::SliderFloat(label, &v, n.minValue, n.maxValue)) { n.value = v; pushPending(host, n, "value", n.handlerValue, false, v); } break; } case NodeType::Progress: { const char *overlay = n.text.empty() ? nullptr : n.text.c_str(); ImGui::ProgressBar(n.value, ImVec2(-1.f, 0.f), overlay); break; } case NodeType::Image: { const float w = n.sizeX > 0.f ? n.sizeX : 32.f; const float h = n.sizeY > 0.f ? n.sizeY : 32.f; const ImVec2 pos = ImGui::GetCursorScreenPos(); const ImVec2 size(w, h); const ImU32 tint = ImGui::ColorConvertFloat4ToU32(ImVec4(n.tintR, n.tintG, n.tintB, n.tintA)); if (n.textureId != 0 && g_backend) { void *handle = g_backend->textureHandle(n.textureId); int tw = 0, th = 0; g_backend->textureSize(n.textureId, &tw, &th); drawNinePatch(handle, pos, size, ImVec2(n.uv0x, n.uv0y), ImVec2(n.uv1x, n.uv1y), n.borderL, n.borderT, n.borderR, n.borderB, tw, th, tint); } else { ImGui::GetWindowDrawList()->AddRectFilled(pos, ImVec2(pos.x + w, pos.y + h), tint, n.cornerRadius); } ImGui::Dummy(size); break; } case NodeType::ImageButton: { const float w = n.sizeX > 0.f ? n.sizeX : 32.f; const float h = n.sizeY > 0.f ? n.sizeY : 32.f; const ImVec2 size(w, h); const std::string sid = n.id.empty() ? "imgbtn" : n.id; ImGui::PushID(sid.c_str()); bool clicked = false; if (n.textureId != 0 && g_backend) { void *handle = g_backend->textureHandle(n.textureId); if (handle) { clicked = ImGui::ImageButton(static_cast(handle), size, ImVec2(n.uv0x, n.uv0y), ImVec2(n.uv1x, n.uv1y), -1, ImVec4(0.f, 0.f, 0.f, 0.f), ImVec4(n.tintR, n.tintG, n.tintB, n.tintA)); } } if (!clicked && n.textureId == 0) { clicked = ImGui::InvisibleButton(sid.c_str(), size); const ImVec2 pmin = ImGui::GetItemRectMin(); const ImVec2 pmax = ImGui::GetItemRectMax(); ImU32 col = ImGui::ColorConvertFloat4ToU32(ImVec4(n.tintR, n.tintG, n.tintB, n.tintA)); if (ImGui::IsItemHovered()) col = ImGui::GetColorU32(ImGuiCol_ButtonHovered); ImGui::GetWindowDrawList()->AddRectFilled(pmin, pmax, col, n.cornerRadius); } ImGui::PopID(); if (clicked) pushPending(host, n, "click", n.handlerClick); break; } case NodeType::Combo: { // Options are newline-separated in valueText; selected index in value. std::vector storage; size_t start = 0; while (start <= n.valueText.size()) { const size_t end = n.valueText.find('\n', start); storage.push_back(n.valueText.substr( start, end == std::string::npos ? std::string::npos : end - start)); if (end == std::string::npos) break; start = end + 1; } std::vector items; items.reserve(storage.size()); for (const auto &s : storage) items.push_back(s.c_str()); int idx = int(n.value); const char *label = n.text.empty() ? "Combo" : n.text.c_str(); if (ImGui::Combo(label, &idx, items.data(), int(items.size()))) { n.value = float(idx); pushPending(host, n, "value", n.handlerValue, false, float(idx)); } break; } case NodeType::InputText: { char buf[1024]; std::memset(buf, 0, sizeof(buf)); if (!n.valueText.empty()) { std::strncpy(buf, n.valueText.c_str(), sizeof(buf) - 1); } const char *label = n.text.empty() ? "Input" : n.text.c_str(); if (ImGui::InputText(label, buf, sizeof(buf))) { n.valueText = buf; pushPending(host, n, "text", n.handlerText, false, 0.f, n.valueText); } break; } case NodeType::CollapsingHeader: { ImGuiTreeNodeFlags flags = n.open ? ImGuiTreeNodeFlags_DefaultOpen : 0; const char *label = n.text.empty() ? "Section" : n.text.c_str(); if (ImGui::CollapsingHeader(label, flags)) { if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild); } break; } case NodeType::Child: { ImVec2 size(n.sizeX, n.sizeY); const char *cid = n.id.empty() ? "child" : n.id.c_str(); if (ImGui::BeginChild(cid, size, true)) { if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild); } ImGui::EndChild(); break; } case NodeType::ScrollList: { // Virtualized list: only the rows intersecting the viewport are drawn. std::vector kids; for (int c = n.firstChild; c >= 0; c = tree->nodes[size_t(c)].nextSibling) { if (c >= int(tree->nodes.size())) break; if (tree->nodes[size_t(c)].visible) kids.push_back(c); } const int total = int(kids.size()); const char *cid = n.id.empty() ? "scrolllist" : n.id.c_str(); const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f, n.sizeY > 0.f ? n.sizeY : 120.f); ImGui::BeginChild(cid, size, true); const float itemH = n.itemHeight > 0.f ? n.itemHeight : ImGui::GetFrameHeight(); const float contentW = ImGui::GetContentRegionAvail().x; const float scrollY = ImGui::GetScrollY(); const float viewH = ImGui::GetWindowHeight(); if (total > 0 && itemH > 0.f) { int first = int(scrollY / itemH); if (first < 0) first = 0; if (first > total) first = total; int last = int(std::ceil((scrollY + viewH) / itemH)); if (last < first) last = first; if (last > total) last = total; for (int i = first; i < last; ++i) { UINode &child = tree->nodes[size_t(kids[size_t(i)])]; ImGui::SetCursorPos(ImVec2(0.f, float(i) * itemH)); const float oldX = child.sizeX; const float oldY = child.sizeY; if (contentW > 0.f) child.sizeX = contentW; if (contentW > 0.f) ImGui::PushItemWidth(contentW); walkNode(host, tree, kids[size_t(i)]); child.sizeX = oldX; child.sizeY = oldY; if (contentW > 0.f) ImGui::PopItemWidth(); // Force the row to occupy exactly itemH so rows stay uniform. ImGui::SetCursorPosY(float(i) * itemH + itemH); } } // Establish the full scrollable extent (scrollbar reflects total rows). ImGui::SetCursorPosY(float(total) * itemH); ImGui::EndChild(); break; } case NodeType::Viewport: { const float w = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x; const float h = n.sizeY > 0.f ? n.sizeY : ImGui::GetContentRegionAvail().y; const ImVec2 size(std::max(1.f, w), std::max(1.f, h)); const std::string key = viewportKey(host, n); ViewportState *vs = UISystem::ensureViewport(key, int(size.x), int(size.y)); const ImVec2 rectMin = ImGui::GetCursorScreenPos(); const std::string label = "##vp" + (n.id.empty() ? std::string("viewport") : n.id); ImGui::InvisibleButton(label.c_str(), size); const bool hovered = ImGui::IsItemHovered(); const bool active = ImGui::IsItemActive(); const ImVec2 mouse = ImGui::GetMousePos(); if (vs) { vs->hovered = hovered; vs->active = active; vs->mouseX = mouse.x - rectMin.x; vs->mouseY = mouse.y - rectMin.y; if (active) { vs->dragDX = ImGui::GetIO().MouseDelta.x; vs->dragDY = ImGui::GetIO().MouseDelta.y; } else { vs->dragDX = 0.f; vs->dragDY = 0.f; } vs->wheel = hovered ? ImGui::GetIO().MouseWheel : 0.f; } if (vs && vs->textureId && g_backend) { void *handle = g_backend->textureHandle(vs->textureId); if (handle) { ImGui::SetCursorScreenPos(rectMin); ImGui::Image(static_cast(handle), size); } else { ImGui::GetWindowDrawList()->AddRectFilled( rectMin, ImVec2(rectMin.x + size.x, rectMin.y + size.y), IM_COL32(10, 12, 16, 255)); } } else { ImGui::GetWindowDrawList()->AddRectFilled( rectMin, ImVec2(rectMin.x + size.x, rectMin.y + size.y), IM_COL32(10, 12, 16, 255)); } break; } case NodeType::Flex: walkFlex(host, tree, n); break; case NodeType::Spacer: // Outside Flex: honor explicit size, otherwise a tiny dummy. emitSpacer(n.sizeX > 0.f ? n.sizeX : 0.f, n.sizeY > 0.f ? n.sizeY : 0.f, true); break; } } void walk(UIHost *host, UIHost::Tree *tree, int index) { walkSiblings(host, tree, index); } } // namespace std::vector &UISystem::pendingEvents() { return g_pending; } std::vector &UISystem::clickQueue() { return g_clicks; } std::vector &UISystem::changeQueue() { return g_changes; } void UISystem::setBackend(UIBackend *backend) { g_backend = backend; } UIBackend *UISystem::backend() { return g_backend; } const UIStats &UISystem::stats() { return g_stats; } ViewportState *UISystem::ensureViewport(const std::string &key, int w, int h) { if (key.empty() || w <= 0 || h <= 0) return nullptr; ViewportState &vs = g_viewports[key]; vs.key = key; auto *gfx = eve::ModuleManager::getInstance("Graphics"); if (!gfx) return &vs; if (!vs.canvas || vs.width != w || vs.height != h) { if (vs.textureId && g_backend) g_backend->unregisterTexture(vs.textureId); vs.textureId = 0; vs.canvas = gfx->newCanvas(w, h); vs.width = w; vs.height = h; if (vs.canvas && g_backend) vs.textureId = g_backend->registerTexture(vs.canvas->getTexture()); } return &vs; } ViewportState *UISystem::viewportState(const std::string &hostName, const std::string &nodeId) { auto it = g_viewports.find(hostName + "/" + nodeId); return it == g_viewports.end() ? nullptr : &it->second; } UIHost *UISystem::findHost(const std::string &name) { if (name.empty()) return nullptr; if (ecs::current()->getManager() == nullptr) return nullptr; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [meta, tree] = *it; (void)tree; if (meta->name == name) return meta->entity; } return nullptr; } UIHost *UISystem::findHostByOwner(uint32_t ownerId) { if (ownerId == 0) return nullptr; if (ecs::current()->getManager() == nullptr) return nullptr; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [meta, tree] = *it; (void)tree; if (meta->ownerId == ownerId) return meta->entity; } return nullptr; } void UISystem::render() { if (ecs::current()->getManager() == nullptr) return; applyThemeToImGui(globalTheme()); struct Item { UIHost *host; UIHost::Meta *meta; UIHost::Tree *tree; }; std::vector items; auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [meta, tree] = *it; if (!meta->visible) continue; UIHost *host = meta->entity; if (!host) continue; items.push_back(Item{host, meta, tree}); } std::stable_sort(items.begin(), items.end(), [](const Item &a, const Item &b) { return a.meta->layer < b.meta->layer; }); g_stats.hostCount = int(items.size()); g_stats.nodeCount = 0; g_stats.measureMs = 0.0; g_stats.walkMs = 0.0; for (auto &item : items) { item.tree->dirty = false; // Real measure pass: nested containers, text metrics, margins/min/max. const auto m0 = std::chrono::steady_clock::now(); measureTree(*item.tree); const auto m1 = std::chrono::steady_clock::now(); g_stats.nodeCount += int(item.tree->nodes.size()); if (item.tree->root >= 0) walk(item.host, item.tree, item.tree->root); const auto m2 = std::chrono::steady_clock::now(); g_stats.measureMs += std::chrono::duration(m1 - m0).count(); g_stats.walkMs += std::chrono::duration(m2 - m1).count(); } } void UISystem::dispatchEvents() { auto events = std::move(g_pending); g_pending.clear(); for (auto &ev : events) { if (!ev.host) continue; if (ev.kind == "click" && !ev.nodeId.empty()) { UIClick c; c.hostName = ev.hostName.empty() ? ev.host->meta()->name : ev.hostName; c.nodeId = ev.nodeId; g_clicks.push_back(std::move(c)); } if ((ev.kind == "toggle" || ev.kind == "value" || ev.kind == "text") && !ev.nodeId.empty()) { UIChange ch; ch.hostName = ev.hostName.empty() ? ev.host->meta()->name : ev.hostName; ch.nodeId = ev.nodeId; ch.kind = ev.kind; g_changes.push_back(std::move(ch)); } if (ev.kind == "toggle" && ev.handlerIndex != 0) { auto t = ev.host->tree(); size_t idx = size_t(ev.handlerIndex - 1); if (idx < t->toggleHandlers.size() && t->toggleHandlers[idx]) t->toggleHandlers[idx](ev.toggleValue); continue; } if (ev.kind == "value" && ev.handlerIndex != 0) { auto t = ev.host->tree(); size_t idx = size_t(ev.handlerIndex - 1); if (idx < t->valueHandlers.size() && t->valueHandlers[idx]) t->valueHandlers[idx](ev.floatValue); continue; } if (ev.kind == "text" && ev.handlerIndex != 0) { auto t = ev.host->tree(); size_t idx = size_t(ev.handlerIndex - 1); if (idx < t->textHandlers.size() && t->textHandlers[idx]) t->textHandlers[idx](ev.textValue); continue; } if (ev.handlerIndex == 0) continue; auto t = ev.host->tree(); size_t idx = size_t(ev.handlerIndex - 1); if (idx >= t->clickHandlers.size()) continue; if (t->clickHandlers[idx]) t->clickHandlers[idx](); } } std::string UISystem::consumeClick() { if (g_clicks.empty()) return {}; UIClick c = std::move(g_clicks.front()); g_clicks.erase(g_clicks.begin()); if (c.hostName.empty()) return c.nodeId; return c.hostName + "/" + c.nodeId; } std::string UISystem::consumeClickFor(const std::string &hostName) { for (auto it = g_clicks.begin(); it != g_clicks.end(); ++it) { if (it->hostName != hostName) continue; std::string id = std::move(it->nodeId); g_clicks.erase(it); return id; } return {}; } std::string UISystem::consumeChange() { if (g_changes.empty()) return {}; UIChange c = std::move(g_changes.front()); g_changes.erase(g_changes.begin()); if (c.hostName.empty()) return c.nodeId; return c.hostName + "/" + c.nodeId; } } // namespace eve::ui