mirror of
https://github.com/coah80/LegacyVulkEdition.git
synced 2026-06-17 23:01:52 +00:00
896 lines
27 KiB
C++
896 lines
27 KiB
C++
#include "vui.h"
|
|
#include <cstring>
|
|
#include <algorithm>
|
|
|
|
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<Element> 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<int>(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<uint8_t>(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<uint8_t>(style.color.a * alpha)};
|
|
Color shadow = {style.shadowColor.r, style.shadowColor.g, style.shadowColor.b,
|
|
static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(textCol.a * alpha)};
|
|
|
|
if (style.shadowOffset > 0 && style.shadowColor.a > 0) {
|
|
Color sc = {style.shadowColor.r, style.shadowColor.g, style.shadowColor.b,
|
|
static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<int>(text.size());
|
|
}
|
|
|
|
if (!active) return;
|
|
|
|
int ch;
|
|
while ((ch = r.pollChar()) >= 0) {
|
|
if (ch >= 32 && static_cast<int>(text.size()) < maxLength) {
|
|
text.insert(text.begin() + cursorPos, static_cast<char>(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<int>(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<uint8_t>(bgColor.a * alpha)};
|
|
r.drawRect(0, 0, w, h, bg);
|
|
}
|
|
|
|
r.pushScissor(worldX(), worldY(), w, h);
|
|
|
|
int startIdx = static_cast<int>(scrollOffset / itemHeight);
|
|
if (startIdx < 0) startIdx = 0;
|
|
int endIdx = startIdx + visibleCount + 1;
|
|
if (endIdx > static_cast<int>(items.size())) endIdx = static_cast<int>(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<uint8_t>(selectedColor.a * alpha)};
|
|
r.drawRect(0, iy, w, itemHeight, sc);
|
|
} else if (i == hoveredIndex_) {
|
|
Color hc = {hoverColor.r, hoverColor.g, hoverColor.b,
|
|
static_cast<uint8_t>(hoverColor.a * alpha)};
|
|
r.drawRect(0, iy, w, itemHeight, hc);
|
|
}
|
|
|
|
Color tc = {textColor.r, textColor.g, textColor.b,
|
|
static_cast<uint8_t>(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<int>(localY / itemHeight);
|
|
if (idx >= 0 && idx < static_cast<int>(items.size()))
|
|
hoveredIndex_ = idx;
|
|
|
|
float dy = r.scrollDeltaY();
|
|
if (dy != 0) {
|
|
scrollOffset -= dy * itemHeight * 0.5f;
|
|
float maxScroll = std::max(0.0f, static_cast<float>(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<int>(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<void()> clickFn) {
|
|
auto b = std::make_unique<Button>();
|
|
b->label = lbl;
|
|
b->style = btnStyle;
|
|
b->onClick = std::move(clickFn);
|
|
b->w = btnStyle.width;
|
|
b->h = btnStyle.height;
|
|
auto *ptr = b.get();
|
|
addChild(std::move(b));
|
|
return *ptr;
|
|
}
|
|
|
|
Text &VStack::text(const std::string &str, const TextStyle &textStyle) {
|
|
auto t = std::make_unique<Text>();
|
|
t->text = str;
|
|
t->style = textStyle;
|
|
t->w = 400;
|
|
t->h = 30;
|
|
auto *ptr = t.get();
|
|
addChild(std::move(t));
|
|
return *ptr;
|
|
}
|
|
|
|
Slider &VStack::slider(const SliderStyle &sliderStyle, float min, float max, float val,
|
|
std::function<void(float)> changeFn) {
|
|
auto s = std::make_unique<Slider>();
|
|
s->style = sliderStyle;
|
|
s->minVal = min;
|
|
s->maxVal = max;
|
|
s->value = val;
|
|
s->onChange = std::move(changeFn);
|
|
s->w = sliderStyle.width;
|
|
s->h = sliderStyle.height;
|
|
auto *ptr = s.get();
|
|
addChild(std::move(s));
|
|
return *ptr;
|
|
}
|
|
|
|
Checkbox &VStack::checkbox(const std::string &lbl, const CheckboxStyle &cbStyle, bool isChecked,
|
|
std::function<void(bool)> changeFn) {
|
|
auto c = std::make_unique<Checkbox>();
|
|
c->label = lbl;
|
|
c->style = cbStyle;
|
|
c->checked = isChecked;
|
|
c->onChange = std::move(changeFn);
|
|
c->w = cbStyle.size + 8.0f + 200.0f;
|
|
c->h = cbStyle.size;
|
|
auto *ptr = c.get();
|
|
addChild(std::move(c));
|
|
return *ptr;
|
|
}
|
|
|
|
Image &VStack::image(int tex, float width, float height) {
|
|
auto img = std::make_unique<Image>();
|
|
img->texture = tex;
|
|
img->w = width;
|
|
img->h = height;
|
|
auto *ptr = img.get();
|
|
addChild(std::move(img));
|
|
return *ptr;
|
|
}
|
|
|
|
void HStack::render(Renderer &) {
|
|
float cx = 0;
|
|
for (auto &child : children) {
|
|
child->posX = cx;
|
|
cx += child->w + spacing;
|
|
}
|
|
}
|
|
|
|
Button &HStack::button(const std::string &lbl, const ButtonStyle &btnStyle,
|
|
std::function<void()> clickFn) {
|
|
auto b = std::make_unique<Button>();
|
|
b->label = lbl;
|
|
b->style = btnStyle;
|
|
b->onClick = std::move(clickFn);
|
|
b->w = btnStyle.width;
|
|
b->h = btnStyle.height;
|
|
auto *ptr = b.get();
|
|
addChild(std::move(b));
|
|
return *ptr;
|
|
}
|
|
|
|
Text &HStack::text(const std::string &str, const TextStyle &textStyle) {
|
|
auto t = std::make_unique<Text>();
|
|
t->text = str;
|
|
t->style = textStyle;
|
|
t->w = 400;
|
|
t->h = 30;
|
|
auto *ptr = t.get();
|
|
addChild(std::move(t));
|
|
return *ptr;
|
|
}
|
|
|
|
Image &HStack::image(int tex, float width, float height) {
|
|
auto img = std::make_unique<Image>();
|
|
img->texture = tex;
|
|
img->w = width;
|
|
img->h = height;
|
|
auto *ptr = img.get();
|
|
addChild(std::move(img));
|
|
return *ptr;
|
|
}
|
|
|
|
Scene::Scene() = default;
|
|
Scene::Scene(const std::string &sceneName) : name(sceneName) {}
|
|
|
|
void Scene::render(Renderer &r) {
|
|
root.updateTree(0.016f);
|
|
float s = (float)r.windowH() / baseH;
|
|
float virtualW = (float)r.windowW() / s;
|
|
float offsetX = (virtualW - baseW) * 0.5f;
|
|
r.pushTransform();
|
|
r.scale(s, s);
|
|
r.translate(offsetX, 0);
|
|
root.renderTree(r);
|
|
r.popTransform();
|
|
}
|
|
|
|
static void collectFocusable(Element &el, std::vector<Element *> &out) {
|
|
if (!el.visible) return;
|
|
if (el.focusable) out.push_back(&el);
|
|
for (auto &child : el.children)
|
|
collectFocusable(*child, out);
|
|
}
|
|
|
|
void Scene::handleInput(Renderer &r) {
|
|
if (onBack && r.isKeyClicked(256))
|
|
onBack();
|
|
|
|
std::vector<Element *> focusables;
|
|
collectFocusable(root, focusables);
|
|
|
|
bool upPressed = r.isKeyClicked(265);
|
|
bool downPressed = r.isKeyClicked(264);
|
|
|
|
if ((upPressed || downPressed) && !focusables.empty()) {
|
|
for (auto *el : focusables) el->focused = false;
|
|
|
|
if (focusIndex < 0) {
|
|
focusIndex = downPressed ? 0 : (int)focusables.size() - 1;
|
|
} else {
|
|
if (downPressed) focusIndex++;
|
|
if (upPressed) focusIndex--;
|
|
if (focusIndex < 0) focusIndex = (int)focusables.size() - 1;
|
|
if (focusIndex >= (int)focusables.size()) focusIndex = 0;
|
|
}
|
|
focusables[focusIndex]->focused = true;
|
|
}
|
|
|
|
if (r.isMouseClickedAnywhere()) {
|
|
for (auto *el : focusables) el->focused = false;
|
|
focusIndex = -1;
|
|
}
|
|
|
|
float s = (float)r.windowH() / baseH;
|
|
float virtualW = (float)r.windowW() / s;
|
|
float offsetX = (virtualW - baseW) * 0.5f;
|
|
float origMX = r.mouseX();
|
|
float origMY = r.mouseY();
|
|
r.feedMousePosition(origMX / s + offsetX, origMY / s);
|
|
root.handleInputTree(r);
|
|
r.feedMousePosition(origMX, origMY);
|
|
}
|
|
|
|
Element *Scene::findElement(const std::string &searchName) const {
|
|
return root.findChild(searchName);
|
|
}
|
|
|
|
Image &Scene::background(int tex) {
|
|
auto img = std::make_unique<Image>();
|
|
img->texture = tex;
|
|
img->w = 1280;
|
|
img->h = 720;
|
|
auto *ptr = img.get();
|
|
root.addChild(std::move(img));
|
|
return *ptr;
|
|
}
|
|
|
|
Image &Scene::image(int tex, Rect bounds) {
|
|
auto img = std::make_unique<Image>();
|
|
img->texture = tex;
|
|
img->posX = bounds.x;
|
|
img->posY = bounds.y;
|
|
img->w = bounds.w;
|
|
img->h = bounds.h;
|
|
auto *ptr = img.get();
|
|
root.addChild(std::move(img));
|
|
return *ptr;
|
|
}
|
|
|
|
Text &Scene::text(const std::string &str, Rect bounds, const TextStyle &textStyle) {
|
|
auto t = std::make_unique<Text>();
|
|
t->text = str;
|
|
t->style = textStyle;
|
|
t->posX = bounds.x;
|
|
t->posY = bounds.y;
|
|
t->w = bounds.w;
|
|
t->h = bounds.h;
|
|
auto *ptr = t.get();
|
|
root.addChild(std::move(t));
|
|
return *ptr;
|
|
}
|
|
|
|
Panel &Scene::panel(Rect bounds, const PanelStyle &panelStyle) {
|
|
auto p = std::make_unique<Panel>();
|
|
p->style = panelStyle;
|
|
p->posX = bounds.x;
|
|
p->posY = bounds.y;
|
|
p->w = bounds.w;
|
|
p->h = bounds.h;
|
|
auto *ptr = p.get();
|
|
root.addChild(std::move(p));
|
|
return *ptr;
|
|
}
|
|
|
|
Button &Scene::button(const std::string &lbl, Rect bounds, const ButtonStyle &btnStyle,
|
|
std::function<void()> clickFn) {
|
|
auto b = std::make_unique<Button>();
|
|
b->label = lbl;
|
|
b->style = btnStyle;
|
|
b->onClick = std::move(clickFn);
|
|
b->posX = bounds.x;
|
|
b->posY = bounds.y;
|
|
b->w = bounds.w;
|
|
b->h = bounds.h;
|
|
auto *ptr = b.get();
|
|
root.addChild(std::move(b));
|
|
return *ptr;
|
|
}
|
|
|
|
VStack &Scene::vstack(float x, float y, float sp) {
|
|
auto s = std::make_unique<VStack>();
|
|
s->posX = x;
|
|
s->posY = y;
|
|
s->spacing = sp;
|
|
auto *ptr = s.get();
|
|
root.addChild(std::move(s));
|
|
return *ptr;
|
|
}
|
|
|
|
HStack &Scene::hstack(float x, float y, float sp) {
|
|
auto s = std::make_unique<HStack>();
|
|
s->posX = x;
|
|
s->posY = y;
|
|
s->spacing = sp;
|
|
auto *ptr = s.get();
|
|
root.addChild(std::move(s));
|
|
return *ptr;
|
|
}
|
|
|
|
Slider &Scene::slider(Rect bounds, const SliderStyle &sliderStyle,
|
|
float min, float max, float val,
|
|
std::function<void(float)> changeFn) {
|
|
auto s = std::make_unique<Slider>();
|
|
s->style = sliderStyle;
|
|
s->minVal = min;
|
|
s->maxVal = max;
|
|
s->value = val;
|
|
s->onChange = std::move(changeFn);
|
|
s->posX = bounds.x;
|
|
s->posY = bounds.y;
|
|
s->w = bounds.w;
|
|
s->h = bounds.h;
|
|
auto *ptr = s.get();
|
|
root.addChild(std::move(s));
|
|
return *ptr;
|
|
}
|
|
|
|
Checkbox &Scene::checkbox(const std::string &lbl, float x, float y,
|
|
const CheckboxStyle &cbStyle, bool isChecked,
|
|
std::function<void(bool)> changeFn) {
|
|
auto c = std::make_unique<Checkbox>();
|
|
c->label = lbl;
|
|
c->style = cbStyle;
|
|
c->checked = isChecked;
|
|
c->onChange = std::move(changeFn);
|
|
c->posX = x;
|
|
c->posY = y;
|
|
c->w = cbStyle.size + 8.0f + 200.0f;
|
|
c->h = cbStyle.size;
|
|
auto *ptr = c.get();
|
|
root.addChild(std::move(c));
|
|
return *ptr;
|
|
}
|
|
|
|
TextInput &Scene::textInput(Rect bounds, const TextInputStyle &inputStyle,
|
|
const std::string &placeholderText,
|
|
std::function<void(const std::string &)> changeFn) {
|
|
auto t = std::make_unique<TextInput>();
|
|
t->style = inputStyle;
|
|
t->placeholder = placeholderText;
|
|
t->onChange = std::move(changeFn);
|
|
t->posX = bounds.x;
|
|
t->posY = bounds.y;
|
|
t->w = bounds.w;
|
|
t->h = bounds.h;
|
|
auto *ptr = t.get();
|
|
root.addChild(std::move(t));
|
|
return *ptr;
|
|
}
|
|
|
|
ScrollList &Scene::scrollList(Rect bounds, int visCount, float iHeight) {
|
|
auto s = std::make_unique<ScrollList>();
|
|
s->visibleCount = visCount;
|
|
s->itemHeight = iHeight;
|
|
s->posX = bounds.x;
|
|
s->posY = bounds.y;
|
|
s->w = bounds.w;
|
|
s->h = bounds.h;
|
|
auto *ptr = s.get();
|
|
root.addChild(std::move(s));
|
|
return *ptr;
|
|
}
|
|
|
|
void SceneManager::push(std::unique_ptr<Scene> scene) {
|
|
if (insideHandleInput_)
|
|
pendingPushes_.push_back(std::move(scene));
|
|
else
|
|
stack_.push_back(std::move(scene));
|
|
}
|
|
|
|
void SceneManager::push(Scene &&scene) {
|
|
push(std::make_unique<Scene>(std::move(scene)));
|
|
}
|
|
|
|
std::unique_ptr<Scene> SceneManager::pop() {
|
|
if (insideHandleInput_) {
|
|
pendingPops_++;
|
|
return nullptr;
|
|
}
|
|
if (stack_.empty()) return nullptr;
|
|
auto scene = std::move(stack_.back());
|
|
stack_.pop_back();
|
|
return scene;
|
|
}
|
|
|
|
Scene *SceneManager::top() const {
|
|
if (stack_.empty()) return nullptr;
|
|
return stack_.back().get();
|
|
}
|
|
|
|
bool SceneManager::empty() const { return stack_.empty(); }
|
|
int SceneManager::depth() const { return static_cast<int>(stack_.size()); }
|
|
|
|
void SceneManager::render(Renderer &r) {
|
|
if (!stack_.empty())
|
|
stack_.back()->render(r);
|
|
}
|
|
|
|
void SceneManager::handleInput(Renderer &r) {
|
|
insideHandleInput_ = true;
|
|
if (!stack_.empty())
|
|
stack_.back()->handleInput(r);
|
|
insideHandleInput_ = false;
|
|
flushDeferred();
|
|
}
|
|
|
|
void SceneManager::flushDeferred() {
|
|
while (pendingPops_ > 0 && !stack_.empty()) {
|
|
stack_.pop_back();
|
|
pendingPops_--;
|
|
}
|
|
pendingPops_ = 0;
|
|
for (auto &s : pendingPushes_)
|
|
stack_.push_back(std::move(s));
|
|
pendingPushes_.clear();
|
|
}
|
|
|
|
} // namespace vui
|