Files
EVEngine/src/modules/ui/UISystem.cpp
T

1564 lines
70 KiB
C++

#include "ui/UISystem.h"
#include "ui/Layout.h"
#include "ui/Icons.h"
#include "ui/Theme.h"
#include "ui/UIBackend.h"
#include "ui/WorldAnchorProjection.h"
#include "common/Module.h"
#include "common/Profile.h"
#include "graphics/Graphics.h"
#include "graphics/ClipSpace.h"
#include "graphics/RenderSystem3D.h"
#include <imgui.h>
#include <imgui_internal.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <map>
#include <vector>
namespace eve::ui {
namespace {
std::vector<UIEvent> g_pending;
std::vector<UIClick> g_clicks;
std::vector<UIChange> g_changes;
std::vector<UIDrop> g_drops;
std::vector<std::string> g_pendingFileDrops;
UIBackend *g_backend = nullptr;
UIStats g_stats;
std::map<std::string, ViewportState> g_viewports;
struct ActiveDragPayload {
uint64_t serial = 0;
std::string sourceHostName;
std::string sourceNodeId;
std::string payloadType;
std::string payloadText;
};
std::optional<ActiveDragPayload> g_activeDrag;
uint64_t g_nextDragSerial = 1;
bool acceptsDropType(const UINode &node, const std::string &type) {
return node.dropTarget && (node.acceptedDropType == "*" || node.acceptedDropType == type);
}
void renderDragDrop(UIHost *host, const UINode &node) {
if (UISystem::dragDropSupport() != DragDropSupport::Supported || !host ||
!node.enabled)
return;
const std::string hostName = host->meta()->name;
if (node.dragSource && !node.dragPayloadType.empty() && ImGui::BeginDragDropSource()) {
if (!g_activeDrag || g_activeDrag->sourceHostName != hostName ||
g_activeDrag->sourceNodeId != node.id ||
g_activeDrag->payloadType != node.dragPayloadType ||
g_activeDrag->payloadText != node.dragPayloadText) {
g_activeDrag = ActiveDragPayload{g_nextDragSerial++, hostName, node.id,
node.dragPayloadType, node.dragPayloadText};
}
const uint64_t serial = g_activeDrag->serial;
ImGui::SetDragDropPayload("EVE_UI_DND", &serial, sizeof(serial), ImGuiCond_Once);
ImGui::TextUnformatted(node.dragPayloadText.empty() ? node.dragPayloadType.c_str()
: node.dragPayloadText.c_str());
ImGui::EndDragDropSource();
}
if (node.dropTarget && ImGui::BeginDragDropTarget()) {
const bool acceptsActive = g_activeDrag &&
acceptsDropType(node, g_activeDrag->payloadType);
if (const ImGuiPayload *payload =
acceptsActive ? ImGui::AcceptDragDropPayload("EVE_UI_DND") : nullptr) {
if (payload->IsDelivery() && payload->DataSize == int(sizeof(uint64_t)) &&
g_activeDrag) {
const uint64_t serial = *static_cast<const uint64_t *>(payload->Data);
if (serial == g_activeDrag->serial) {
g_drops.push_back({DragDropOrigin::Internal,
g_activeDrag->sourceHostName,
g_activeDrag->sourceNodeId,
hostName,
node.id,
g_activeDrag->payloadType,
g_activeDrag->payloadText});
g_activeDrag.reset();
}
}
}
ImGui::EndDragDropTarget();
}
if (!g_pendingFileDrops.empty() && acceptsDropType(node, "file") &&
ImGui::IsItemHovered()) {
for (auto &path : g_pendingFileDrops) {
g_drops.push_back({DragDropOrigin::OperatingSystemFile, "", "", hostName,
node.id, "file", std::move(path)});
}
g_pendingFileDrops.clear();
}
}
graphics::Camera3D *activeCamera() {
if (ecs::current()->getManager<graphics::Camera3D>() == nullptr) return nullptr;
auto cameras = ecs::View<graphics::Camera3D, graphics::Camera3D::Data>();
for (auto it = cameras.begin(); it != cameras.end(); ++it) {
auto [data] = *it;
if (data->active) return data->entity;
}
return nullptr;
}
bool updateWorldAnchor(UIHost &host, graphics::Camera3D *camera, float width, float height) {
auto anchor = host.worldAnchor();
if (!anchor->enabled) {
anchor->state = WorldAnchorState::Disabled;
return true;
}
if (!camera || width <= 0.f || height <= 0.f) {
anchor->state = WorldAnchorState::NoCamera;
return false;
}
const auto data = camera->data();
const glm::vec3 eye(data->eyeX, data->eyeY, data->eyeZ);
const glm::vec3 target(data->targetX, data->targetY, data->targetZ);
const glm::vec3 up(data->upX, data->upY, data->upZ);
const glm::mat4 view = glm::lookAtRH(eye, target, up);
const glm::mat4 projection = graphics::cameraProjectionVulkanRH_ZO(
data->orthographic, glm::radians(data->fovYDeg), data->orthoHeight,
width / height, data->nearZ, data->farZ);
const auto projected = projectWorldAnchor(*anchor, projection * view, data->eyeX,
data->eyeY, data->eyeZ, width, height);
anchor->state = projected.state;
anchor->screenX = projected.screenX;
anchor->screenY = projected.screenY;
anchor->depth = projected.depth;
anchor->scale = projected.scale;
anchor->displacementX = 0.f;
anchor->displacementY = 0.f;
return projected.render;
}
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 = {}) {
if (!n.enabled || n.mouseFilter == MouseFilter::Ignore) return;
UIEvent ev;
if (!host) return;
ev.host = host->handle();
ev.hostName = host ? host->meta()->name : "";
ev.nodeId = n.id;
if (host) {
auto tree = host->tree();
if (!tree->nodes.empty()) ev.nodeIndex = int(&n - tree->nodes.data());
}
ev.kind = kind;
ev.handlerIndex = handlerIndex;
ev.toggleValue = toggleValue;
ev.floatValue = floatValue;
ev.textValue = std::move(textValue);
g_pending.push_back(std::move(ev));
}
std::string nodeLabel(const UINode &node, const char *fallback) {
std::string label = node.text.empty() ? fallback : node.text;
if (!node.id.empty()) label += "###" + node.id;
return label;
}
bool privateUseGlyph(const std::string &text, ImWchar *out) {
if (text.size() != 3) return false;
const auto b0 = static_cast<unsigned char>(text[0]);
const auto b1 = static_cast<unsigned char>(text[1]);
const auto b2 = static_cast<unsigned char>(text[2]);
if ((b0 & 0xf0u) != 0xe0u || (b1 & 0xc0u) != 0x80u || (b2 & 0xc0u) != 0x80u)
return false;
const unsigned int codepoint =
((b0 & 0x0fu) << 12u) | ((b1 & 0x3fu) << 6u) | (b2 & 0x3fu);
if (codepoint < 0xe000u || codepoint > 0xf8ffu) return false;
if (out) *out = static_cast<ImWchar>(codepoint);
return true;
}
bool isInteractive(NodeType type) {
switch (type) {
case NodeType::Button:
case NodeType::Checkbox:
case NodeType::Slider:
case NodeType::InputText:
case NodeType::ImageButton:
case NodeType::Combo:
case NodeType::Viewport:
case NodeType::SearchField:
case NodeType::Switch:
case NodeType::MenuItem: return true;
default: return false;
}
}
bool keyPressed(ImGuiKey key) {
const int index = ImGui::GetKeyIndex(key);
return index >= 0 && ImGui::IsKeyPressed(index, false);
}
void routeExplicitFocusInput(UIHost *host, const UINode &node) {
if (!host || !node.focused || node.focusMode != FocusMode::All) return;
if (keyPressed(ImGuiKey_Tab)) {
const bool backwards = ImGui::GetIO().KeyShift;
const std::string &target = backwards ? node.focusPrevious : node.focusNext;
if (!target.empty())
host->moveFocus(backwards ? FocusDirection::Previous : FocusDirection::Next);
return;
}
if (ImGui::IsItemActive()) return;
if (!node.focusLeft.empty() && keyPressed(ImGuiKey_LeftArrow))
host->moveFocus(FocusDirection::Left);
else if (!node.focusRight.empty() && keyPressed(ImGuiKey_RightArrow))
host->moveFocus(FocusDirection::Right);
else if (!node.focusUp.empty() && keyPressed(ImGuiKey_UpArrow))
host->moveFocus(FocusDirection::Up);
else if (!node.focusDown.empty() && keyPressed(ImGuiKey_DownArrow))
host->moveFocus(FocusDirection::Down);
}
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;
UINode &node = tree->nodes[size_t(index)];
// Desktop composition behaves like CSS grid rows: menu/toolbar and
// status bar keep their intrinsic height while the workspace consumes
// the remaining block size. An explicit split height still wins.
if (node.type == NodeType::SplitPane && node.sizeY <= 0.f && next >= 0 &&
next < int(tree->nodes.size()) &&
tree->nodes[size_t(next)].type == NodeType::StatusBar) {
const float oldY = node.sizeY;
const float reserve = tree->nodes[size_t(next)].measuredH +
ImGui::GetStyle().ItemSpacing.y;
node.sizeY = std::max(1.f, ImGui::GetContentRegionAvail().y - reserve);
walkNode(host, tree, index);
node.sizeY = oldY;
} else {
walkNode(host, tree, index);
}
index = next;
}
}
void walkPaneChild(UIHost *host, UIHost::Tree *tree, int index) {
if (index < 0 || index >= int(tree->nodes.size())) return;
UINode &child = tree->nodes[size_t(index)];
const ImVec2 avail = ImGui::GetContentRegionAvail();
const float oldX = child.sizeX;
const float oldY = child.sizeY;
// A direct SplitPane child fills its allocated pane. Descendants can still
// specialize their own dimensions through normal widget layout props.
child.sizeX = std::max(1.f, avail.x);
child.sizeY = std::max(1.f, avail.y);
child.measuredW = child.sizeX;
child.measuredH = child.sizeY;
walkNode(host, tree, index);
child.sizeX = oldX;
child.sizeY = oldY;
}
bool hasDirectChildType(const UIHost::Tree &tree, const UINode &parent, NodeType type) {
for (int child = parent.firstChild; child >= 0;
child = tree.nodes[size_t(child)].nextSibling) {
if (child >= int(tree.nodes.size())) break;
if (tree.nodes[size_t(child)].type == type) return true;
}
return false;
}
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<ImTextureID>(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);
}
/** Queue the same nine-patch geometry for backends without per-command texture handles. */
void queueNinePatch(uint64_t textureId, const ImVec2 &pos, const ImVec2 &size,
const ImVec2 &uv0, const ImVec2 &uv1, float bL, float bT, float bR,
float bB, int texW, int texH, float r, float g, float b, float a,
bool opaque = false) {
if (!g_backend) return;
const auto draw = [&](float x0, float y0, float x1, float y1, float u0, float v0,
float u1, float v1) {
if (x1 <= x0 || y1 <= y0) return;
g_backend->queueTextureDraw(textureId, pos.x + x0, pos.y + y0, x1 - x0, y1 - y0,
u0, v0, u1, v1, r, g, b, a, opaque);
};
if ((bL <= 0.f && bT <= 0.f && bR <= 0.f && bB <= 0.f) || texW <= 0 || texH <= 0) {
draw(0.f, 0.f, size.x, size.y, uv0.x, uv0.y, uv1.x, uv1.y);
return;
}
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);
const float uPerPx = (uv1.x - uv0.x) / float(texW);
const float vPerPx = (uv1.y - uv0.y) / float(texH);
const float uL = uv0.x + bl * uPerPx;
const float uR = uv1.x - br * uPerPx;
const float vT = uv0.y + bt * vPerPx;
const float vB = uv1.y - bb * vPerPx;
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);
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);
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 bool fillsContainer =
flex.type == NodeType::Toolbar || flex.type == NodeType::StatusBar;
const float flexW = flex.sizeX > 0.f ? flex.sizeX : avail.x;
const float flexH =
flex.sizeY > 0.f ? flex.sizeY : (fillsContainer ? avail.y : flex.measuredH);
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<int> kids;
std::vector<int> absKids;
std::vector<FlexItemSpec> 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();
ImGui::Dummy(ImVec2(std::max(0.f, flexW), std::max(0.f, flexH)));
const ImVec2 flowEnd = 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();
}
ImGui::SetCursorScreenPos(flowEnd);
}
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;
const bool scopedTheme = n.themePreset != ThemePreset::Inherit;
ImGuiStyle previousStyle;
float previousFontScale = 1.f;
ImGuiConfigFlags previousConfigFlags = ImGuiConfigFlags_None;
if (scopedTheme) {
previousStyle = ImGui::GetStyle();
previousFontScale = ImGui::GetIO().FontGlobalScale;
previousConfigFlags = ImGui::GetIO().ConfigFlags;
applyThemeToImGui(n.themePreset == ThemePreset::Dark ? Theme::dark() : Theme::light());
}
const bool interactive = isInteractive(n.type);
const bool disableItem = interactive && !n.enabled;
const bool disableNavigation = interactive && n.focusMode != FocusMode::All;
if (disableItem) {
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.6f);
}
if (disableNavigation) ImGui::PushItemFlag(ImGuiItemFlags_NoNav, true);
if (interactive && n.focusRequested && n.enabled && n.focusMode != FocusMode::None)
ImGui::SetKeyboardFocusHere();
switch (n.type) {
case NodeType::Window: {
const std::string visibleTitle = n.text.empty() ? "Window" : n.text;
std::string title = visibleTitle;
// Keep the host name in ImGui's stable ID without exposing internal
// paths such as "inventory/Inspector" in the visible title bar.
if (host && !host->meta()->name.empty())
title += "###" + host->meta()->name + "/" + visibleTitle;
const bool modal = host && host->meta()->modal;
ImGuiWindowFlags flags = 0;
if (hasDirectChildType(*tree, n, NodeType::MenuBar)) flags |= ImGuiWindowFlags_MenuBar;
const bool desktopChrome = hasDirectChildType(*tree, n, NodeType::SplitPane) &&
(hasDirectChildType(*tree, n, NodeType::Toolbar) ||
hasDirectChildType(*tree, n, NodeType::StatusBar));
if (desktopChrome)
flags |= ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
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;
const auto worldAnchor = host ? host->worldAnchor() : nullptr;
if (worldAnchor->enabled) {
ImGui::SetNextWindowPos(ImVec2(worldAnchor->screenX, worldAnchor->screenY),
ImGuiCond_Always,
ImVec2(host->meta()->pivotX, host->meta()->pivotY));
flags |= ImGuiWindowFlags_NoMove;
if (!host->meta()->hasSize && winW <= 0.f && winH <= 0.f)
flags |= ImGuiWindowFlags_AlwaysAutoResize;
} else if (host && host->meta()->hasPos) {
const ImVec2 display = ImGui::GetIO().DisplaySize;
// SetNextWindowPos applies the pivot itself. Passing an already
// pivot-adjusted top-left position shifts the window by its pivot
// a second time (a centered window moves half its width left).
const float x = host->meta()->anchorX * display.x + host->meta()->posX;
const float y = host->meta()->anchorY * display.y + host->meta()->posY;
ImGui::SetNextWindowPos(ImVec2(x, y),
host->meta()->lockPos ? ImGuiCond_Always
: ImGuiCond_FirstUseEver,
ImVec2(host->meta()->pivotX, host->meta()->pivotY));
if (host->meta()->lockPos) 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),
host && host->meta()->lockSize ? ImGuiCond_Always : ImGuiCond_FirstUseEver);
} 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;
if (!host->meta()->hasSize && host->meta()->percentW <= 0.f &&
host->meta()->percentH <= 0.f)
flags |= ImGuiWindowFlags_AlwaysAutoResize;
ImGui::SetNextWindowBgAlpha(host->meta()->overlayBgAlpha);
}
const bool flushOverlay = host && host->meta()->overlay && host->meta()->overlayFlush;
if (flushOverlay) ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
if (modal) {
ImGui::OpenPopup(title.c_str());
if (ImGui::BeginPopupModal(title.c_str(), nullptr,
ImGuiWindowFlags_AlwaysAutoResize | flags)) {
if (worldAnchor->enabled)
ImGui::SetWindowFontScale(worldAnchor->scale);
if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
ImGui::EndPopup();
}
} else {
ImGui::Begin(title.c_str(), nullptr, flags);
if (worldAnchor->enabled)
ImGui::SetWindowFontScale(worldAnchor->scale);
if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
ImGui::End();
}
if (flushOverlay) ImGui::PopStyleVar();
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: {
ImWchar iconCodepoint = 0;
const bool iconOnly = privateUseGlyph(n.text, &iconCodepoint);
const std::string label =
iconOnly ? "##iconButton" + (n.id.empty() ? std::string("button") : n.id)
: nodeLabel(n, "Button");
const bool sized = n.sizeX > 0.f || n.sizeY > 0.f;
const float defaultSide = ImGui::GetFrameHeight();
const ImVec2 buttonSize(iconOnly ? (n.sizeX > 0.f ? n.sizeX : defaultSide)
: (n.sizeX > 0.f ? n.sizeX : 0.f),
iconOnly ? (n.sizeY > 0.f ? n.sizeY : defaultSide)
: (n.sizeY > 0.f ? n.sizeY : 0.f));
const bool clicked = (sized || iconOnly) ? ImGui::Button(label.c_str(), buttonSize)
: ImGui::Button(label.c_str());
if (iconOnly) {
const ImVec2 rectMin = ImGui::GetItemRectMin();
const ImVec2 rectMax = ImGui::GetItemRectMax();
ImFont *font = ImGui::GetFont();
const ImFontGlyph *glyph = font ? font->FindGlyph(iconCodepoint) : nullptr;
if (glyph) {
const float fontSize = ImGui::GetFontSize();
const float fontScale = fontSize / font->FontSize;
const float glyphW = (glyph->X1 - glyph->X0) * fontScale;
const float glyphH = (glyph->Y1 - glyph->Y0) * fontScale;
const ImVec2 glyphOrigin(
(rectMin.x + rectMax.x - glyphW) * 0.5f - glyph->X0 * fontScale,
(rectMin.y + rectMax.y - glyphH) * 0.5f - glyph->Y0 * fontScale);
ImGui::GetWindowDrawList()->AddText(font, fontSize, glyphOrigin,
ImGui::GetColorU32(ImGuiCol_Text),
n.text.c_str());
}
}
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) {
int tw = 0, th = 0;
g_backend->textureSize(n.textureId, &tw, &th);
if (g_backend->usesQueuedTextureDraws()) {
queueNinePatch(n.textureId, pos, size, ImVec2(n.uv0x, n.uv0y),
ImVec2(n.uv1x, n.uv1y), n.borderL, n.borderT, n.borderR,
n.borderB, tw, th, n.tintR, n.tintG, n.tintB, n.tintA);
} else {
void *handle = g_backend->textureHandle(n.textureId);
if (handle) {
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);
}
}
} 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;
bool itemCreated = false;
if (n.textureId != 0 && g_backend) {
if (g_backend->usesQueuedTextureDraws()) {
clicked = ImGui::InvisibleButton(sid.c_str(), size);
itemCreated = true;
const ImVec2 pmin = ImGui::GetItemRectMin();
const float hover = ImGui::IsItemHovered() ? 1.12f : 1.f;
g_backend->queueTextureDraw(n.textureId, pmin.x, pmin.y, size.x, size.y,
n.uv0x, n.uv0y, n.uv1x, n.uv1y,
std::min(1.f, n.tintR * hover),
std::min(1.f, n.tintG * hover),
std::min(1.f, n.tintB * hover), n.tintA, false);
} else {
void *handle = g_backend->textureHandle(n.textureId);
if (handle) {
clicked = ImGui::ImageButton(
static_cast<ImTextureID>(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));
itemCreated = true;
}
}
}
if (!itemCreated) {
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<std::string> 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<const char *> 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::SearchField: {
char buf[1024];
std::memset(buf, 0, sizeof(buf));
if (!n.valueText.empty()) std::strncpy(buf, n.valueText.c_str(), sizeof(buf) - 1);
const std::string label = "##search" + (n.id.empty() ? std::string() : "###" + n.id);
const ImGuiStyle &style = ImGui::GetStyle();
const std::string hint = n.text.empty() ? "Search" : n.text;
const char *glyph = iconGlyph(Icon::Search);
const ImVec2 iconSize = ImGui::CalcTextSize(glyph);
const float gap = globalTheme().layout.searchIconGap * themeUiScale();
const float width = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
n.measuredW = std::max(1.f, width);
ImGui::PushItemWidth(std::max(1.f, width));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
ImVec2(style.FramePadding.x + iconSize.x + gap,
style.FramePadding.y));
if (ImGui::InputTextWithHint(label.c_str(), hint.c_str(), buf, sizeof(buf))) {
n.valueText = buf;
pushPending(host, n, "text", n.handlerText, false, 0.f, n.valueText);
}
ImGui::PopStyleVar();
ImGui::PopItemWidth();
const ImVec2 rectMin = ImGui::GetItemRectMin();
const ImVec2 rectMax = ImGui::GetItemRectMax();
ImGui::GetWindowDrawList()->AddText(
ImVec2(rectMin.x + style.FramePadding.x,
rectMin.y + (rectMax.y - rectMin.y - iconSize.y) * 0.5f),
ImGui::GetColorU32(ImGuiCol_TextDisabled), glyph);
n.measuredH = rectMax.y - rectMin.y;
break;
}
case NodeType::Switch: {
const float height = ImGui::GetFrameHeight();
const float trackH = std::max(16.f, height * 0.68f);
const float trackW = height * 1.7f;
const float radius = trackH * 0.5f;
const std::string id = "##switch" + (n.id.empty() ? std::string() : "###" + n.id);
ImGui::BeginGroup();
const ImVec2 pos = ImGui::GetCursorScreenPos();
if (ImGui::InvisibleButton(id.c_str(), ImVec2(trackW, height))) {
n.checked = !n.checked;
pushPending(host, n, "toggle", n.handlerToggle, n.checked);
}
const bool hovered = ImGui::IsItemHovered();
const ImU32 bg = ImGui::GetColorU32(
n.checked ? ImGuiCol_CheckMark
: (hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg));
const float y = pos.y + (height - trackH) * 0.5f;
ImDrawList *draw = ImGui::GetWindowDrawList();
draw->AddRectFilled(ImVec2(pos.x, y), ImVec2(pos.x + trackW, y + trackH), bg, radius);
const float knobR = radius - 2.f;
const float knobX = n.checked ? pos.x + trackW - radius : pos.x + radius;
draw->AddCircleFilled(ImVec2(knobX, y + radius), knobR,
ImGui::GetColorU32(ImGuiCol_Text));
if (!n.text.empty()) {
ImGui::SameLine(0.f, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(n.text.c_str());
}
ImGui::EndGroup();
break;
}
case NodeType::Badge: {
const ImVec2 textSize = ImGui::CalcTextSize(n.text.c_str());
const ImVec2 padding(ImGui::GetStyle().FramePadding.x, 3.f);
const ImVec2 pos = ImGui::GetCursorScreenPos();
const ImVec2 size(textSize.x + padding.x * 2.f, textSize.y + padding.y * 2.f);
ImDrawList *draw = ImGui::GetWindowDrawList();
const ImU32 bg = ImGui::ColorConvertFloat4ToU32(
ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
const ImU32 border = ImGui::ColorConvertFloat4ToU32(
ImVec4(n.tintR, n.tintG, n.tintB, std::min(1.f, n.tintA + 0.35f)));
draw->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), bg, size.y * 0.5f);
draw->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), border, size.y * 0.5f);
draw->AddText(ImVec2(pos.x + padding.x, pos.y + padding.y),
ImGui::GetColorU32(ImGuiCol_Text), n.text.c_str());
ImGui::Dummy(size);
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::Card: {
const ThemeLayout &layout = globalTheme().layout;
const ImVec2 padding(layout.cardPaddingX * themeUiScale(),
layout.cardPaddingY * themeUiScale());
const float height = n.sizeY > 0.f
? n.sizeY
: std::max(ImGui::GetFrameHeight(),
n.measuredH + padding.y * 2.f);
const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f, height);
const std::string id = n.id.empty() ? "card" : n.id;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg));
if (ImGui::BeginChild(id.c_str(), size, true, ImGuiWindowFlags_AlwaysUseWindowPadding)) {
if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
break;
}
case NodeType::NinePatchPanel: {
const float width = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
const float height = n.sizeY > 0.f
? n.sizeY
: std::max(n.minSizeY,
n.measuredH > 0.f ? n.measuredH
: ImGui::GetFrameHeight());
const ImVec2 pos = ImGui::GetCursorScreenPos();
const ImVec2 size(std::max(n.minSizeX, width), height);
if (n.textureId != 0 && g_backend) {
int tw = 0, th = 0;
g_backend->textureSize(n.textureId, &tw, &th);
if (g_backend->usesQueuedTextureDraws()) {
queueNinePatch(n.textureId, pos, size, ImVec2(n.uv0x, n.uv0y),
ImVec2(n.uv1x, n.uv1y), n.borderL, n.borderT, n.borderR,
n.borderB, tw, th, n.tintR, n.tintG, n.tintB, n.tintA);
} else if (void *handle = g_backend->textureHandle(n.textureId)) {
const ImU32 tint = ImGui::ColorConvertFloat4ToU32(
ImVec4(n.tintR, n.tintG, n.tintB, n.tintA));
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);
}
}
const std::string id = n.id.empty() ? "ninePatchPanel" : n.id;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
if (ImGui::BeginChild(id.c_str(), size, false,
ImGuiWindowFlags_AlwaysUseWindowPadding)) {
ImGui::SetCursorPos(ImVec2(n.paddingL, n.paddingT));
const ImVec2 contentSize(std::max(1.f, size.x - n.paddingL - n.paddingR),
std::max(1.f, size.y - n.paddingT - n.paddingB));
const std::string contentId = id + "##content";
if (ImGui::BeginChild(contentId.c_str(), contentSize, false)) {
if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
}
ImGui::EndChild();
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
break;
}
case NodeType::SectionHeader: {
const float sectionGap = globalTheme().layout.sectionSpacingY * themeUiScale();
const ImVec2 pos = ImGui::GetCursorScreenPos();
const float height = ImGui::GetFrameHeight();
const float totalHeight = sectionGap + height;
const float width = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
const float headerY = pos.y + sectionGap;
ImDrawList *draw = ImGui::GetWindowDrawList();
draw->AddRectFilled(ImVec2(pos.x, headerY + 4.f),
ImVec2(pos.x + 3.f, headerY + height - 4.f),
ImGui::GetColorU32(ImGuiCol_CheckMark), 1.f);
draw->AddText(ImVec2(pos.x + 10.f,
headerY + (height - ImGui::GetFontSize()) * 0.5f),
ImGui::GetColorU32(ImGuiCol_Text), n.text.c_str());
const float separatorY = pos.y + totalHeight - 1.f;
draw->AddLine(ImVec2(pos.x, separatorY), ImVec2(pos.x + width, separatorY),
ImGui::GetColorU32(ImGuiCol_Separator));
ImGui::Dummy(ImVec2(width, totalHeight));
break;
}
case NodeType::MenuBar:
if (ImGui::BeginMenuBar()) {
if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
ImGui::EndMenuBar();
}
break;
case NodeType::Menu: {
const std::string label = nodeLabel(n, "Menu");
if (ImGui::BeginMenu(label.c_str())) {
if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
ImGui::EndMenu();
}
break;
}
case NodeType::MenuItem: {
const std::string label = nodeLabel(n, "Command");
const char *shortcut = n.valueText.empty() ? nullptr : n.valueText.c_str();
if (ImGui::MenuItem(label.c_str(), shortcut, n.checked, true))
pushPending(host, n, "click", n.handlerClick);
break;
}
case NodeType::Toolbar:
case NodeType::StatusBar: {
const bool status = n.type == NodeType::StatusBar;
const ThemeLayout &layout = globalTheme().layout;
const float scale = themeUiScale();
const float defaultHeight =
(status ? layout.statusBarHeight : layout.toolbarHeight) * scale;
const float height = n.sizeY > 0.f ? n.sizeY : defaultHeight;
const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f, height);
const std::string id = n.id.empty() ? (status ? "statusbar" : "toolbar") : n.id;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
ImVec2(layout.barPaddingX * scale, layout.barPaddingY * scale));
ImGui::PushStyleColor(ImGuiCol_ChildBg,
ImGui::GetStyleColorVec4(status ? ImGuiCol_WindowBg
: ImGuiCol_MenuBarBg));
if (ImGui::BeginChild(id.c_str(), size, false,
ImGuiWindowFlags_AlwaysUseWindowPadding |
ImGuiWindowFlags_NoScrollbar)) {
walkFlex(host, tree, n);
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
break;
}
case NodeType::Toolbox: {
const ThemeLayout &layout = globalTheme().layout;
const float scale = themeUiScale();
const float cell = n.itemHeight > 0.f ? n.itemHeight : layout.toolboxCellSize * scale;
const ImVec2 padding(layout.panelPaddingX * scale, layout.panelPaddingY * scale);
const float outerWidth = n.sizeX > 0.f ? n.sizeX : ImGui::GetContentRegionAvail().x;
const float contentWidth = std::max(1.f, outerWidth - padding.x * 2.f);
const float spacing = ImGui::GetStyle().ItemSpacing.x;
const int capacity = std::max(1, int((contentWidth + spacing) / (cell + spacing)));
const int requested = int(n.value) > 0 ? int(n.value) : capacity;
const int columns = std::max(1, std::min(requested, capacity));
int count = 0;
for (int childIndex = n.firstChild; childIndex >= 0;
childIndex = tree->nodes[size_t(childIndex)].nextSibling) {
if (tree->nodes[size_t(childIndex)].visible) ++count;
}
const int rows = (count + columns - 1) / columns;
const float autoHeight = std::max(
cell, float(rows) * cell + float(std::max(0, rows - 1)) *
ImGui::GetStyle().ItemSpacing.y + padding.y * 2.f);
const ImVec2 size(n.sizeX > 0.f ? n.sizeX : 0.f, n.sizeY > 0.f ? n.sizeY : autoHeight);
const std::string id = n.id.empty() ? "toolbox" : n.id;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg));
if (ImGui::BeginChild(id.c_str(), size, false, ImGuiWindowFlags_AlwaysUseWindowPadding)) {
int visibleIndex = 0;
for (int childIndex = n.firstChild; childIndex >= 0;
childIndex = tree->nodes[size_t(childIndex)].nextSibling) {
UINode &child = tree->nodes[size_t(childIndex)];
if (!child.visible) continue;
const float oldX = child.sizeX;
const float oldY = child.sizeY;
child.sizeX = cell;
child.sizeY = cell;
walkNode(host, tree, childIndex);
child.sizeX = oldX;
child.sizeY = oldY;
++visibleIndex;
if (visibleIndex % columns != 0) ImGui::SameLine();
}
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
break;
}
case NodeType::Sidebar: {
const ThemeLayout &layout = globalTheme().layout;
const float scale = themeUiScale();
const ImVec2 size(n.sizeX > 0.f ? n.sizeX : layout.sidebarWidth * scale,
n.sizeY > 0.f ? n.sizeY : 0.f);
const std::string id = n.id.empty() ? "sidebar" : n.id;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
ImVec2(layout.panelPaddingX * scale, layout.panelPaddingY * scale));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg));
if (ImGui::BeginChild(id.c_str(), size, true, ImGuiWindowFlags_AlwaysUseWindowPadding)) {
if (n.firstChild >= 0) walkSiblings(host, tree, n.firstChild);
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
break;
}
case NodeType::SplitPane: {
const bool row = n.flexDirection == FlexDirection::Row;
const ImVec2 avail = ImGui::GetContentRegionAvail();
const ImVec2 size(n.sizeX > 0.f ? n.sizeX : avail.x, n.sizeY > 0.f ? n.sizeY : avail.y);
n.measuredW = size.x;
n.measuredH = size.y;
const ThemeLayout &layout = globalTheme().layout;
const float splitter = layout.splitterSize * themeUiScale();
const float mainSize = std::max(1.f, (row ? size.x : size.y) - splitter);
const float minPane = std::min(layout.minPaneSize * themeUiScale(), mainSize * 0.5f);
const float ratioLo = std::max(n.minValue, minPane / mainSize);
const float ratioHi = std::min(n.maxValue, 1.f - minPane / mainSize);
const std::string id = n.id.empty() ? "splitpane" : n.id;
ImGui::PushID(id.c_str());
const ImGuiID ratioStateId = ImGui::GetID("ratio");
const float storedRatio = ImGui::GetStateStorage()->GetFloat(ratioStateId, n.value);
const float ratio = std::max(ratioLo, std::min(ratioHi, storedRatio));
ImGui::GetStateStorage()->SetFloat(ratioStateId, ratio);
n.value = ratio;
const float firstMain = std::floor(mainSize * ratio);
const float secondMain = std::max(1.f, mainSize - firstMain);
const int first = n.firstChild;
const int second = first >= 0 ? tree->nodes[size_t(first)].nextSibling : -1;
ImGui::BeginGroup();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
if (first >= 0) {
const ImVec2 paneSize(row ? firstMain : size.x, row ? size.y : firstMain);
if (ImGui::BeginChild("first", paneSize, false,
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse))
walkPaneChild(host, tree, first);
ImGui::EndChild();
}
if (row) ImGui::SameLine(0.f, 0.f);
const ImVec2 handleSize(row ? splitter : size.x, row ? size.y : splitter);
ImGui::InvisibleButton("splitter", handleSize);
const bool hovered = ImGui::IsItemHovered();
const bool active = ImGui::IsItemActive();
if (hovered || active)
ImGui::SetMouseCursor(row ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS);
ImGui::GetWindowDrawList()->AddRectFilled(
ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
ImGui::GetColorU32(active ? ImGuiCol_CheckMark
: (hovered ? ImGuiCol_SeparatorHovered
: ImGuiCol_Separator)));
if (active) {
const float delta = row ? ImGui::GetIO().MouseDelta.x : ImGui::GetIO().MouseDelta.y;
const float next = std::max(ratioLo, std::min(ratioHi, ratio + delta / mainSize));
if (next != ratio) {
n.value = next;
ImGui::GetStateStorage()->SetFloat(ratioStateId, next);
pushPending(host, n, "value", n.handlerValue, false, next);
}
}
if (row) ImGui::SameLine(0.f, 0.f);
if (second >= 0) {
const ImVec2 paneSize(row ? secondMain : size.x, row ? size.y : secondMain);
if (ImGui::BeginChild("second", paneSize, false,
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse))
walkPaneChild(host, tree, second);
ImGui::EndChild();
}
ImGui::PopStyleVar();
ImGui::EndGroup();
ImGui::PopID();
break;
}
case NodeType::ScrollList: {
// Virtualized list: only the rows intersecting the viewport are drawn.
std::vector<int> 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);
auto viewport = 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 (viewport.ok()) {
ViewportState &vs = viewport.value().get();
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.textureId && g_backend && g_backend->usesQueuedTextureDraws()) {
g_backend->queueTextureDraw(vs.textureId, rectMin.x, rectMin.y, size.x, size.y, 0.f, 0.f, 1.f, 1.f, 1.f,
1.f, 1.f, 1.f, true);
} else if (vs.textureId && g_backend) {
void *handle = g_backend->textureHandle(vs.textureId);
if (handle) {
ImGui::SetCursorScreenPos(rectMin);
ImGui::Image(static_cast<ImTextureID>(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));
}
} 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;
}
if (interactive) {
n.focused = ImGui::IsItemFocused();
routeExplicitFocusInput(host, n);
n.focusRequested = false;
}
if (disableNavigation) ImGui::PopItemFlag();
if (disableItem) {
ImGui::PopStyleVar();
ImGui::PopItemFlag();
}
renderDragDrop(host, n);
if (!n.tooltip.empty() && ImGui::IsItemHovered(ImGuiHoveredFlags_None))
ImGui::SetTooltip("%s", n.tooltip.c_str());
if (scopedTheme) {
ImGui::GetStyle() = previousStyle;
ImGui::GetIO().FontGlobalScale = previousFontScale;
ImGui::GetIO().ConfigFlags = previousConfigFlags;
}
}
void walk(UIHost *host, UIHost::Tree *tree, int index) { walkSiblings(host, tree, index); }
} // namespace
std::vector<UIEvent> &UISystem::pendingEvents() { return g_pending; }
std::vector<UIClick> &UISystem::clickQueue() { return g_clicks; }
std::vector<UIChange> &UISystem::changeQueue() { return g_changes; }
std::vector<UIDrop> &UISystem::dropQueue() { return g_drops; }
DragDropSupport UISystem::dragDropSupport() noexcept {
#if (defined(_WIN32) || defined(__APPLE__) || defined(__linux__)) && \
!defined(__ANDROID__) && !defined(__EMSCRIPTEN__)
return DragDropSupport::Supported;
#else
return DragDropSupport::UnsupportedPlatform;
#endif
}
void UISystem::enqueuePlatformFileDrop(const std::string &path) {
if (dragDropSupport() == DragDropSupport::Supported && !path.empty())
g_pendingFileDrops.push_back(path);
}
std::optional<UIDrop> UISystem::consumeDrop() {
if (g_drops.empty()) return std::nullopt;
UIDrop result = std::move(g_drops.front());
g_drops.erase(g_drops.begin());
return result;
}
void UISystem::setBackend(UIBackend &backend) { g_backend = &backend; }
void UISystem::clearBackend() noexcept { g_backend = nullptr; }
std::optional<std::reference_wrapper<UIBackend>> UISystem::backend() {
if (!g_backend) return std::nullopt;
return std::ref(*g_backend);
}
const UIStats &UISystem::stats() { return g_stats; }
eve::Result<std::reference_wrapper<ViewportState>> UISystem::ensureViewport(const std::string &key, int w, int h) {
using Return = eve::Result<std::reference_wrapper<ViewportState>>;
if (key.empty() || w <= 0 || h <= 0) {
return Return::failure(eve::Status::failure(
eve::StatusCode::Rejected,
eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "viewport key and dimensions must be valid")));
}
auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>("Graphics");
if (!gfx) {
return Return::failure(eve::Status::failure(
eve::StatusCode::Unsupported,
eve::Diagnostic::error(eve::DiagnosticCode::Unsupported, "viewport requires the Graphics module")));
}
ViewportState &vs = g_viewports[key];
vs.key = key;
if (!vs.canvas || vs.width != w || vs.height != h) {
graphics::Canvas *canvas = gfx->newCanvas(w, h);
if (!canvas) {
return Return::failure(eve::Status::failure(
eve::StatusCode::Failed,
eve::Diagnostic::error(eve::DiagnosticCode::Failed, "Graphics could not create the viewport canvas")));
}
if (vs.textureId && g_backend) g_backend->unregisterTexture(vs.textureId);
vs.textureId = 0;
vs.canvas = canvas;
vs.width = w;
vs.height = h;
if (vs.canvas && g_backend)
vs.textureId = g_backend->registerTexture(vs.canvas->getTexture());
}
return Return::success(std::ref(vs));
}
std::optional<std::reference_wrapper<ViewportState>> UISystem::viewportState(const std::string &hostName,
const std::string &nodeId) {
auto it = g_viewports.find(hostName + "/" + nodeId);
if (it == g_viewports.end()) return std::nullopt;
return std::ref(it->second);
}
UIHostHandle UISystem::findHost(const std::string &name) {
if (name.empty()) return {};
if (ecs::current()->getManager<UIHost>() == nullptr) return {};
auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [meta, tree] = *it;
(void)tree;
if (meta->name == name && UIHost::resolve(meta->entity)) return meta->entity;
}
return {};
}
UIHostHandle UISystem::findHostByOwner(uint32_t ownerId) {
if (ownerId == 0) return {};
if (ecs::current()->getManager<UIHost>() == nullptr) return {};
auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [meta, tree] = *it;
(void)tree;
if (meta->ownerId == ownerId && UIHost::resolve(meta->entity)) return meta->entity;
}
return {};
}
void UISystem::render() {
EV_PROFILE_MODULE("ui", "UISystem::render");
if (ecs::current()->getManager<UIHost>() == nullptr) return;
applyThemeToImGui(globalTheme());
struct Item {
UIHost *host;
UIHost::Meta *meta;
UIHost::Tree *tree;
bool render = true;
};
std::vector<Item> items;
graphics::Camera3D *camera = activeCamera();
const ImVec2 display = ImGui::GetIO().DisplaySize;
auto view = ecs::View<UIHost, UIHost::Meta, UIHost::Tree>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [meta, tree] = *it;
if (!meta->visible) continue;
auto host = UIHost::resolve(meta->entity);
if (!host) continue;
if (!updateWorldAnchor(host->get(), camera, display.x, display.y)) continue;
items.push_back(Item{&host->get(), meta, tree, true});
}
std::stable_sort(items.begin(), items.end(),
[](const Item &a, const Item &b) { return a.meta->layer < b.meta->layer; });
g_stats.nodeCount = 0;
g_stats.measureMs = 0.0;
g_stats.walkMs = 0.0;
for (auto &item : items) {
item.tree->dirty = false;
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());
g_stats.measureMs +=
std::chrono::duration<double, std::milli>(m1 - m0).count();
}
std::vector<WorldAnchorLayoutItem> anchorItems;
anchorItems.reserve(items.size());
const ImGuiStyle &style = ImGui::GetStyle();
for (std::size_t i = 0; i < items.size(); ++i) {
auto &item = items[i];
auto anchor = item.host->worldAnchor();
if (!anchor->enabled) continue;
float width = item.meta->hasSize ? item.meta->sizeX : 0.f;
float height = item.meta->hasSize ? item.meta->sizeY : 0.f;
if (item.meta->percentW > 0.f) width = item.meta->percentW * display.x;
if (item.meta->percentH > 0.f) height = item.meta->percentH * display.y;
if (item.tree->root >= 0) {
const auto &root = item.tree->nodes[std::size_t(item.tree->root)];
if (width <= 0.f) width = root.measuredW + style.WindowPadding.x * 2.f;
if (height <= 0.f) height = root.measuredH + style.WindowPadding.y * 2.f;
}
width *= anchor->scale;
height *= anchor->scale;
anchorItems.push_back({i,
anchor->screenX,
anchor->screenY,
width,
height,
item.meta->pivotX,
item.meta->pivotY,
anchor->depth,
anchor->safeMargin,
anchor->overlapPadding,
anchor->maxDisplacement,
anchor->overlapPriority,
anchor->overlapPolicy == WorldAnchorOverlapPolicy::Avoid});
}
for (const auto &resolved :
resolveWorldAnchorOverlaps(std::move(anchorItems), display.x, display.y)) {
auto &item = items[resolved.stableIndex];
auto anchor = item.host->worldAnchor();
anchor->screenX = resolved.screenX;
anchor->screenY = resolved.screenY;
anchor->displacementX = resolved.displacementX;
anchor->displacementY = resolved.displacementY;
item.render = resolved.render;
if (!resolved.render) anchor->state = WorldAnchorState::Crowded;
}
g_stats.hostCount = int(std::count_if(items.begin(), items.end(),
[](const Item &item) { return item.render; }));
for (auto &item : items) {
if (!item.render) continue;
const auto w0 = std::chrono::steady_clock::now();
if (item.tree->root >= 0) walk(item.host, item.tree, item.tree->root);
const auto w1 = std::chrono::steady_clock::now();
g_stats.walkMs += std::chrono::duration<double, std::milli>(w1 - w0).count();
}
}
void UISystem::dispatchEvents() {
auto events = std::move(g_pending);
g_pending.clear();
for (auto &ev : events) {
auto resolvedHost = UIHost::resolve(ev.host);
if (!resolvedHost) continue;
UIHost *host = &resolvedHost->get();
if (ev.kind == "click" && !ev.nodeId.empty()) {
UIClick c;
c.hostName = ev.hostName.empty() ? 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() ? host->meta()->name : ev.hostName;
ch.nodeId = ev.nodeId;
ch.kind = ev.kind;
g_changes.push_back(std::move(ch));
}
if (ev.kind == "click") {
auto tree = host->tree();
int index = ev.nodeIndex;
if (index < 0 || index >= int(tree->nodes.size()) ||
tree->nodes[size_t(index)].id != ev.nodeId) {
auto target = host->findById(ev.nodeId);
index = target && !tree->nodes.empty() ? int(&target->get() - tree->nodes.data()) : -1;
}
bool first = true;
while (index >= 0 && index < int(tree->nodes.size())) {
const UINode &node = tree->nodes[size_t(index)];
if (!node.visible || !node.enabled) break;
const uint32_t handler = first ? ev.handlerIndex : node.handlerClick;
if (node.mouseFilter != MouseFilter::Ignore && handler != 0) {
const size_t handlerSlot = size_t(handler - 1);
if (handlerSlot < tree->clickHandlers.size() &&
tree->clickHandlers[handlerSlot])
tree->clickHandlers[handlerSlot]();
}
if (node.mouseFilter == MouseFilter::Stop) break;
index = node.parent;
first = false;
}
continue;
}
if (ev.kind == "toggle" && ev.handlerIndex != 0) {
auto t = 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 = 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 = host->tree();
size_t idx = size_t(ev.handlerIndex - 1);
if (idx < t->textHandlers.size() && t->textHandlers[idx])
t->textHandlers[idx](ev.textValue);
continue;
}
}
}
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