VEX Engine
Retro looking game engine made in vulkan mostly because i wanted to understand it better.
Loading...
Searching...
No Matches
VexUI.cpp
1#define STB_TRUETYPE_IMPLEMENTATION
2
3#include "glm/ext/matrix_clip_space.hpp"
4#include <climits>
7#include <glm/glm.hpp>
8#include <immintrin.h>
9
10#include <components/ErrorUtils.hpp>
11#include <components/PathUtils.hpp>
12#include "components/HardwareInfo.hpp"
13
14namespace vex {
15
16void calculateLayout(LayoutNode* node, float availableW, float availableH, bool isRoot, const VexUI* ui, float forcedW, float forcedH) {
17 if (!node) return;
18
19 float pl = node->paddingLeft.resolveOr(availableW, 0.f);
20 float pr = node->paddingRight.resolveOr(availableW, 0.f);
21 float pt = node->paddingTop.resolveOr(availableH, 0.f);
22 float pb = node->paddingBottom.resolveOr(availableH, 0.f);
23 float bw = node->borderWidth.resolveOr(availableW, 0.f);
24 float padX = pl + pr + bw * 2.f;
25 float padY = pt + pb + bw * 2.f;
26
27 float explicitW = !std::isnan(forcedW) ? forcedW : node->width.resolve(availableW);
28 float explicitH = !std::isnan(forcedH) ? forcedH : node->height.resolve(availableH);
29
30 if (isRoot) {
31 explicitW = availableW;
32 explicitH = availableH;
33 }
34
35 float innerAvailW = std::isnan(explicitW) ? NAN : std::max(0.0f, explicitW - padX);
36 float innerAvailH = std::isnan(explicitH) ? NAN : std::max(0.0f, explicitH - padY);
37
38 if (node->measureFunc) {
39 float maxW = std::isnan(innerAvailW) ? FLT_MAX : innerAvailW;
40 float maxH = std::isnan(innerAvailH) ? FLT_MAX : innerAvailH;
41
42 node->measureFunc(node, maxW, maxH);
43
44 float measuredW = node->computedWidth + padX;
45 float measuredH = node->computedHeight + padY;
46
47 node->computedWidth = std::isnan(explicitW) ? measuredW : explicitW;
48 node->computedHeight = std::isnan(explicitH) ? measuredH : explicitH;
49 return;
50 }
51
52 std::vector<LayoutNode*> flow;
53 for (auto* c : node->children) {
54 if (c->positionType == PositionType::Absolute) continue;
55 flow.push_back(c);
56 }
57
58 float totalMain = 0.f;
59 float maxCross = 0.f;
60 float totalGrow = 0.f;
61 float totalShrink = 0.f;
62
63 for (auto* c : flow) {
64 calculateLayout(c, innerAvailW, innerAvailH, false, ui);
65
66 float cml = c->marginLeft.resolveOr(innerAvailW, 0.f);
67 float cmr = c->marginRight.resolveOr(innerAvailW, 0.f);
68 float cmt = c->marginTop.resolveOr(innerAvailH, 0.f);
69 float cmb = c->marginBottom.resolveOr(innerAvailH, 0.f);
70
71 float outerW = c->computedWidth + cml + cmr;
72 float outerH = c->computedHeight + cmt + cmb;
73
74 if (node->flexDirection == FlexDirection::Row) {
75 totalMain += outerW;
76 maxCross = std::max(maxCross, outerH);
77 } else {
78 totalMain += outerH;
79 maxCross = std::max(maxCross, outerW);
80 }
81 totalGrow += c->flexGrow;
82 totalShrink += c->flexShrink;
83 }
84
85 node->computedWidth = !std::isnan(explicitW) ? explicitW : (node->flexDirection == FlexDirection::Row ? totalMain : maxCross) + padX;
86 node->computedHeight = !std::isnan(explicitH) ? explicitH : (node->flexDirection == FlexDirection::Column ? totalMain : maxCross) + padY;
87
88 float innerW = std::max(0.0f, node->computedWidth - padX);
89 float innerH = std::max(0.0f, node->computedHeight - padY);
90 float freeMain = (node->flexDirection == FlexDirection::Row ? innerW : innerH) - totalMain;
91
92 bool needsRelayout = false;
93
94 for (auto* c : flow) {
95 float childForcedW = NAN;
96 float childForcedH = NAN;
97 bool changed = false;
98
99 if (freeMain > 0.f && totalGrow > 0.f && c->flexGrow > 0.f) {
100 float extra = freeMain * (c->flexGrow / totalGrow);
101 if (node->flexDirection == FlexDirection::Row) childForcedW = c->computedWidth + extra;
102 else childForcedH = c->computedHeight + extra;
103 changed = true;
104 } else if (freeMain < 0.f && totalShrink > 0.f && c->flexShrink > 0.f) {
105 float shrink = (-freeMain) * (c->flexShrink / totalShrink);
106 if (node->flexDirection == FlexDirection::Row) childForcedW = std::max(0.0f, c->computedWidth - shrink);
107 else childForcedH = std::max(0.0f, c->computedHeight - shrink);
108 changed = true;
109 }
110
111 Align al = c->alignSelf == Align::Auto ? node->alignItems : c->alignSelf;
112 if (al == Align::Stretch) {
113 float cml = c->marginLeft.resolveOr(innerW, 0.f);
114 float cmr = c->marginRight.resolveOr(innerW, 0.f);
115 float cmt = c->marginTop.resolveOr(innerH, 0.f);
116 float cmb = c->marginBottom.resolveOr(innerH, 0.f);
117
118 if (node->flexDirection == FlexDirection::Row && std::isnan(c->height.resolve(innerH))) {
119 childForcedH = std::max(0.0f, innerH - cmt - cmb);
120 changed = true;
121 } else if (node->flexDirection == FlexDirection::Column && std::isnan(c->width.resolve(innerW))) {
122 childForcedW = std::max(0.0f, innerW - cml - cmr);
123 changed = true;
124 }
125 }
126
127 if (changed) {
128 calculateLayout(c, innerW, innerH, false, ui, childForcedW, childForcedH);
129 needsRelayout = true;
130 }
131 }
132
133 if (needsRelayout) {
134 totalMain = 0.f;
135 for (auto* c : flow) {
136 float cml = c->marginLeft.resolveOr(innerW, 0.f);
137 float cmr = c->marginRight.resolveOr(innerW, 0.f);
138 float cmt = c->marginTop.resolveOr(innerH, 0.f);
139 float cmb = c->marginBottom.resolveOr(innerH, 0.f);
140 totalMain += (node->flexDirection == FlexDirection::Row) ? (c->computedWidth + cml + cmr) : (c->computedHeight + cmt + cmb);
141 }
142 freeMain = (node->flexDirection == FlexDirection::Row ? innerW : innerH) - totalMain;
143 }
144
145 float mainPos = node->flexDirection == FlexDirection::Row ? (pl + bw) : (pt + bw);
146 float crossStart = node->flexDirection == FlexDirection::Row ? (pt + bw) : (pl + bw);
147 float space = 0.f;
148
149 if (freeMain > 0.f && totalGrow == 0.f) {
150 if (node->justifyContent == Justify::Center) mainPos += freeMain / 2.0f;
151 else if (node->justifyContent == Justify::FlexEnd) mainPos += freeMain;
152 else if (node->justifyContent == Justify::SpaceBetween && flow.size() > 1) {
153 space = freeMain / (flow.size() - 1);
154 }
155 }
156
157 for (auto* c : flow) {
158 float cml = c->marginLeft.resolveOr(innerW, 0.f);
159 float cmr = c->marginRight.resolveOr(innerW, 0.f);
160 float cmt = c->marginTop.resolveOr(innerH, 0.f);
161 float cmb = c->marginBottom.resolveOr(innerH, 0.f);
162
163 float cMain = node->flexDirection == FlexDirection::Row ? (c->computedWidth + cml + cmr) : (c->computedHeight + cmt + cmb);
164 float cCross = node->flexDirection == FlexDirection::Row ? (c->computedHeight + cmt + cmb) : (c->computedWidth + cml + cmr);
165 float pCross = node->flexDirection == FlexDirection::Row ? innerH : innerW;
166
167 float crossOff = 0.f;
168 Align al = c->alignSelf == Align::Auto ? node->alignItems : c->alignSelf;
169 if (al == Align::Baseline) al = Align::FlexStart;
170
171 if (al == Align::Center) crossOff = (pCross - cCross) / 2.0f;
172 else if (al == Align::FlexEnd) crossOff = pCross - cCross;
173
174 if (node->flexDirection == FlexDirection::Row) {
175 c->computedLeft = mainPos + cml;
176 c->computedTop = crossStart + crossOff + cmt;
177 } else {
178 c->computedTop = mainPos + cmt;
179 c->computedLeft = crossStart + crossOff + cml;
180 }
181 mainPos += cMain + space;
182 }
183
184 for (auto* c : node->children) {
185 if (c->positionType != PositionType::Absolute) continue;
186
187 calculateLayout(c, innerW, innerH, false, ui);
188
189 float cml = c->marginLeft.resolveOr(innerW, 0.f);
190 float cmr = c->marginRight.resolveOr(innerW, 0.f);
191 float cmt = c->marginTop.resolveOr(innerH, 0.f);
192 float cmb = c->marginBottom.resolveOr(innerH, 0.f);
193
194 float l = c->left.resolve(innerW);
195 float r = c->right.resolve(innerW);
196 float t = c->top.resolve(innerH);
197 float b = c->bottom.resolve(innerH);
198
199 if (!std::isnan(l)) c->computedLeft = bw + l + cml;
200 else if (!std::isnan(r)) c->computedLeft = node->computedWidth - bw - c->computedWidth - r - cmr;
201 else c->computedLeft = bw + cml;
202
203 if (!std::isnan(t)) c->computedTop = bw + t + cmt;
204 else if (!std::isnan(b)) c->computedTop = node->computedHeight - bw - c->computedHeight - b - cmb;
205 else c->computedTop = bw + cmt;
206 }
207}
208
209std::vector<std::string> VexUI::wrapText(const std::string& text, const FontAtlas& a, float maxWidth) {
210 std::vector<std::string> lines;
211 std::string currentLine;
212 float currentWidth = 0.f;
213 size_t lastSpacePos = std::string::npos;
214
215 for (char ch : text) {
216 if (ch == '\n') {
217 lines.push_back(currentLine);
218 currentLine.clear();
219 currentWidth = 0.f;
220 lastSpacePos = std::string::npos;
221 continue;
222 }
223 if (ch < 32 || ch > 127) continue;
224
225 const stbtt_bakedchar& cd = a.cdata[ch - 32];
226 float advance = cd.xadvance;
227
228 if (currentWidth + advance > maxWidth && !currentLine.empty()) {
229 if (ch == ' ') {
230 lines.push_back(currentLine);
231 currentLine.clear();
232 currentWidth = 0.f;
233 lastSpacePos = std::string::npos;
234 } else if (lastSpacePos != std::string::npos) {
235 lines.push_back(currentLine.substr(0, lastSpacePos));
236 std::string remainder = currentLine.substr(lastSpacePos + 1);
237 currentLine = remainder + ch;
238 currentWidth = 0.f;
239 for (char r : currentLine) currentWidth += a.cdata[r - 32].xadvance;
240 lastSpacePos = std::string::npos;
241 } else {
242 lines.push_back(currentLine);
243 currentLine = std::string(1, ch);
244 currentWidth = advance;
245 lastSpacePos = std::string::npos;
246 }
247 } else {
248 if (ch == ' ') lastSpacePos = currentLine.length();
249 currentLine += ch;
250 currentWidth += advance;
251 }
252 }
253 if (!currentLine.empty()) lines.push_back(currentLine);
254 return lines;
255}
256
257static void rotateQuadScalar(float* outVerts, float pivotX, float pivotY, float sinA, float cosA, float x0, float y0, float x1, float y1) {
258 float px[] = {x0, x1, x0, x1};
259 float py[] = {y0, y0, y1, y1};
260
261 for(int i=0; i<4; ++i) {
262 float dx = px[i] - pivotX;
263 float dy = py[i] - pivotY;
264 outVerts[i*2+0] = pivotX + (dx * cosA - dy * sinA);
265 outVerts[i*2+1] = pivotY + (dx * sinA + dy * cosA);
266 }
267}
268
269__attribute__((target("avx2")))
270static void rotateQuadAvX2(float* outVerts, float pivotX, float pivotY, float sinA, float cosA, float x0, float y0, float x1, float y1) {
271 __m256 vPos = _mm256_setr_ps(x0, y0, x1, y0, x0, y1, x1, y1);
272 __m256 vPivot = _mm256_setr_ps(pivotX, pivotY, pivotX, pivotY, pivotX, pivotY, pivotX, pivotY);
273 __m256 vDelta = _mm256_sub_ps(vPos, vPivot);
274 __m256 vCos = _mm256_set1_ps(cosA);
275 __m256 vSin = _mm256_set1_ps(sinA);
276 __m256 vDeltaSwapped = _mm256_permute_ps(vDelta, 0xB1);
277 __m256 t1 = _mm256_mul_ps(vDelta, vCos);
278 __m256 t2 = _mm256_mul_ps(vDeltaSwapped, vSin);
279 __m256 vResult = _mm256_addsub_ps(t1, t2);
280 vResult = _mm256_add_ps(vResult, vPivot);
281 _mm256_storeu_ps(outVerts, vResult);
282}
283
284void UIUnitValue::parse(const nlohmann::json& jval) {
285 if (jval.is_number()) {
286 value = jval.get<float>();
287 type = UIUnitType::Psp;
288 } else if (jval.is_string()) {
289 std::string s = jval.get<std::string>();
290 if (s == "auto") { value = 0.f; type = UIUnitType::Auto; return; }
291 size_t pos = 0;
292 try { value = std::stof(s, &pos); } catch(...) { value = 0.f; type = UIUnitType::Auto; return; }
293 if (pos >= s.length()) { type = UIUnitType::Psp; return; }
294 std::string unit = s.substr(pos);
295 unit.erase(0, unit.find_first_not_of(" \t"));
296 unit.erase(unit.find_last_not_of(" \t") + 1);
297
298 if (unit == "px") type = UIUnitType::Px;
299 else if (unit == "%") type = UIUnitType::Percent;
300 else if (unit == "vw") type = UIUnitType::Vw;
301 else if (unit == "vh") type = UIUnitType::Vh;
302 else type = UIUnitType::Psp;
303 }
304}
305
306nlohmann::json UIUnitValue::toJson() const {
307 if (type == UIUnitType::Auto) return "auto";
308 if (type == UIUnitType::Psp) return value;
309 std::string s = std::to_string(value);
310 s.erase(s.find_last_not_of('0') + 1, std::string::npos);
311 if (s.back() == '.') s.pop_back();
312
313 if (type == UIUnitType::Px) return s + "px";
314 if (type == UIUnitType::Percent) return s + "%";
315 if (type == UIUnitType::Vw) return s + "vw";
316 if (type == UIUnitType::Vh) return s + "vh";
317 return value;
318}
319
320float UIUnitValue::getPixels(const VexUI* ui) const {
321 if (!ui) return value;
322 switch (type) {
323 case UIUnitType::Px: return value;
324 case UIUnitType::Psp: return value * ui->getPspMultiplier();
325 case UIUnitType::Vw: return value * (ui->getRenderResolution().x / 100.0f);
326 case UIUnitType::Vh: return value * (ui->getRenderResolution().y / 100.0f);
327 case UIUnitType::Percent: return value;
328 case UIUnitType::Auto: return NAN;
329 }
330 return NAN;
331}
332
333Widget::~Widget() {
334 if (layoutNode) delete layoutNode;
335}
336
337void Widget::applyLayout(VexUI* uiManager) {
338 if (!uiManager || !layoutNode) return;
339
340 auto mapUnit = [&](const UIUnitValue& val) -> FlexValue {
341 if (val.type == UIUnitType::Auto) return {NAN, false};
342 if (val.type == UIUnitType::Percent) return {val.value, true};
343 return {val.getPixels(uiManager), false};
344 };
345
346 if (nodeJson.contains("type")) {
347 std::string t = nodeJson["type"].get<std::string>();
348 if (t == "label") type = WidgetType::Label;
349 else if (t == "image") type = WidgetType::Image;
350 else if (t == "button") type = WidgetType::Button;
351 }
352
353 if (nodeJson.contains("id")) id = nodeJson["id"].get<std::string>();
354 if (nodeJson.contains("text")) text = nodeJson["text"].get<std::string>();
355 if (nodeJson.contains("image")) image = nodeJson["image"].get<std::string>();
356
357 if (nodeJson.contains("size") && nodeJson["size"].is_array() && nodeJson["size"].size() == 2) {
358 size.x = UIUnitValue::parseJson(nodeJson["size"][0]);
359 size.y = UIUnitValue::parseJson(nodeJson["size"][1]);
360 layoutNode->width = mapUnit(size.x);
361 layoutNode->height = mapUnit(size.y);
362 }
363
364 if (nodeJson.contains("style")) {
365 const auto& s = nodeJson["style"];
366 if (s.contains("color") && s["color"].is_array() && s["color"].size() == 4) {
367 style.color = glm::vec4(s["color"][0].get<float>(), s["color"][1].get<float>(), s["color"][2].get<float>(), s["color"][3].get<float>());
368 }
369 if (s.contains("bgColor") && s["bgColor"].is_array() && s["bgColor"].size() == 4) {
370 style.bgColor = glm::vec4(s["bgColor"][0].get<float>(), s["bgColor"][1].get<float>(), s["bgColor"][2].get<float>(), s["bgColor"][3].get<float>());
371 }
372 if (s.contains("font")) style.font = s["font"].get<std::string>();
373 if (s.contains("size")) style.fontSize = UIUnitValue::parseJson(s["size"]);
374 }
375
376 if (nodeJson.contains("layout")) {
377 std::string l = nodeJson["layout"].get<std::string>();
378 if (l == "row") layoutNode->flexDirection = FlexDirection::Row;
379 else if (l == "column") layoutNode->flexDirection = FlexDirection::Column;
380 }
381
382 if (nodeJson.contains("justify")) {
383 std::string jst = nodeJson["justify"].get<std::string>();
384 if (jst == "space-between") layoutNode->justifyContent = Justify::SpaceBetween;
385 else if (jst == "center") layoutNode->justifyContent = Justify::Center;
386 else if (jst == "flex-end") layoutNode->justifyContent = Justify::FlexEnd;
387 else if (jst == "flex-start") layoutNode->justifyContent = Justify::FlexStart;
388 }
389
390 if (nodeJson.contains("align")) {
391 std::string al = nodeJson["align"].get<std::string>();
392 if (al == "baseline") layoutNode->alignItems = Align::Baseline;
393 else if (al == "center") layoutNode->alignItems = Align::Center;
394 else if (al == "flex-start") layoutNode->alignItems = Align::FlexStart;
395 else if (al == "flex-end") layoutNode->alignItems = Align::FlexEnd;
396 else if (al == "stretch") layoutNode->alignItems = Align::Stretch;
397 }
398
399 if (nodeJson.contains("rotation")) rotation = nodeJson["rotation"].get<float>();
400 if (nodeJson.contains("flexGrow")) layoutNode->flexGrow = nodeJson["flexGrow"].get<float>();
401 if (nodeJson.contains("flexShrink")) layoutNode->flexShrink = nodeJson["flexShrink"].get<float>();
402
403 if (nodeJson.contains("position")) {
404 std::string pos = nodeJson["position"].get<std::string>();
405 if (pos == "absolute") {
406 layoutNode->positionType = PositionType::Absolute;
407 if (nodeJson.contains("left")) layoutNode->left = mapUnit(UIUnitValue::parseJson(nodeJson["left"]));
408 if (nodeJson.contains("right")) layoutNode->right = mapUnit(UIUnitValue::parseJson(nodeJson["right"]));
409 if (nodeJson.contains("top")) layoutNode->top = mapUnit(UIUnitValue::parseJson(nodeJson["top"]));
410 if (nodeJson.contains("bottom")) layoutNode->bottom = mapUnit(UIUnitValue::parseJson(nodeJson["bottom"]));
411 } else if (pos == "relative") {
412 layoutNode->positionType = PositionType::Relative;
413 }
414 }
415
416 if (nodeJson.contains("padding")) {
417 auto p = mapUnit(UIUnitValue::parseJson(nodeJson["padding"]));
418 layoutNode->paddingLeft = layoutNode->paddingRight = layoutNode->paddingTop = layoutNode->paddingBottom = p;
419 } else if (nodeJson.contains("pading")) {
420 auto p = mapUnit(UIUnitValue::parseJson(nodeJson["pading"]));
421 layoutNode->paddingLeft = layoutNode->paddingRight = layoutNode->paddingTop = layoutNode->paddingBottom = p;
422 }
423
424 if (nodeJson.contains("paddingLeft")) layoutNode->paddingLeft = mapUnit(UIUnitValue::parseJson(nodeJson["paddingLeft"]));
425 if (nodeJson.contains("paddingRight")) layoutNode->paddingRight = mapUnit(UIUnitValue::parseJson(nodeJson["paddingRight"]));
426 if (nodeJson.contains("paddingTop")) layoutNode->paddingTop = mapUnit(UIUnitValue::parseJson(nodeJson["paddingTop"]));
427 if (nodeJson.contains("paddingBottom")) layoutNode->paddingBottom = mapUnit(UIUnitValue::parseJson(nodeJson["paddingBottom"]));
428
429 if (nodeJson.contains("margin")) {
430 auto m = mapUnit(UIUnitValue::parseJson(nodeJson["margin"]));
431 layoutNode->marginLeft = layoutNode->marginRight = layoutNode->marginTop = layoutNode->marginBottom = m;
432 }
433 if (nodeJson.contains("marginLeft")) layoutNode->marginLeft = mapUnit(UIUnitValue::parseJson(nodeJson["marginLeft"]));
434 if (nodeJson.contains("marginRight")) layoutNode->marginRight = mapUnit(UIUnitValue::parseJson(nodeJson["marginRight"]));
435 if (nodeJson.contains("marginTop")) layoutNode->marginTop = mapUnit(UIUnitValue::parseJson(nodeJson["marginTop"]));
436 if (nodeJson.contains("marginBottom")) layoutNode->marginBottom = mapUnit(UIUnitValue::parseJson(nodeJson["marginBottom"]));
437
438 if (nodeJson.contains("borderWidth")) {
439 style.borderWidth = UIUnitValue::parseJson(nodeJson["borderWidth"]);
440 layoutNode->borderWidth = mapUnit(style.borderWidth);
441 }
442 if (nodeJson.contains("borderColor") && nodeJson["borderColor"].is_array() && nodeJson["borderColor"].size() == 4) {
443 style.borderColor = glm::vec4(nodeJson["borderColor"][0].get<float>(), nodeJson["borderColor"][1].get<float>(), nodeJson["borderColor"][2].get<float>(), nodeJson["borderColor"][3].get<float>());
444 }
445
446 if (nodeJson.contains("textAlign")) {
447 std::string ta = nodeJson["textAlign"].get<std::string>();
448 if (ta == "center") textAlign = TextAlign::Center;
449 else if (ta == "right") textAlign = TextAlign::Right;
450 }
451
452 if (nodeJson.contains("alignSelf")) {
453 std::string as = nodeJson["alignSelf"].get<std::string>();
454 if (as == "baseline") layoutNode->alignSelf = Align::Baseline;
455 else if (as == "center") layoutNode->alignSelf = Align::Center;
456 else if (as == "flex-start") layoutNode->alignSelf = Align::FlexStart;
457 else if (as == "flex-end") layoutNode->alignSelf = Align::FlexEnd;
458 else if (as == "stretch") layoutNode->alignSelf = Align::Stretch;
459 }
460}
461
463 : m_ctx(ctx), m_vfs(vfs), m_res(res), m_resMgr(resMgr) {}
464
465VexUI::~VexUI() {
466 freeTree(m_root);
467 for (auto& [k, a] : m_fontAtlases) {
468 if (a.view) vkDestroyImageView(m_ctx.device, a.view, nullptr);
469 if (a.image) vmaDestroyImage(m_ctx.allocator, a.image, a.alloc);
470 }
471 if (m_uiSampler) vkDestroySampler(m_ctx.device, m_uiSampler, nullptr);
472 if (m_vb) vmaDestroyBuffer(m_ctx.allocator, m_vb, m_vbAlloc);
473}
474
476 const size_t vbBytes = 2 * 1024 * 1024;
477 VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
478 bi.size = vbBytes;
479 bi.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
480 VmaAllocationCreateInfo ai{};
481 ai.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
482 vmaCreateBuffer(m_ctx.allocator, &bi, &ai, &m_vb, &m_vbAlloc, nullptr);
483 m_vbSize = vbBytes;
484
485 VkSamplerCreateInfo si{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
486 si.magFilter = si.minFilter = VK_FILTER_NEAREST;
487 si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
488 vkCreateSampler(m_ctx.device, &si, nullptr, &m_uiSampler);
489 initialized = true;
490 return true;
491}
492
493float VexUI::getPspMultiplier() const {
494 if (!m_resMgr) return 1.0f;
495 float current = m_resMgr->getUpscaleRatio();
496 if (current <= 0.0f) return 1.0f;
497 return m_resMgr->getPotencialUpscaleRatio() / current;
498}
499
500void VexUI::safeUpdate(const std::string& id, std::function<void(Widget*)> action) {
501 if (initialized) {
502 if (Widget* w = findById(m_root, id)) {
503 action(w);
504 }
505 } else {
506 pendingSetters.push_back([this, id, action]() {
507 if (Widget* w = findById(m_root, id)) {
508 action(w);
509 }
510 });
511 }
512}
513
515 if (!w) return;
516 float pxSize = w->style.fontSize.getPixels(this);
517 if (!w->style.font.empty() && pxSize > 0.f) {
518 std::string key = w->style.font + "_" + std::to_string(static_cast<int>(pxSize));
519 if (m_fontAtlases.count(key) > 0) goto recurse;
520
521 auto data = m_vfs->load_file(GetAssetPath(w->style.font));
522 if (!data) goto recurse;
523
524 FontAtlas atlas;
525 if (!stbtt_InitFont(&atlas.info, (unsigned char*)data->data.data(), 0)) goto recurse;
526
527 int ascent, descent, lineGap;
528 stbtt_GetFontVMetrics(&atlas.info, &ascent, &descent, &lineGap);
529
530 float scale = stbtt_ScaleForPixelHeight(&atlas.info, pxSize);
531 atlas.ascent = static_cast<float>(ascent) * scale;
532 atlas.descent = static_cast<float>(descent) * scale;
533 atlas.scale = scale;
534
535 int texDim = 512;
536 while (texDim < pxSize * 12 && texDim < 8192) texDim *= 2;
537
538 std::vector<unsigned char> bitmap(texDim * texDim, 0);
539 atlas.cdata.resize(96);
540 stbtt_BakeFontBitmap((unsigned char*)data->data.data(), 0, pxSize, bitmap.data(), texDim, texDim, 32, 96, atlas.cdata.data());
541
542 std::vector<unsigned char> rgba(texDim * texDim * 4);
543 for (int i = 0; i < texDim * texDim; ++i) {
544 unsigned char v = bitmap[i];
545 v = (v > 127) ? 255 : 0;
546 rgba[i*4 + 0] = 255; rgba[i*4 + 1] = 255; rgba[i*4 + 2] = 255; rgba[i*4 + 3] = v;
547 }
548
549 std::string texName = "ui_font_" + key;
550 m_res->createTextureFromRaw(rgba, texDim, texDim, texName);
551
552 atlas.texIdx = m_res->getTextureIndex(texName);
553 atlas.width = texDim;
554 atlas.height = texDim;
555 atlas.bakedSize = pxSize;
556 m_fontAtlases[key] = atlas;
557 }
558recurse:
559 const auto& children = w->children;
560 size_t count = children.size();
561 for (size_t i = 0; i < count; ++i) {
562 if (i + 1 < count) _mm_prefetch(reinterpret_cast<const char*>(children[i + 1]) + 64, _MM_HINT_T0);
563 loadFonts(children[i]);
564 }
565}
566
568 if (!w) return;
569 if (!w->image.empty()) m_res->loadTexture(GetAssetPath(w->image), GetAssetPath(w->image));
570 const auto& children = w->children;
571 size_t count = children.size();
572 for (size_t i = 0; i < count; ++i) {
573 if (i + 1 < count) _mm_prefetch(reinterpret_cast<const char*>(children[i + 1]) + 64, _MM_HINT_T0);
574 loadImages(children[i]);
575 }
576}
577
578Widget* VexUI::parseNode(const nlohmann::json& j) {
579 Widget* w = new Widget();
580 w->ui = this;
581 w->layoutNode = new LayoutNode();
582 w->layoutNode->context = w;
583
584 w->nodeJson = j;
585 w->applyLayout(this);
586
587 if (j.contains("children") && j["children"].is_array()) {
588 for (const auto& c : j["children"]) {
589 Widget* child = parseNode(c);
590 child->parent = w;
591
592 if (child->type == WidgetType::Label || child->type == WidgetType::Button) {
593 child->layoutNode->measureFunc = VexUI::measureTextNode;
594 }
595
596 if (!child->children.empty()) {
597 if (child->layoutNode->alignSelf == Align::Auto) {
598 child->layoutNode->alignSelf = Align::Stretch;
599 }
600 }
601 w->children.push_back(child);
602 w->layoutNode->insertChild(child->layoutNode, w->layoutNode->children.size());
603 }
604 }
605 return w;
606}
607
608void VexUI::load(const std::string& path) {
609 if(initialized){
610 std::string realPath = GetAssetPath(path);
611 auto data = m_vfs->load_file(realPath);
612 if (!data) return;
613
614 nlohmann::json json;
615 try { json = nlohmann::json::parse(data->data.begin(), data->data.end()); }
616 catch (...) { return; }
617
618 freeTree(m_root);
619 m_root = nullptr;
620 m_focusedWidget = nullptr;
621 if (json.contains("root")) {
622 m_root = parseNode(json["root"]);
623 if (json["root"].contains("zindex")) zIndex = json["root"]["zindex"].get<int>();
624 }
625 if (m_root && json.contains("overlays") && json["overlays"].is_array()) {
626 for (const auto& ov : json["overlays"]) {
627 Widget* overlay = parseNode(ov);
628 overlay->layoutNode->positionType = PositionType::Absolute;
629 m_root->children.push_back(overlay);
630 m_root->layoutNode->insertChild(overlay->layoutNode, m_root->layoutNode->children.size());
631 }
632 }
633 if (m_root) {
634 loadFonts(m_root);
635 loadImages(m_root);
636 }
637 }else{
638 loadPending = true;
639 loadPath = path;
640 }
641}
642
643void VexUI::layout(glm::uvec2 res) {
644 if (m_root) calculateLayout(m_root->layoutNode, static_cast<float>(res.x), static_cast<float>(res.y), true, this);
645}
646
647Widget* VexUI::findWidgetAt(Widget* w, glm::vec2 pos, glm::vec2 parentOffset) {
648 if (!w || !w->layoutNode) return nullptr;
649
650 float absX = parentOffset.x + w->layoutNode->computedLeft;
651 float absY = parentOffset.y + w->layoutNode->computedTop;
652 float width = w->layoutNode->computedWidth;
653 float height = w->layoutNode->computedHeight;
654
655 glm::vec2 pivot = { absX + width * 0.5f, absY + height * 0.5f };
656 glm::vec2 checkPos = pos;
657 if (w->rotation != 0.f) {
658 float rads = glm::radians(-w->rotation);
659 float c = cos(rads), s = sin(rads);
660 glm::vec2 d = pos - pivot;
661 checkPos.x = pivot.x + (d.x * c - d.y * s);
662 checkPos.y = pivot.y + (d.x * s + d.y * c);
663 }
664
665 bool isInside = (checkPos.x >= absX && checkPos.x <= absX + width && checkPos.y >= absY && checkPos.y <= absY + height);
666
667 for (auto it = w->children.rbegin(); it != w->children.rend(); ++it) {
668 if (Widget* hit = findWidgetAt(*it, pos, {absX, absY})) return hit;
669 }
670 return isInside ? w : nullptr;
671}
672
673Widget* VexUI::findById(Widget* w, const std::string& id) {
674 if (!w) return nullptr;
675 if (w->id == id) return w;
676 for (auto* c : w->children) if (auto* f = findById(c, id)) return f;
677 return nullptr;
678}
679
680void VexUI::setText(const std::string& id, const std::string& txt) {
681 safeUpdate(id, [txt](Widget* w) {
682 if (w->text != txt) {
683 w->text = txt;
684 w->nodeJson["text"] = txt;
685 }
686 });
687}
688
689void VexUI::setOnClick(const std::string& id, std::function<void()> cb) {
690 safeUpdate(id, [cb](Widget* w) {
691 if (w->type == WidgetType::Button) w->onClick = std::move(cb);
692 });
693}
694
695void VexUI::processEvent(const SDL_Event& ev) {
696 if (!m_root) return;
697 float sx = static_cast<float>(m_ctx.swapchainExtent.width) / m_ctx.currentRenderResolution.x;
698 float sy = static_cast<float>(m_ctx.swapchainExtent.height) / m_ctx.currentRenderResolution.y;
699
700 if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
701 glm::vec2 mouse(ev.button.x / sx, ev.button.y / sy);
702 if (Widget* w = findWidgetAt(m_root, mouse, {0, 0}); w && w->type == WidgetType::Button && w->onClick) w->onClick();
703 }
704 else if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
705 SDL_GamepadButton button = (SDL_GamepadButton)ev.gbutton.button;
706 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
707 if (m_focusedWidget && m_focusedWidget->onClick) m_focusedWidget->onClick();
708 }
709 else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) navigateToWidget(-1.0f, 0.0f);
710 else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) navigateToWidget(1.0f, 0.0f);
711 else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) navigateToWidget(0.0f, -1.0f);
712 else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) navigateToWidget(0.0f, 1.0f);
713 }
714 else if (ev.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
715 SDL_GamepadAxis axis = (SDL_GamepadAxis)ev.gaxis.axis;
716 float value = ev.gaxis.value / 32767.0f;
717 if (axis == SDL_GAMEPAD_AXIS_LEFTX) {
718 bool wasAbove = std::abs(m_gamepadAxisX) > GAMEPAD_AXIS_THRESHOLD;
719 bool isAbove = std::abs(value) > GAMEPAD_AXIS_THRESHOLD;
720 if (!wasAbove && isAbove) navigateToWidget((value > 0.0f) ? 1.0f : -1.0f, 0.0f);
721 m_gamepadAxisX = value;
722 }
723 else if (axis == SDL_GAMEPAD_AXIS_LEFTY) {
724 bool wasAbove = std::abs(m_gamepadAxisY) > GAMEPAD_AXIS_THRESHOLD;
725 bool isAbove = std::abs(value) > GAMEPAD_AXIS_THRESHOLD;
726 if (!wasAbove && isAbove) navigateToWidget(0.0f, (value > 0.0f) ? 1.0f : -1.0f);
727 m_gamepadAxisY = value;
728 }
729 }
730}
731
732void VexUI::navigateToWidget(float dirX, float dirY) {
733 std::vector<Widget*> navigable;
734 getNavigableWidgets(navigable);
735 if (navigable.empty()) return;
736
737 bool focusedIsValid = false;
738 if (m_focusedWidget) {
739 for (Widget* w : navigable) {
740 if (w == m_focusedWidget) { focusedIsValid = true; break; }
741 }
742 }
743 if (!focusedIsValid) { setFocusedWidget(navigable.front()); return; }
744
745 glm::vec2 fromPos = getWidgetCenter(m_focusedWidget);
746 glm::vec2 direction{dirX, dirY};
747
748 Widget* nextWidget = findClosestNavigableWidget(fromPos, direction, navigable);
749 if (nextWidget) setFocusedWidget(nextWidget);
750}
751
752void VexUI::getNavigableWidgets(std::vector<Widget*>& out) { if (m_root) collectNavigableWidgets(m_root, out); }
753
754void VexUI::collectNavigableWidgets(Widget* w, std::vector<Widget*>& out) {
755 if (!w) return;
756 if (isWidgetNavigable(w)) out.push_back(w);
757 for (Widget* child : w->children) collectNavigableWidgets(child, out);
758}
759
761 if (!w) return false;
762 return w->type == WidgetType::Button;
763}
764
766 if (!w || !w->layoutNode) return {0.0f, 0.0f};
767
768 float x = w->layoutNode->computedLeft;
769 float y = w->layoutNode->computedTop;
770 float width = w->layoutNode->computedWidth;
771 float height = w->layoutNode->computedHeight;
772
773 Widget* parent = w->parent;
774 while (parent) {
775 x += parent->layoutNode->computedLeft;
776 y += parent->layoutNode->computedTop;
777 parent = parent->parent;
778 }
779 return {x + width * 0.5f, y + height * 0.5f};
780}
781
782Widget* VexUI::findClosestNavigableWidget(const glm::vec2& fromPos, const glm::vec2& direction, const std::vector<Widget*>& candidates) {
783 Widget* closest = nullptr;
784 float closestScore = -FLT_MAX;
785 bool isHorizontalNav = std::abs(direction.x) > std::abs(direction.y);
786
787 for (Widget* candidate : candidates) {
788 if (candidate == m_focusedWidget) continue;
789
790 glm::vec2 toCandidate = getWidgetCenter(candidate) - fromPos;
791 float distance = glm::length(toCandidate);
792 if (distance < 1.0f) continue;
793
794 float dot = glm::dot(glm::normalize(direction), glm::normalize(toCandidate));
795 float crossAlignment = 1.0f;
796
797 if (isHorizontalNav) crossAlignment = std::max(0.0f, 1.0f - (std::abs(toCandidate.y) / 500.0f));
798 else crossAlignment = std::max(0.0f, 1.0f - (std::abs(toCandidate.x) / 500.0f));
799
800 if (dot > -0.2f) {
801 float score = dot * 3.0f + crossAlignment * 0.5f - (distance / 800.0f);
802 if (score > closestScore) { closestScore = score; closest = candidate; }
803 }
804 }
805 return closest;
806}
807
809 if (w && !isWidgetNavigable(w)) return;
810 if (w == m_focusedWidget) return;
811 if (m_focusedWidget && m_focusedWidget->onFocusLost) m_focusedWidget->onFocusLost();
812 m_focusedWidget = w;
813 if (m_focusedWidget && m_focusedWidget->onFocusEnter) m_focusedWidget->onFocusEnter();
814}
815
816glm::vec2 VexUI::calculateTextSize(Widget* w, float maxWidth) {
817 float pxSize = w->style.fontSize.getPixels(this);
818 std::string key = w->style.font + "_" + std::to_string(static_cast<int>(pxSize));
819 auto it = m_fontAtlases.find(key);
820 if (it == m_fontAtlases.end() || w->text.empty()) return {0, 0};
821
822 const FontAtlas& a = it->second;
823 std::vector<std::string> lines = wrapText(w->text, a, maxWidth);
824
825 float lineHeight = a.ascent - a.descent;
826 float totalHeight = lines.size() * lineHeight;
827 float totalWidth = 0.f;
828
829 for (const auto& line : lines) {
830 float lineWidth = 0.f;
831 for (char ch : line) {
832 if (ch < 32 || ch > 127) continue;
833 lineWidth += a.cdata[ch - 32].xadvance;
834 }
835 totalWidth = std::max(totalWidth, lineWidth);
836 }
837 return {totalWidth, totalHeight};
838}
839
840void VexUI::measureTextNode(LayoutNode* node, float width, float height) {
841 Widget* w = static_cast<Widget*>(node->context);
842 if (!w || !w->ui) return;
843
844 float maxW = (width > 0.0f && width != FLT_MAX && !std::isnan(width)) ? width : FLT_MAX;
845 glm::vec2 measuredSize = w->ui->calculateTextSize(w, maxW);
846
847 node->computedWidth = measuredSize.x;
848 node->computedHeight = measuredSize.y;
849}
850
851void VexUI::batch(Widget* w, std::vector<float>& verts, glm::vec2 parentOffset) {
852 if (!w || !w->layoutNode) return;
853
854 float x = parentOffset.x + w->layoutNode->computedLeft;
855 float y = parentOffset.y + w->layoutNode->computedTop;
856 float width = w->layoutNode->computedWidth;
857 float height = w->layoutNode->computedHeight;
858
859 glm::vec2 pivot = { x + width * 0.5f, y + height * 0.5f };
860 float rads = glm::radians(w->rotation);
861 float cosA = cos(rads);
862 float sinA = sin(rads);
863
864 auto pushQuad = [&](float x0, float y0, float u0, float v0, float x1, float y1, float u1, float v1, const glm::vec4& col, float texIdx) {
865 float rV[8];
866 static bool useAVX2 = HardwareInfo::HasAVX2();
867 if (useAVX2) rotateQuadAvX2(rV, pivot.x, pivot.y, sinA, cosA, x0, y0, x1, y1);
868 else rotateQuadScalar(rV, pivot.x, pivot.y, sinA, cosA, x0, y0, x1, y1);
869
870 verts.insert(verts.end(), {rV[0], rV[1], u0, v0, col.r, col.g, col.b, col.a, texIdx});
871 verts.insert(verts.end(), {rV[2], rV[3], u1, v0, col.r, col.g, col.b, col.a, texIdx});
872 verts.insert(verts.end(), {rV[4], rV[5], u0, v1, col.r, col.g, col.b, col.a, texIdx});
873
874 verts.insert(verts.end(), {rV[2], rV[3], u1, v0, col.r, col.g, col.b, col.a, texIdx});
875 verts.insert(verts.end(), {rV[6], rV[7], u1, v1, col.r, col.g, col.b, col.a, texIdx});
876 verts.insert(verts.end(), {rV[4], rV[5], u0, v1, col.r, col.g, col.b, col.a, texIdx});
877 };
878
879 if (w->style.bgColor.a > 0.f) {
880 pushQuad(x, y, 0, 0, x + width, y + height, 1, 1, w->style.bgColor, -1.f);
881 }
882
883 float bw = w->style.borderWidth.getPixels(this);
884 if (bw > 0.f && w->style.borderColor.a > 0.f) {
885 pushQuad(x, y, 0, 0, x + width, y + bw, 1, 1, w->style.borderColor, -1.f);
886 pushQuad(x, y + height - bw, 0, 0, x + width, y + height, 1, 1, w->style.borderColor, -1.f);
887 pushQuad(x, y, 0, 0, x + bw, y + height, 1, 1, w->style.borderColor, -1.f);
888 pushQuad(x + width - bw, y, 0, 0, x + width, y + height, 1, 1, w->style.borderColor, -1.f);
889 }
890
891 if ((w->type == WidgetType::Label || w->type == WidgetType::Button) && !w->text.empty() && !w->style.font.empty()) {
892 float pxSize = w->style.fontSize.getPixels(this);
893 std::string key = w->style.font + "_" + std::to_string(static_cast<int>(pxSize));
894 auto it = m_fontAtlases.find(key);
895 if (it != m_fontAtlases.end()) {
896 const FontAtlas& a = it->second;
897
898 float bPadL = w->layoutNode->paddingLeft.resolveOr(width, 0.f) + bw;
899 float bPadR = w->layoutNode->paddingRight.resolveOr(width, 0.f) + bw;
900 float bPadT = w->layoutNode->paddingTop.resolveOr(height, 0.f) + bw;
901 float bPadB = w->layoutNode->paddingBottom.resolveOr(height, 0.f) + bw;
902
903 float innerW = std::max(0.0f, width - bPadL - bPadR);
904 float innerH = std::max(0.0f, height - bPadT - bPadB);
905
906 std::vector<std::string> lines = wrapText(w->text, a, innerW);
907
908 float lineHeight = a.ascent - a.descent;
909 float totalTextHeight = lines.size() * lineHeight;
910 float verticalOffset = (innerH - totalTextHeight) / 2.0f;
911
912 float cy = y + bPadT + verticalOffset + a.ascent;
913
914 for (const auto& line : lines) {
915 float lineWidth = 0.f;
916 for (char ch : line) {
917 if (ch < 32 || ch > 127) continue;
918 lineWidth += a.cdata[ch - 32].xadvance;
919 }
920
921 float startX = x + bPadL;
922 if (w->textAlign == TextAlign::Center) startX = x + bPadL + (innerW - lineWidth) / 2.f;
923 else if (w->textAlign == TextAlign::Right) startX = x + bPadL + (innerW - lineWidth);
924
925 float cx = startX;
926 for (char ch : line) {
927 if (ch < 32 || ch > 127) continue;
928 const stbtt_bakedchar& cd = a.cdata[ch - 32];
929
930 pushQuad(cx + cd.xoff, cy + cd.yoff, cd.x0 / float(a.width), cd.y0 / float(a.height),
931 cx + cd.xoff + (cd.x1 - cd.x0), cy + cd.yoff + (cd.y1 - cd.y0),
932 cd.x1 / float(a.width), cd.y1 / float(a.height),
933 w->style.color, static_cast<float>(a.texIdx));
934 cx += cd.xadvance;
935 }
936 cy += lineHeight;
937 }
938 }
939 }
940
941 if (w->type == WidgetType::Image && !w->image.empty()) {
942 uint32_t idx = m_res->getTextureIndex(GetAssetPath(w->image));
943 if (idx != UINT32_MAX) pushQuad(x, y, 0, 0, x + width, y + height, 1, 1, {1,1,1,1}, static_cast<float>(idx));
944 }
945
946 const auto& children = w->children;
947 size_t count = children.size();
948 for (size_t i = 0; i < count; ++i) {
949 if (i + 1 < count) _mm_prefetch(reinterpret_cast<const char*>(children[i + 1]) + 64, _MM_HINT_T0);
950 batch(children[i], verts, {x, y});
951 }
952}
953
954void VexUI::render(VkCommandBuffer cmd, VkPipeline pipeline, VkPipelineLayout pipelineLayout, int currentFrame) {
955 if (!m_root) return;
956
957 glm::uvec2 currentRes = m_ctx.currentRenderResolution;
958 float currentPspMult = getPspMultiplier();
959
960 if (m_lastRenderRes != currentRes || m_lastPspMult != currentPspMult) {
961 m_lastRenderRes = currentRes;
962 m_lastPspMult = currentPspMult;
963
964 std::function<void(Widget*)> syncTree = [&](Widget* w) {
965 if (!w) return;
966 w->applyLayout(this);
967 for (auto* c : w->children) syncTree(c);
968 };
969 syncTree(m_root);
970
971 for (auto& [k, a] : m_fontAtlases) {
972 if (a.view) vkDestroyImageView(m_ctx.device, a.view, nullptr);
973 if (a.image) vmaDestroyImage(m_ctx.allocator, a.image, a.alloc);
974 }
975 m_fontAtlases.clear();
976 loadFonts(m_root);
977 }
978
979 layout(m_ctx.currentRenderResolution);
980 vkDeviceWaitIdle(m_ctx.device);
981
982 std::vector<float> verts;
983 verts.reserve(1024 * 9);
984 batch(m_root, verts);
985 uploadVerts(verts);
986
987 if (verts.empty()) return;
988
989 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
990
991 if (VkDescriptorSet globalUBO = m_res->getUBODescriptorSet(currentFrame); globalUBO != VK_NULL_HANDLE) {
992 uint32_t dynamicOffset = 0;
993 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &globalUBO, 1, &dynamicOffset);
994 }
995
996 UIPushConstants uiPC{ glm::ortho(0.0f, static_cast<float>(m_ctx.currentRenderResolution.x), 0.0f, static_cast<float>(m_ctx.currentRenderResolution.y), -1.0f, 1.0f) };
997 vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(UIPushConstants), &uiPC);
998
999 VkDeviceSize offset = 0;
1000 vkCmdBindVertexBuffers(cmd, 0, 1, &m_vb, &offset);
1001
1002 uint32_t totalVertices = verts.size() / 9;
1003 uint32_t currentVertex = 0;
1004 int currentTexIndex = INT_MIN;
1005
1006 while (currentVertex < totalVertices) {
1007 int texIndex = static_cast<int>(verts[currentVertex * 9 + 8]);
1008 if (texIndex != currentTexIndex) {
1009 currentTexIndex = texIndex;
1010 if (!m_ctx.supportsBindlessTextures) {
1011 VkDescriptorSet currentTexSet = m_res->getTextureDescriptorSet(currentFrame, texIndex >= 0 ? texIndex : 0);
1012 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 1, 1, &currentTexSet, 0, nullptr);
1013 }
1014 }
1015 vkCmdDraw(cmd, 6, 1, currentVertex, 0);
1016 currentVertex += 6;
1017 }
1018}
1019
1020void VexUI::uploadVerts(const std::vector<float>& verts) {
1021 if (verts.empty()) return;
1022 size_t bytes = verts.size() * sizeof(float);
1023 void* dst; vmaMapMemory(m_ctx.allocator, m_vbAlloc, &dst);
1024 memcpy(dst, verts.data(), bytes);
1025 vmaUnmapMemory(m_ctx.allocator, m_vbAlloc);
1026}
1027
1029 if (!w) return;
1030 for (auto* c : w->children) freeTree(c);
1031 delete w;
1032}
1033
1034void VexUI::setRotation(const std::string& id, float degrees) {
1035 safeUpdate(id, [this, degrees](Widget* w) {
1036 w->nodeJson["rotation"] = degrees;
1037 w->applyLayout(this);
1038 });
1039}
1040
1041void VexUI::setPosition(const std::string& id, UIUnitValue x, UIUnitValue y) {
1042 safeUpdate(id, [this, x, y](Widget* w) {
1043 w->nodeJson["position"] = "absolute";
1044 w->nodeJson["left"] = x.toJson();
1045 w->nodeJson["top"] = y.toJson();
1046 w->applyLayout(this);
1047 });
1048}
1049
1050void VexUI::setSize(const std::string& id, UIUnitValue width, UIUnitValue height) {
1051 safeUpdate(id, [this, width, height](Widget* w) {
1052 w->nodeJson["size"] = {width.toJson(), height.toJson()};
1053 w->applyLayout(this);
1054 });
1055}
1056
1057void VexUI::setImage(const std::string& id, const std::string& path) {
1058 safeUpdate(id, [this, path](Widget* w) {
1059 w->image = path;
1060 w->nodeJson["image"] = path;
1061 m_res->loadTexture(GetAssetPath(path), GetAssetPath(path));
1062 });
1063}
1064
1065void VexUI::setFont(const std::string& id, const std::string& fontPath, UIUnitValue fontSize) {
1066 safeUpdate(id, [this, fontPath, fontSize](Widget* w) {
1067 w->nodeJson["style"]["font"] = fontPath;
1068 w->nodeJson["style"]["size"] = fontSize.toJson();
1069 w->applyLayout(this);
1070 this->loadFonts(w);
1071 });
1072}
1073
1074void VexUI::setColor(const std::string& id, glm::vec4 color) {
1075 safeUpdate(id, [this, color](Widget* w) {
1076 w->nodeJson["style"]["color"] = {color.r, color.g, color.b, color.a};
1077 w->applyLayout(this);
1078 });
1079}
1080
1081void VexUI::setBackgroundColor(const std::string& id, glm::vec4 color) {
1082 safeUpdate(id, [this, color](Widget* w) {
1083 w->nodeJson["style"]["bgColor"] = {color.r, color.g, color.b, color.a};
1084 w->applyLayout(this);
1085 });
1086}
1087
1088void VexUI::setBorder(const std::string& id, UIUnitValue width, glm::vec4 color) {
1089 safeUpdate(id, [this, width, color](Widget* w) {
1090 w->nodeJson["borderWidth"] = width.toJson();
1091 w->nodeJson["borderColor"] = {color.r, color.g, color.b, color.a};
1092 w->applyLayout(this);
1093 });
1094}
1095
1096} // namespace vex
This file defines structs needed for UI rendering.
This file defines vex ui class, very basic ui system.
static bool HasAVX2()
Checks if the CPU supports the AVX2 instruction set.
Class responsible for managing resolution settings, calculating render resolution based on selected m...
float getUpscaleRatio() const
Function to get the current upscale ratio. Read window resolution divided by render resolution.
float getPotencialUpscaleRatio()
Calculates the potential upscale ratio for a given height. @description allows you to create consista...
Class defining VexUI, it initializes the UI system, loads, renders, converts ui data,...
Definition VexUI.hpp:139
VexUI(VulkanContext &ctx, VirtualFileSystem *vfs, VulkanResources *res, ResolutionManager *resMgr)
Constructor for VexUI.
Definition VexUI.cpp:462
void setColor(const std::string &id, glm::vec4 color)
Set text/tint color.
Definition VexUI.cpp:1074
void setSize(const std::string &id, UIUnitValue w, UIUnitValue h)
Resize a widget.
Definition VexUI.cpp:1050
void setRotation(const std::string &id, float degrees)
Set rotation of a widget in degrees.
Definition VexUI.cpp:1034
void loadImages(Widget *w)
Loads images for the UI.
Definition VexUI.cpp:567
bool isWidgetNavigable(Widget *w) const
Check if widget is navigable (button type).
Definition VexUI.cpp:760
void batch(Widget *w, std::vector< float > &verts, glm::vec2 parentOffset={0.f, 0.f})
Batches the UI for rendering.
Definition VexUI.cpp:851
glm::vec2 calculateTextSize(Widget *w, float maxWidth=FLT_MAX)
Calculates the size of the text.
Definition VexUI.cpp:816
void setText(const std::string &id, const std::string &txt)
Set text of a UI element.
Definition VexUI.cpp:680
bool init()
Initialize the UI system, components needed later on.
Definition VexUI.cpp:475
void getNavigableWidgets(std::vector< Widget * > &out)
Get all navigable widgets (buttons).
Definition VexUI.cpp:752
void setPosition(const std::string &id, UIUnitValue x, UIUnitValue y)
Move a widget (sets Left/Top yoga properties). Works best with position: absolute,...
Definition VexUI.cpp:1041
static void measureTextNode(LayoutNode *node, float width, float height)
Measures a text node.
Definition VexUI.cpp:840
Widget * findById(Widget *w, const std::string &id)
Finds a widget by its id.
Definition VexUI.cpp:673
void loadFonts(Widget *w)
Loads fonts for the UI.
Definition VexUI.cpp:514
void processEvent(const SDL_Event &ev)
Process mouse and keyboard events.
Definition VexUI.cpp:695
void safeUpdate(const std::string &id, std::function< void(Widget *)> action)
Updates a widget safely.
Definition VexUI.cpp:500
void setFont(const std::string &id, const std::string &fontPath, UIUnitValue fontSize)
Change font properties.
Definition VexUI.cpp:1065
Widget * findClosestNavigableWidget(const glm::vec2 &fromPos, const glm::vec2 &direction, const std::vector< Widget * > &candidates)
Find closest navigable widget in a direction.
Definition VexUI.cpp:782
void setBorder(const std::string &id, UIUnitValue width, glm::vec4 color)
Set border properties.
Definition VexUI.cpp:1088
void navigateToWidget(float dirX, float dirY)
Navigate to widget in a given direction.
Definition VexUI.cpp:732
void freeTree(Widget *w)
Frees the widget tree.
Definition VexUI.cpp:1028
void setOnClick(const std::string &id, std::function< void()> cb)
Set on-click callback of a UI element.
Definition VexUI.cpp:689
Widget * parseNode(const nlohmann::json &j)
Parses a node from json to widgets.
Definition VexUI.cpp:578
void layout(glm::uvec2 res)
Layouts the UI using yoga.
Definition VexUI.cpp:643
void uploadVerts(const std::vector< float > &verts)
Uploads the vertex buffer to the GPU.
Definition VexUI.cpp:1020
glm::vec2 getWidgetCenter(Widget *w)
Get widget center position in screen space.
Definition VexUI.cpp:765
std::vector< std::string > wrapText(const std::string &text, const FontAtlas &a, float maxWidth)
Wraps text to fit within a given width.
Definition VexUI.cpp:209
void setImage(const std::string &id, const std::string &path)
Change the image of an image widget.
Definition VexUI.cpp:1057
void load(const std::string &path)
Load UI data from a JSON file.
Definition VexUI.cpp:608
Widget * findWidgetAt(Widget *w, glm::vec2 pos, glm::vec2 parentOffset={0.f, 0.f})
Finds a widget at a position.
Definition VexUI.cpp:647
void setBackgroundColor(const std::string &id, glm::vec4 color)
Set background color.
Definition VexUI.cpp:1081
void render(VkCommandBuffer cmd, VkPipeline pipeline, VkPipelineLayout pipelineLayout, int currentFrame)
Render the UI. It is called by main rendering function.
Definition VexUI.cpp:954
void collectNavigableWidgets(Widget *w, std::vector< Widget * > &out)
Recursively collect navigable widgets.
Definition VexUI.cpp:754
void setFocusedWidget(Widget *w)
Set focus to a widget and trigger focus callbacks.
Definition VexUI.cpp:808
This class provides abstraction of file system needed for loading packed and unpacked assets.
This class manages resources like textures, descriptor sets, and uniform buffers.
Definition Resources.hpp:23
std::string VEX_EXPORT GetAssetPath(const std::string &relativePath)
Resolves the absolute path of an asset based on the current build configuration.
Definition PathUtils.cpp:73
void calculateLayout(LayoutNode *node, float parentWidth, float parentHeight, bool isRoot=false, const VexUI *ui=nullptr, float forcedW=NAN, float forcedH=NAN)
Calculate the layout of a node tree using flex layout algorithm.
Definition VexUI.cpp:16
float resolve(float parent) const
Resolve the flex value to pixels based on parent dimension.
Definition Layout.hpp:25
float resolveOr(float parent, float fallback) const
Resolve the flex value to pixels, with a fallback if unspecified.
Definition Layout.hpp:34
Font atlas structure.
Definition VexUI.hpp:76
Layout node representing a single UI element in the layout tree.
Definition Layout.hpp:41
FlexValue paddingLeft
Left padding.
Definition Layout.hpp:70
FlexValue paddingBottom
Bottom padding.
Definition Layout.hpp:73
void insertChild(LayoutNode *child, size_t index)
Insert a child node at a specific index.
Definition Layout.hpp:111
FlexValue height
Element height.
Definition Layout.hpp:49
void * context
Opaque context pointer for user data.
Definition Layout.hpp:44
FlexValue borderWidth
Border width.
Definition Layout.hpp:78
std::vector< LayoutNode * > children
Child nodes.
Definition Layout.hpp:43
Justify justifyContent
Justify content alignment.
Definition Layout.hpp:91
float computedHeight
Computed height.
Definition Layout.hpp:105
std::function< void(LayoutNode *, float, float)> measureFunc
Measure function for intrinsic sizing (e.g., text).
Definition Layout.hpp:98
float flexGrow
Flex grow factor.
Definition Layout.hpp:83
FlexValue width
Element width.
Definition Layout.hpp:48
float computedWidth
Computed width.
Definition Layout.hpp:104
float computedTop
Computed top position.
Definition Layout.hpp:103
Align alignSelf
Align self override.
Definition Layout.hpp:93
PositionType positionType
Position type (relative or absolute).
Definition Layout.hpp:90
FlexDirection flexDirection
Direction of flex layout.
Definition Layout.hpp:89
FlexValue paddingRight
Right padding.
Definition Layout.hpp:72
float computedLeft
Computed left position.
Definition Layout.hpp:102
FlexValue paddingTop
Top padding.
Definition Layout.hpp:71
Align alignItems
Align items cross-axis alignment.
Definition Layout.hpp:92
Push constants for UI rendering.
Definition UIVertex.hpp:23
Struct storing unit value logic natively.
Definition VexUI.hpp:41
void parse(const nlohmann::json &jval)
Parses a json value into the unit struct.
Definition VexUI.cpp:284
nlohmann::json toJson() const
Converts the unit back to a json-compatible format (preserves strings like "px", "%").
Definition VexUI.cpp:306
float getPixels(const VexUI *ui) const
Dynamically calculates the raw pixel representation required for rendering.
Definition VexUI.cpp:320
Struct holding all vulkan data, like device, surface, swapchain, images, views, and more.
Definition context.hpp:26
Struct defining widget component, it has a lot of redundancy and probably will need cleanup before ex...
Definition VexUI.hpp:113
Represents an RGBA color value. Used mainly for fancy rendering in editor.