1#define STB_TRUETYPE_IMPLEMENTATION
3#include "glm/ext/matrix_clip_space.hpp"
10#include <components/ErrorUtils.hpp>
11#include <components/PathUtils.hpp>
12#include "components/HardwareInfo.hpp"
24 float padX = pl + pr + bw * 2.f;
25 float padY = pt + pb + bw * 2.f;
27 float explicitW = !std::isnan(forcedW) ? forcedW : node->
width.
resolve(availableW);
28 float explicitH = !std::isnan(forcedH) ? forcedH : node->
height.
resolve(availableH);
31 explicitW = availableW;
32 explicitH = availableH;
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);
39 float maxW = std::isnan(innerAvailW) ? FLT_MAX : innerAvailW;
40 float maxH = std::isnan(innerAvailH) ? FLT_MAX : innerAvailH;
47 node->
computedWidth = std::isnan(explicitW) ? measuredW : explicitW;
48 node->
computedHeight = std::isnan(explicitH) ? measuredH : explicitH;
52 std::vector<LayoutNode*> flow;
54 if (c->positionType == PositionType::Absolute)
continue;
58 float totalMain = 0.f;
60 float totalGrow = 0.f;
61 float totalShrink = 0.f;
63 for (
auto* c : flow) {
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);
71 float outerW = c->computedWidth + cml + cmr;
72 float outerH = c->computedHeight + cmt + cmb;
76 maxCross = std::max(maxCross, outerH);
79 maxCross = std::max(maxCross, outerW);
81 totalGrow += c->flexGrow;
82 totalShrink += c->flexShrink;
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;
90 float freeMain = (node->
flexDirection == FlexDirection::Row ? innerW : innerH) - totalMain;
92 bool needsRelayout =
false;
94 for (
auto* c : flow) {
95 float childForcedW = NAN;
96 float childForcedH = NAN;
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;
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);
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);
118 if (node->
flexDirection == FlexDirection::Row && std::isnan(c->height.resolve(innerH))) {
119 childForcedH = std::max(0.0f, innerH - cmt - cmb);
121 }
else if (node->
flexDirection == FlexDirection::Column && std::isnan(c->width.resolve(innerW))) {
122 childForcedW = std::max(0.0f, innerW - cml - cmr);
128 calculateLayout(c, innerW, innerH,
false, ui, childForcedW, childForcedH);
129 needsRelayout =
true;
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);
142 freeMain = (node->
flexDirection == FlexDirection::Row ? innerW : innerH) - totalMain;
145 float mainPos = node->
flexDirection == FlexDirection::Row ? (pl + bw) : (pt + bw);
146 float crossStart = node->
flexDirection == FlexDirection::Row ? (pt + bw) : (pl + bw);
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);
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);
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;
167 float crossOff = 0.f;
168 Align al = c->alignSelf == Align::Auto ? node->
alignItems : c->alignSelf;
169 if (al == Align::Baseline) al = Align::FlexStart;
171 if (al == Align::Center) crossOff = (pCross - cCross) / 2.0f;
172 else if (al == Align::FlexEnd) crossOff = pCross - cCross;
175 c->computedLeft = mainPos + cml;
176 c->computedTop = crossStart + crossOff + cmt;
178 c->computedTop = mainPos + cmt;
179 c->computedLeft = crossStart + crossOff + cml;
181 mainPos += cMain + space;
185 if (c->positionType != PositionType::Absolute)
continue;
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);
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);
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;
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;
210 std::vector<std::string> lines;
211 std::string currentLine;
212 float currentWidth = 0.f;
213 size_t lastSpacePos = std::string::npos;
215 for (
char ch : text) {
217 lines.push_back(currentLine);
220 lastSpacePos = std::string::npos;
223 if (ch < 32 || ch > 127)
continue;
225 const stbtt_bakedchar& cd = a.cdata[ch - 32];
226 float advance = cd.xadvance;
228 if (currentWidth + advance > maxWidth && !currentLine.empty()) {
230 lines.push_back(currentLine);
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;
239 for (
char r : currentLine) currentWidth += a.cdata[r - 32].xadvance;
240 lastSpacePos = std::string::npos;
242 lines.push_back(currentLine);
243 currentLine = std::string(1, ch);
244 currentWidth = advance;
245 lastSpacePos = std::string::npos;
248 if (ch ==
' ') lastSpacePos = currentLine.length();
250 currentWidth += advance;
253 if (!currentLine.empty()) lines.push_back(currentLine);
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};
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);
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);
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; }
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);
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;
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();
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";
321 if (!ui)
return value;
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;
334 if (layoutNode)
delete layoutNode;
337void Widget::applyLayout(VexUI* uiManager) {
338 if (!uiManager || !layoutNode)
return;
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};
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;
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>();
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);
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>());
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>());
372 if (s.contains(
"font")) style.font = s[
"font"].get<std::string>();
373 if (s.contains(
"size")) style.fontSize = UIUnitValue::parseJson(s[
"size"]);
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;
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;
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;
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>();
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;
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;
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"]));
429 if (nodeJson.contains(
"margin")) {
430 auto m = mapUnit(UIUnitValue::parseJson(nodeJson[
"margin"]));
431 layoutNode->marginLeft = layoutNode->marginRight = layoutNode->marginTop = layoutNode->marginBottom = m;
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"]));
438 if (nodeJson.contains(
"borderWidth")) {
439 style.borderWidth = UIUnitValue::parseJson(nodeJson[
"borderWidth"]);
440 layoutNode->borderWidth = mapUnit(style.borderWidth);
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>());
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;
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;
463 : m_ctx(ctx), m_vfs(vfs), m_res(res), m_resMgr(resMgr) {}
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);
471 if (m_uiSampler) vkDestroySampler(m_ctx.device, m_uiSampler,
nullptr);
472 if (m_vb) vmaDestroyBuffer(m_ctx.allocator, m_vb, m_vbAlloc);
476 const size_t vbBytes = 2 * 1024 * 1024;
477 VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
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);
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);
493float VexUI::getPspMultiplier()
const {
494 if (!m_resMgr)
return 1.0f;
496 if (current <= 0.0f)
return 1.0f;
506 pendingSetters.push_back([
this,
id, action]() {
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;
521 auto data = m_vfs->load_file(
GetAssetPath(w->style.font));
522 if (!data)
goto recurse;
525 if (!stbtt_InitFont(&atlas.info, (
unsigned char*)data->data.data(), 0))
goto recurse;
527 int ascent, descent, lineGap;
528 stbtt_GetFontVMetrics(&atlas.info, &ascent, &descent, &lineGap);
530 float scale = stbtt_ScaleForPixelHeight(&atlas.info, pxSize);
531 atlas.ascent =
static_cast<float>(ascent) * scale;
532 atlas.descent =
static_cast<float>(descent) * scale;
536 while (texDim < pxSize * 12 && texDim < 8192) texDim *= 2;
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());
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;
549 std::string texName =
"ui_font_" + key;
550 m_res->createTextureFromRaw(
rgba, texDim, texDim, texName);
552 atlas.texIdx = m_res->getTextureIndex(texName);
553 atlas.width = texDim;
554 atlas.height = texDim;
555 atlas.bakedSize = pxSize;
556 m_fontAtlases[key] = atlas;
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);
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);
585 w->applyLayout(
this);
587 if (j.contains(
"children") && j[
"children"].is_array()) {
588 for (
const auto& c : j[
"children"]) {
592 if (child->type == WidgetType::Label || child->type == WidgetType::Button) {
596 if (!child->children.empty()) {
597 if (child->layoutNode->
alignSelf == Align::Auto) {
598 child->layoutNode->
alignSelf = Align::Stretch;
601 w->children.push_back(child);
611 auto data = m_vfs->load_file(realPath);
615 try { json = nlohmann::json::parse(data->data.begin(), data->data.end()); }
616 catch (...) {
return; }
620 m_focusedWidget =
nullptr;
621 if (json.contains(
"root")) {
623 if (json[
"root"].contains(
"zindex")) zIndex = json[
"root"][
"zindex"].get<
int>();
625 if (m_root && json.contains(
"overlays") && json[
"overlays"].is_array()) {
626 for (
const auto& ov : json[
"overlays"]) {
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());
644 if (m_root)
calculateLayout(m_root->layoutNode,
static_cast<float>(res.x),
static_cast<float>(res.y),
true,
this);
648 if (!w || !w->layoutNode)
return nullptr;
650 float absX = parentOffset.x + w->layoutNode->
computedLeft;
651 float absY = parentOffset.y + w->layoutNode->
computedTop;
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);
665 bool isInside = (checkPos.x >= absX && checkPos.x <= absX + width && checkPos.y >= absY && checkPos.y <= absY + height);
667 for (
auto it = w->children.rbegin(); it != w->children.rend(); ++it) {
670 return isInside ? w :
nullptr;
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;
682 if (w->text != txt) {
684 w->nodeJson[
"text"] = txt;
691 if (w->type == WidgetType::Button) w->onClick = std::move(cb);
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;
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();
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();
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);
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;
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;
733 std::vector<Widget*> navigable;
735 if (navigable.empty())
return;
737 bool focusedIsValid =
false;
738 if (m_focusedWidget) {
739 for (
Widget* w : navigable) {
740 if (w == m_focusedWidget) { focusedIsValid =
true;
break; }
746 glm::vec2 direction{dirX, dirY};
761 if (!w)
return false;
762 return w->type == WidgetType::Button;
766 if (!w || !w->layoutNode)
return {0.0f, 0.0f};
773 Widget* parent = w->parent;
777 parent = parent->parent;
779 return {x + width * 0.5f, y + height * 0.5f};
783 Widget* closest =
nullptr;
784 float closestScore = -FLT_MAX;
785 bool isHorizontalNav = std::abs(direction.x) > std::abs(direction.y);
787 for (
Widget* candidate : candidates) {
788 if (candidate == m_focusedWidget)
continue;
791 float distance = glm::length(toCandidate);
792 if (distance < 1.0f)
continue;
794 float dot = glm::dot(glm::normalize(direction), glm::normalize(toCandidate));
795 float crossAlignment = 1.0f;
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));
801 float score = dot * 3.0f + crossAlignment * 0.5f - (distance / 800.0f);
802 if (score > closestScore) { closestScore = score; closest = candidate; }
810 if (w == m_focusedWidget)
return;
811 if (m_focusedWidget && m_focusedWidget->onFocusLost) m_focusedWidget->onFocusLost();
813 if (m_focusedWidget && m_focusedWidget->onFocusEnter) m_focusedWidget->onFocusEnter();
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};
823 std::vector<std::string> lines =
wrapText(w->text, a, maxWidth);
825 float lineHeight = a.ascent - a.descent;
826 float totalHeight = lines.size() * lineHeight;
827 float totalWidth = 0.f;
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;
835 totalWidth = std::max(totalWidth, lineWidth);
837 return {totalWidth, totalHeight};
842 if (!w || !w->ui)
return;
844 float maxW = (width > 0.0f && width != FLT_MAX && !std::isnan(width)) ? width : FLT_MAX;
852 if (!w || !w->layoutNode)
return;
855 float y = parentOffset.y + w->layoutNode->
computedTop;
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);
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) {
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);
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});
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});
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);
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);
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()) {
903 float innerW = std::max(0.0f, width - bPadL - bPadR);
904 float innerH = std::max(0.0f, height - bPadT - bPadB);
906 std::vector<std::string> lines =
wrapText(w->text, a, innerW);
908 float lineHeight = a.ascent - a.descent;
909 float totalTextHeight = lines.size() * lineHeight;
910 float verticalOffset = (innerH - totalTextHeight) / 2.0f;
912 float cy = y + bPadT + verticalOffset + a.ascent;
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;
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);
926 for (
char ch : line) {
927 if (ch < 32 || ch > 127)
continue;
928 const stbtt_bakedchar& cd = a.cdata[ch - 32];
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));
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));
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});
954void VexUI::render(VkCommandBuffer cmd, VkPipeline pipeline, VkPipelineLayout pipelineLayout,
int currentFrame) {
957 glm::uvec2 currentRes = m_ctx.currentRenderResolution;
958 float currentPspMult = getPspMultiplier();
960 if (m_lastRenderRes != currentRes || m_lastPspMult != currentPspMult) {
961 m_lastRenderRes = currentRes;
962 m_lastPspMult = currentPspMult;
964 std::function<void(
Widget*)> syncTree = [&](
Widget* w) {
966 w->applyLayout(
this);
967 for (
auto* c : w->children) syncTree(c);
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);
975 m_fontAtlases.clear();
979 layout(m_ctx.currentRenderResolution);
980 vkDeviceWaitIdle(m_ctx.device);
982 std::vector<float> verts;
983 verts.reserve(1024 * 9);
984 batch(m_root, verts);
987 if (verts.empty())
return;
989 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
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);
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);
999 VkDeviceSize offset = 0;
1000 vkCmdBindVertexBuffers(cmd, 0, 1, &m_vb, &offset);
1002 uint32_t totalVertices = verts.size() / 9;
1003 uint32_t currentVertex = 0;
1004 int currentTexIndex = INT_MIN;
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, ¤tTexSet, 0,
nullptr);
1015 vkCmdDraw(cmd, 6, 1, currentVertex, 0);
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);
1030 for (
auto* c : w->children)
freeTree(c);
1036 w->nodeJson[
"rotation"] = degrees;
1037 w->applyLayout(
this);
1043 w->nodeJson[
"position"] =
"absolute";
1044 w->nodeJson[
"left"] = x.
toJson();
1045 w->nodeJson[
"top"] = y.
toJson();
1046 w->applyLayout(
this);
1052 w->nodeJson[
"size"] = {width.
toJson(), height.
toJson()};
1053 w->applyLayout(
this);
1060 w->nodeJson[
"image"] = path;
1067 w->nodeJson[
"style"][
"font"] = fontPath;
1068 w->nodeJson[
"style"][
"size"] = fontSize.
toJson();
1069 w->applyLayout(
this);
1076 w->nodeJson[
"style"][
"color"] = {color.r, color.g, color.b, color.a};
1077 w->applyLayout(
this);
1083 w->nodeJson[
"style"][
"bgColor"] = {color.r, color.g, color.b, color.a};
1084 w->applyLayout(
this);
1090 w->nodeJson[
"borderWidth"] = width.
toJson();
1091 w->nodeJson[
"borderColor"] = {color.r, color.g, color.b, color.a};
1092 w->applyLayout(
this);
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,...
VexUI(VulkanContext &ctx, VirtualFileSystem *vfs, VulkanResources *res, ResolutionManager *resMgr)
Constructor for VexUI.
void setColor(const std::string &id, glm::vec4 color)
Set text/tint color.
void setSize(const std::string &id, UIUnitValue w, UIUnitValue h)
Resize a widget.
void setRotation(const std::string &id, float degrees)
Set rotation of a widget in degrees.
void loadImages(Widget *w)
Loads images for the UI.
bool isWidgetNavigable(Widget *w) const
Check if widget is navigable (button type).
void batch(Widget *w, std::vector< float > &verts, glm::vec2 parentOffset={0.f, 0.f})
Batches the UI for rendering.
glm::vec2 calculateTextSize(Widget *w, float maxWidth=FLT_MAX)
Calculates the size of the text.
void setText(const std::string &id, const std::string &txt)
Set text of a UI element.
bool init()
Initialize the UI system, components needed later on.
void getNavigableWidgets(std::vector< Widget * > &out)
Get all navigable widgets (buttons).
void setPosition(const std::string &id, UIUnitValue x, UIUnitValue y)
Move a widget (sets Left/Top yoga properties). Works best with position: absolute,...
static void measureTextNode(LayoutNode *node, float width, float height)
Measures a text node.
Widget * findById(Widget *w, const std::string &id)
Finds a widget by its id.
void loadFonts(Widget *w)
Loads fonts for the UI.
void processEvent(const SDL_Event &ev)
Process mouse and keyboard events.
void safeUpdate(const std::string &id, std::function< void(Widget *)> action)
Updates a widget safely.
void setFont(const std::string &id, const std::string &fontPath, UIUnitValue fontSize)
Change font properties.
Widget * findClosestNavigableWidget(const glm::vec2 &fromPos, const glm::vec2 &direction, const std::vector< Widget * > &candidates)
Find closest navigable widget in a direction.
void setBorder(const std::string &id, UIUnitValue width, glm::vec4 color)
Set border properties.
void navigateToWidget(float dirX, float dirY)
Navigate to widget in a given direction.
void freeTree(Widget *w)
Frees the widget tree.
void setOnClick(const std::string &id, std::function< void()> cb)
Set on-click callback of a UI element.
Widget * parseNode(const nlohmann::json &j)
Parses a node from json to widgets.
void layout(glm::uvec2 res)
Layouts the UI using yoga.
void uploadVerts(const std::vector< float > &verts)
Uploads the vertex buffer to the GPU.
glm::vec2 getWidgetCenter(Widget *w)
Get widget center position in screen space.
std::vector< std::string > wrapText(const std::string &text, const FontAtlas &a, float maxWidth)
Wraps text to fit within a given width.
void setImage(const std::string &id, const std::string &path)
Change the image of an image widget.
void load(const std::string &path)
Load UI data from a JSON file.
Widget * findWidgetAt(Widget *w, glm::vec2 pos, glm::vec2 parentOffset={0.f, 0.f})
Finds a widget at a position.
void setBackgroundColor(const std::string &id, glm::vec4 color)
Set background color.
void render(VkCommandBuffer cmd, VkPipeline pipeline, VkPipelineLayout pipelineLayout, int currentFrame)
Render the UI. It is called by main rendering function.
void collectNavigableWidgets(Widget *w, std::vector< Widget * > &out)
Recursively collect navigable widgets.
void setFocusedWidget(Widget *w)
Set focus to a widget and trigger focus callbacks.
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.
std::string VEX_EXPORT GetAssetPath(const std::string &relativePath)
Resolves the absolute path of an asset based on the current build configuration.
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.
float resolve(float parent) const
Resolve the flex value to pixels based on parent dimension.
float resolveOr(float parent, float fallback) const
Resolve the flex value to pixels, with a fallback if unspecified.
Layout node representing a single UI element in the layout tree.
FlexValue paddingLeft
Left padding.
FlexValue paddingBottom
Bottom padding.
void insertChild(LayoutNode *child, size_t index)
Insert a child node at a specific index.
FlexValue height
Element height.
void * context
Opaque context pointer for user data.
FlexValue borderWidth
Border width.
std::vector< LayoutNode * > children
Child nodes.
Justify justifyContent
Justify content alignment.
float computedHeight
Computed height.
std::function< void(LayoutNode *, float, float)> measureFunc
Measure function for intrinsic sizing (e.g., text).
float flexGrow
Flex grow factor.
FlexValue width
Element width.
float computedWidth
Computed width.
float computedTop
Computed top position.
Align alignSelf
Align self override.
PositionType positionType
Position type (relative or absolute).
FlexDirection flexDirection
Direction of flex layout.
FlexValue paddingRight
Right padding.
float computedLeft
Computed left position.
FlexValue paddingTop
Top padding.
Align alignItems
Align items cross-axis alignment.
Push constants for UI rendering.
Struct storing unit value logic natively.
void parse(const nlohmann::json &jval)
Parses a json value into the unit struct.
nlohmann::json toJson() const
Converts the unit back to a json-compatible format (preserves strings like "px", "%").
float getPixels(const VexUI *ui) const
Dynamically calculates the raw pixel representation required for rendering.
Struct holding all vulkan data, like device, surface, swapchain, images, views, and more.
Represents an RGBA color value. Used mainly for fancy rendering in editor.