#include "vui.h" #include #include namespace vui { Element::Element() = default; Element::~Element() = default; Element::Element(Element &&other) noexcept : posX(other.posX), posY(other.posY), w(other.w), h(other.h), alpha(other.alpha), visible(other.visible), name(std::move(other.name)), parent(other.parent), children(std::move(other.children)), hovered_(other.hovered_) { for (auto &c : children) c->parent = this; other.parent = nullptr; } Element &Element::operator=(Element &&other) noexcept { if (this != &other) { posX = other.posX; posY = other.posY; w = other.w; h = other.h; alpha = other.alpha; visible = other.visible; name = std::move(other.name); parent = other.parent; children = std::move(other.children); hovered_ = other.hovered_; for (auto &c : children) c->parent = this; other.parent = nullptr; } return *this; } Element *Element::addChild(std::unique_ptr child) { child->parent = this; auto *ptr = child.get(); children.push_back(std::move(child)); return ptr; } Element *Element::findChild(const std::string &searchName) const { for (auto &c : children) { if (c->name == searchName) return c.get(); Element *found = c->findChild(searchName); if (found) return found; } return nullptr; } float Element::worldX() const { float wx = posX; if (anchorTarget && anchor != Anchor::None) { float tx = anchorTarget->worldX(); float tw = anchorTarget->w; switch (anchor) { case Anchor::TopLeft: case Anchor::CenterLeft: case Anchor::BottomLeft: wx += tx; break; case Anchor::TopCenter: case Anchor::Center: case Anchor::BottomCenter: wx += tx + tw * 0.5f; break; case Anchor::TopRight: case Anchor::CenterRight: case Anchor::BottomRight: wx += tx + tw; break; default: break; } } else { for (Element *p = parent; p; p = p->parent) wx += p->posX; } return wx; } float Element::worldY() const { float wy = posY; if (anchorTarget && anchor != Anchor::None) { float ty = anchorTarget->worldY(); float th = anchorTarget->h; switch (anchor) { case Anchor::TopLeft: case Anchor::TopCenter: case Anchor::TopRight: wy += ty; break; case Anchor::CenterLeft: case Anchor::Center: case Anchor::CenterRight: wy += ty + th * 0.5f; break; case Anchor::BottomLeft: case Anchor::BottomCenter: case Anchor::BottomRight: wy += ty + th; break; default: break; } } else { for (Element *p = parent; p; p = p->parent) wy += p->posY; } return wy; } bool Element::isPointInside(Renderer &r) const { float wx = worldX(); float wy = worldY(); return r.isMouseOver(wx, wy, w, h); } void Element::updateTree(float dt) { if (onUpdate) onUpdate(*this, dt); for (auto &child : children) child->updateTree(dt); } void Element::renderTree(Renderer &r) { if (!visible) return; r.pushTransform(); r.translate(posX, posY); if (rotation != 0 || scaleX != 1 || scaleY != 1) { r.translate(pivotX, pivotY); if (rotation != 0) r.rotate(rotation); if (scaleX != 1 || scaleY != 1) r.scale(scaleX, scaleY); r.translate(-pivotX, -pivotY); } render(r); for (auto &child : children) child->renderTree(r); r.popTransform(); } void Element::handleInputTree(Renderer &r) { if (!visible) return; for (int i = static_cast(children.size()) - 1; i >= 0; --i) children[i]->handleInputTree(r); handleInput(r); } void Element::render(Renderer &) {} void Element::handleInput(Renderer &) {} void Image::render(Renderer &r) { if (texture < 0) return; Color t = {tint.r, tint.g, tint.b, static_cast(tint.a * alpha)}; float drawX = 0, drawY = 0, drawW = w, drawH = h; if (preserveAspect) { int tw = texW > 0 ? texW : r.textureWidth(texture); int th = texH > 0 ? texH : r.textureHeight(texture); if (tw > 0 && th > 0) { float imgAR = (float)tw / (float)th; float boxAR = w / h; if (imgAR > boxAR) { drawW = w; drawH = w / imgAR; } else { drawH = h; drawW = h * imgAR; } drawX = (w - drawW) * 0.5f; drawY = (h - drawH) * 0.5f; } } if (nineSlice) { int sw = texW > 0 ? texW : r.textureWidth(texture); int sh = texH > 0 ? texH : r.textureHeight(texture); r.drawImage9Slice(texture, drawX, drawY, drawW, drawH, sliceInsets[0], sliceInsets[1], sliceInsets[2], sliceInsets[3], sw, sh, t); } else if (useRegion) { r.drawImageRegion(texture, drawX, drawY, drawW, drawH, u0, v0, u1, v1, t); } else { r.drawImage(texture, drawX, drawY, drawW, drawH, t); } } void Text::render(Renderer &r) { if (text.empty()) return; Color col = {style.color.r, style.color.g, style.color.b, static_cast(style.color.a * alpha)}; Color shadow = {style.shadowColor.r, style.shadowColor.g, style.shadowColor.b, static_cast(style.shadowColor.a * alpha)}; float drawX = 0; if (!wrap) { if (style.align == Alignment::Center) { float tw = r.measureText(text.c_str(), style.font); drawX = (w - tw) * 0.5f; } else if (style.align == Alignment::Right) { float tw = r.measureText(text.c_str(), style.font); drawX = w - tw; } } if (wrap) { if (style.shadowOffset > 0 && shadow.a > 0) r.drawTextWrapped(text.c_str(), drawX + style.shadowOffset, style.shadowOffset, w, shadow, style.font); r.drawTextWrapped(text.c_str(), drawX, 0, w, col, style.font); } else { if (style.shadowOffset > 0 && shadow.a > 0) r.drawText(text.c_str(), drawX + style.shadowOffset, style.shadowOffset, shadow, style.font); r.drawText(text.c_str(), drawX, 0, col, style.font); } } void Panel::render(Renderer &r) { if (style.backgroundTex >= 0) { Color t = {255, 255, 255, static_cast(255 * alpha)}; if (style.nineSlice) { int sw = r.textureWidth(style.backgroundTex); int sh = r.textureHeight(style.backgroundTex); r.drawImage9Slice(style.backgroundTex, 0, 0, w, h, style.sliceInsets[0], style.sliceInsets[1], style.sliceInsets[2], style.sliceInsets[3], sw, sh, t); } else { r.drawImage(style.backgroundTex, 0, 0, w, h, t); } } else if (style.backgroundColor.a > 0) { Color col = {style.backgroundColor.r, style.backgroundColor.g, style.backgroundColor.b, static_cast(style.backgroundColor.a * alpha)}; r.drawRect(0, 0, w, h, col); } } void Panel::renderTree(Renderer &r) { if (!visible) return; r.pushTransform(); r.translate(posX, posY); render(r); r.pushTransform(); r.translate(style.padding, style.padding); for (auto &child : children) child->renderTree(r); r.popTransform(); r.popTransform(); } void Button::render(Renderer &r) { float wx = worldX(); float wy = worldY(); bool down = r.isMouseDown(wx, wy, w, h); int tex = style.normalTex; if (down && style.pressedTex >= 0) tex = style.pressedTex; else if (hovered_ && style.hoverTex >= 0) tex = style.hoverTex; if (tex >= 0) { Color t = {255, 255, 255, static_cast(255 * alpha)}; r.drawImage(tex, 0, 0, w, h, t); } if (!label.empty()) { Color textCol = hovered_ ? style.hoverTextColor : style.textColor; Color col = {textCol.r, textCol.g, textCol.b, static_cast(textCol.a * alpha)}; if (style.shadowOffset > 0 && style.shadowColor.a > 0) { Color sc = {style.shadowColor.r, style.shadowColor.g, style.shadowColor.b, static_cast(style.shadowColor.a * alpha)}; r.drawTextCentered(label.c_str(), style.shadowOffset, style.shadowOffset, w, h, sc, style.font); } r.drawTextCentered(label.c_str(), 0, 0, w, h, col, style.font); } } void Button::handleInput(Renderer &r) { hovered_ = isPointInside(r) || focused; float wx = worldX(); float wy = worldY(); if (r.isMouseClicked(wx, wy, w, h) && onClick) onClick(); if (focused && r.isKeyClicked(257) && onClick) onClick(); } void Slider::render(Renderer &r) { Color t = {255, 255, 255, static_cast(255 * alpha)}; float range = maxVal - minVal; float norm = (range > 0) ? (value - minVal) / range : 0; if (style.bgTex >= 0) r.drawImage(style.bgTex, 0, 0, w, h, t); if (style.trackTex >= 0) r.drawImage(style.trackTex, 0, 0, w, h, t); float fillW = w * norm; if (style.fillTex >= 0 && fillW > 0) r.drawImage(style.fillTex, 0, 0, fillW, h, t); if (style.thumbTex >= 0) { float thumbSize = h; float thumbX = fillW - thumbSize * 0.5f; thumbX = std::max(0.0f, std::min(thumbX, w - thumbSize)); r.drawImage(style.thumbTex, thumbX, 0, thumbSize, thumbSize, t); } if (!label.empty()) { Color shadow = {15, 15, 15, t.a}; float textY = (h - r.fontHeight(style.font)) * 0.5f; r.drawTextCentered(label.c_str(), 1, textY + 1, w, r.fontHeight(style.font), shadow, style.font); r.drawTextCentered(label.c_str(), 0, textY, w, r.fontHeight(style.font), style.textColor, style.font); } } void Slider::handleInput(Renderer &r) { float wx = worldX(); float wy = worldY(); bool over = r.isMouseOver(wx, wy, w, h); if (r.isMouseClickedAnywhere() && over) dragging_ = true; if (!r.isMouseDownAnywhere()) dragging_ = false; if (dragging_) { float localX = r.mouseX() - wx; localX = std::max(0.0f, std::min(w, localX)); float range = maxVal - minVal; float newVal = minVal + (localX / w) * range; newVal = std::max(minVal, std::min(maxVal, newVal)); if (newVal != value) { value = newVal; if (onChange) onChange(value); } } } void Checkbox::render(Renderer &r) { Color t = {255, 255, 255, static_cast(255 * alpha)}; float boxSize = style.size; int bgId = (hovered_ && style.hoverTex >= 0) ? style.hoverTex : style.bgTex; if (bgId >= 0) r.drawImage(bgId, 0, 0, boxSize, boxSize, t); if (checked && style.checkTex >= 0) r.drawImage(style.checkTex, 0, 0, boxSize, boxSize, t); if (!label.empty()) { Color col = {style.textColor.r, style.textColor.g, style.textColor.b, static_cast(style.textColor.a * alpha)}; float textX = boxSize + 8.0f; float textY = (boxSize - r.fontHeight(style.font)) * 0.5f; r.drawText(label.c_str(), textX, textY, col, style.font); } } void Checkbox::handleInput(Renderer &r) { float wx = worldX(); float wy = worldY(); hovered_ = r.isMouseOver(wx, wy, w, h) || focused; if (r.isMouseClicked(wx, wy, w, h) || (focused && r.isKeyClicked(257))) { checked = !checked; if (onChange) onChange(checked); } } void TextInput::render(Renderer &r) { Color t = {255, 255, 255, static_cast(255 * alpha)}; if (style.bgTex >= 0) r.drawImage(style.bgTex, 0, 0, w, h, t); int borderId = active ? style.activeBorderTex : style.borderTex; if (borderId >= 0) r.drawImage(borderId, 0, 0, w, h, t); float textY = (h - r.fontHeight(style.font)) * 0.5f; float pad = 6.0f; r.pushScissor(worldX() + pad, worldY(), w - pad * 2, h); if (text.empty() && !active && !placeholder.empty()) { Color pc = {style.placeholderColor.r, style.placeholderColor.g, style.placeholderColor.b, static_cast(style.placeholderColor.a * alpha)}; r.drawText(placeholder.c_str(), pad, textY, pc, style.font); } else if (!text.empty()) { Color tc = {style.textColor.r, style.textColor.g, style.textColor.b, static_cast(style.textColor.a * alpha)}; r.drawText(text.c_str(), pad, textY, tc, style.font); } if (active) { std::string beforeCursor = text.substr(0, cursorPos); float cursorX = pad + r.measureText(beforeCursor.c_str(), style.font); Color cc = {style.textColor.r, style.textColor.g, style.textColor.b, static_cast(style.textColor.a * alpha)}; r.drawRect(cursorX, textY, 1.5f, r.fontHeight(style.font), cc); } r.popScissor(); } void TextInput::handleInput(Renderer &r) { float wx = worldX(); float wy = worldY(); if (r.isMouseClickedAnywhere()) { active = r.isMouseOver(wx, wy, w, h); if (active) cursorPos = static_cast(text.size()); } if (!active) return; int ch; while ((ch = r.pollChar()) >= 0) { if (ch >= 32 && static_cast(text.size()) < maxLength) { text.insert(text.begin() + cursorPos, static_cast(ch)); cursorPos++; if (onChange) onChange(text); } } if (r.isKeyClicked(259)) { if (cursorPos > 0 && !text.empty()) { text.erase(text.begin() + cursorPos - 1); cursorPos--; if (onChange) onChange(text); } } if (r.isKeyClicked(262)) { if (cursorPos < static_cast(text.size())) cursorPos++; } if (r.isKeyClicked(263)) { if (cursorPos > 0) cursorPos--; } } void ScrollList::render(Renderer &r) { if (bgColor.a > 0) { Color bg = {bgColor.r, bgColor.g, bgColor.b, static_cast(bgColor.a * alpha)}; r.drawRect(0, 0, w, h, bg); } r.pushScissor(worldX(), worldY(), w, h); int startIdx = static_cast(scrollOffset / itemHeight); if (startIdx < 0) startIdx = 0; int endIdx = startIdx + visibleCount + 1; if (endIdx > static_cast(items.size())) endIdx = static_cast(items.size()); for (int i = startIdx; i < endIdx; i++) { float iy = i * itemHeight - scrollOffset; if (i == selectedIndex) { Color sc = {selectedColor.r, selectedColor.g, selectedColor.b, static_cast(selectedColor.a * alpha)}; r.drawRect(0, iy, w, itemHeight, sc); } else if (i == hoveredIndex_) { Color hc = {hoverColor.r, hoverColor.g, hoverColor.b, static_cast(hoverColor.a * alpha)}; r.drawRect(0, iy, w, itemHeight, hc); } Color tc = {textColor.r, textColor.g, textColor.b, static_cast(textColor.a * alpha)}; float textY = iy + (itemHeight - r.fontHeight(font)) * 0.5f; r.drawText(items[i].label.c_str(), 8.0f, textY, tc, font); } r.popScissor(); } void ScrollList::handleInput(Renderer &r) { float wx = worldX(); float wy = worldY(); bool over = r.isMouseOver(wx, wy, w, h); hoveredIndex_ = -1; if (over) { float localY = r.mouseY() - wy + scrollOffset; int idx = static_cast(localY / itemHeight); if (idx >= 0 && idx < static_cast(items.size())) hoveredIndex_ = idx; float dy = r.scrollDeltaY(); if (dy != 0) { scrollOffset -= dy * itemHeight * 0.5f; float maxScroll = std::max(0.0f, static_cast(items.size()) * itemHeight - h); scrollOffset = std::max(0.0f, std::min(maxScroll, scrollOffset)); } } if (over && r.isMouseClickedAnywhere() && hoveredIndex_ >= 0) { selectedIndex = hoveredIndex_; if (onSelect) onSelect(selectedIndex); } if (over && r.isKeyClicked(265) && selectedIndex > 0) { selectedIndex--; if (onSelect) onSelect(selectedIndex); float top = selectedIndex * itemHeight; if (top < scrollOffset) scrollOffset = top; } if (over && r.isKeyClicked(264) && selectedIndex < static_cast(items.size()) - 1) { selectedIndex++; if (onSelect) onSelect(selectedIndex); float bottom = (selectedIndex + 1) * itemHeight; if (bottom > scrollOffset + h) scrollOffset = bottom - h; } } void VStack::render(Renderer &) { float cy = 0; for (auto &child : children) { child->posY = cy; cy += child->h + spacing; } } Button &VStack::button(const std::string &lbl, const ButtonStyle &btnStyle, std::function clickFn) { auto b = std::make_unique