15#include <unordered_set>
19#include <spdlog/spdlog.h>
54 std::vector<std::string> splitLines(
const std::string& text)
56 std::vector<std::string> lines;
57 std::stringstream ss(text);
59 while (std::getline(ss, line,
'\n')) {
60 lines.push_back(line);
62 if (text.empty() || text.back() ==
'\n') {
68 float lineWidthForText(
const std::string& text,
const FontResource* font,
const float scale)
70 if (!font || text.empty()) {
77 const int code =
static_cast<unsigned char>(c);
78 const auto it = font->glyphs.find(code);
79 const float advance = it != font->glyphs.end() ? it->second.xadvance * scale : (font->lineHeight * 0.35f * scale);
80 const float kern = prev >= 0 ? font->kerningValue(prev, code) * scale : 0.0f;
81 width += kern + advance;
87 std::vector<std::string> wrapLine(
const std::string& line,
const FontResource* font,
const float scale,
const float maxWidth)
89 if (!font || maxWidth <= 0.0f || line.empty()) {
93 std::vector<std::string> out;
96 while (i < line.size()) {
98 while (j < line.size() && line[j] !=
' ') {
101 const std::string word = line.substr(i, j - i);
102 const bool hasSpace = (j < line.size() && line[j] ==
' ');
103 const std::string token = hasSpace ? (word +
" ") : word;
105 const std::string candidate = current + token;
106 if (!current.empty() && lineWidthForText(candidate, font, scale) > maxWidth) {
107 out.push_back(current);
111 if (current.empty() && lineWidthForText(token, font, scale) > maxWidth) {
114 for (
char c : token) {
115 const std::string next = part + c;
116 if (!part.empty() && lineWidthForText(next, font, scale) > maxWidth) {
127 i = hasSpace ? (j + 1) : j;
130 if (!current.empty()) {
131 out.push_back(current);
136 std::shared_ptr<Mesh> buildTextMesh(
const std::shared_ptr<GraphicsDevice>& gd,
const ElementComponent* element)
138 if (!gd || !element || !element->fontResource() || element->text().empty()) {
143 const float lineHeight = std::max(font->lineHeight, 1.0f);
144 const float scale =
static_cast<float>(element->fontSize()) / lineHeight;
146 std::vector<std::string> lines = splitLines(element->text());
147 if (element->wrapLines()) {
148 std::vector<std::string> wrapped;
149 wrapped.reserve(lines.size());
150 for (
const auto& l : lines) {
151 auto sub = wrapLine(l, font, scale, element->width());
152 wrapped.insert(wrapped.end(), sub.begin(), sub.end());
154 lines = std::move(wrapped);
157 std::vector<float> vertices;
158 std::vector<uint32_t> indices;
159 vertices.reserve(lines.size() * 64u * 14u);
160 indices.reserve(lines.size() * 64u * 6u);
162 const float boxW = element->width();
163 const float boxH = element->height();
164 const Vector2 pivot = element->pivot();
165 const float lineStep = lineHeight * scale;
167 float yTop = (1.0f - pivot.y) * boxH;
170 for (
size_t li = 0; li < lines.size(); ++li) {
171 const std::string& line = lines[li];
173 float lineWidth = 0.0f;
174 int prevForWidth = -1;
175 for (
char c : line) {
176 const int code =
static_cast<unsigned char>(c);
177 if (
const auto it = font->glyphs.find(code); it != font->glyphs.end()) {
178 lineWidth += (prevForWidth >= 0 ? font->kerningValue(prevForWidth, code) * scale : 0.0f);
179 lineWidth += it->second.xadvance * scale;
184 float x = -pivot.x * boxW;
186 x += (boxW - lineWidth) * 0.5f;
188 x += (boxW - lineWidth);
192 for (
char c : line) {
193 const int code =
static_cast<unsigned char>(c);
194 const auto gIt = font->glyphs.find(code);
195 if (gIt == font->glyphs.end()) {
196 x += lineHeight * 0.35f * scale;
202 x += (prev >= 0 ? font->kerningValue(prev, code) * scale : 0.0f);
204 const float gx0 = x + g.
xoffset * scale;
205 const float gyTop = yTop -
static_cast<float>(li) * lineStep - g.yoffset * scale;
206 const float gx1 = gx0 + g.width * scale;
207 const float gyBot = gyTop - g.height * scale;
209 const float atlasW =
static_cast<float>(std::max(font->atlasWidth, 1));
210 const float atlasH =
static_cast<float>(std::max(font->atlasHeight, 1));
211 const float u0 = g.x / atlasW;
212 const float u1 = (g.x + g.width) / atlasW;
214 const float v0 = g.y / atlasH;
215 const float v1 = (g.y + g.height) / atlasH;
218 const std::array<float, 56> quadVerts = {
219 gx0, gyTop, 0.0f, 0.0f, 0.0f, 1.0f, u0, v0, 1.0f,0.0f,0.0f,1.0f, u0, v0,
220 gx1, gyTop, 0.0f, 0.0f, 0.0f, 1.0f, u1, v0, 1.0f,0.0f,0.0f,1.0f, u1, v0,
221 gx1, gyBot, 0.0f, 0.0f, 0.0f, 1.0f, u1, v1, 1.0f,0.0f,0.0f,1.0f, u1, v1,
222 gx0, gyBot, 0.0f, 0.0f, 0.0f, 1.0f, u0, v1, 1.0f,0.0f,0.0f,1.0f, u0, v1
224 vertices.insert(vertices.end(), quadVerts.begin(), quadVerts.end());
226 indices.insert(indices.end(), {vbase + 0u, vbase + 2u, vbase + 1u, vbase + 0u, vbase + 3u, vbase + 2u});
229 x += g.xadvance * scale;
234 if (vertices.empty() || indices.empty()) {
238 const int vertexCount =
static_cast<int>(vertices.size() / 14u);
239 std::vector<uint8_t> vbData(vertices.size() *
sizeof(
float));
240 std::memcpy(vbData.data(), vertices.data(), vbData.size());
242 vbOpts.
data = std::move(vbData);
243 auto vertexFormat = std::make_shared<VertexFormat>(14 *
static_cast<int>(
sizeof(
float)),
true,
false);
244 auto vb = gd->createVertexBuffer(vertexFormat, vertexCount, vbOpts);
246 std::vector<uint8_t> ibData(indices.size() *
sizeof(uint32_t));
247 std::memcpy(ibData.data(), indices.data(), ibData.size());
248 auto ib = gd->createIndexBuffer(
INDEXFORMAT_UINT32,
static_cast<int>(indices.size()), ibData);
250 auto mesh = std::make_shared<Mesh>();
251 mesh->setVertexBuffer(vb);
252 mesh->setIndexBuffer(ib, 0);
256 prim.count =
static_cast<int>(indices.size());
258 mesh->setPrimitive(prim, 0);
262 bounds.setHalfExtents(
Vector3(std::max(boxW * 0.5f, 1.0f), std::max(boxH * 0.5f, 1.0f), 1.0f));
263 mesh->setAabb(bounds);
270 for (
auto& [_, visual] : _textVisuals) {
272 visual.entity->remove();
275 _textVisuals.clear();
277 _sdlRenderer =
nullptr;
280 bool ElementInput::computeElementRect(
const ElementComponent* element, SDL_FRect& outRect)
const
287 const float width = std::max(element->
width(), 0.0f);
288 const float height = std::max(element->
height(), 0.0f);
289 const Vector2 pivot = element->
pivot();
291 outRect.
x = pos.getX() - pivot.x * width;
292 outRect.y = pos.getY() - pivot.y * height;
295 return outRect.w > 0.0f && outRect.h > 0.0f;
302 for (
auto it = elements.rbegin(); it != elements.rend(); ++it) {
309 if (!computeElementRect(element, rect)) {
313 if (x < rect.x || y < rect.y || x > rect.x + rect.w || y > rect.y + rect.h) {
318 element->
fire(
"click", x, y);
320 button->fire(
"click", x, y);
335 if (_engine && _engine->sdlWindow()) {
336 SDL_GetWindowSize(_engine->sdlWindow(), &windowW, &windowH);
340 if (screen && screen->enabled()) {
341 screen->updateScaleFromWindow(windowW, windowH);
345 SDL_SetRenderDrawBlendMode(_sdlRenderer, SDL_BLENDMODE_BLEND);
353 if (!computeElementRect(element, rect)) {
358 const int alpha =
static_cast<int>(std::round(std::clamp(element->
opacity() * c.
a, 0.0f, 1.0f) * 255.0f));
359 const Uint8 r =
static_cast<Uint8
>(std::round(std::clamp(c.
r, 0.0f, 1.0f) * 255.0f));
360 const Uint8 g =
static_cast<Uint8
>(std::round(std::clamp(c.
g, 0.0f, 1.0f) * 255.0f));
361 const Uint8 b =
static_cast<Uint8
>(std::round(std::clamp(c.
b, 0.0f, 1.0f) * 255.0f));
362 const Uint8 a =
static_cast<Uint8
>(std::clamp(alpha, 0, 255));
365 SDL_SetRenderDrawColor(_sdlRenderer, r, g, b, a);
366 SDL_RenderFillRect(_sdlRenderer, &rect);
373 if (!_engine || !_engine->graphicsDevice()) {
379 if (_engine->sdlWindow()) {
380 SDL_GetWindowSize(_engine->sdlWindow(), &windowW, &windowH);
382 float uiWidth =
static_cast<float>(std::max(windowW, 1));
383 float uiHeight =
static_cast<float>(std::max(windowH, 1));
385 if (!screen || !screen->enabled() || !screen->screenSpace()) {
388 const float scale = std::max(screen->scale(), 1e-6f);
389 uiWidth = screen->resolution().x / scale;
390 uiHeight = screen->resolution().y / scale;
394 for (
auto& [_, visual] : _textVisuals) {
395 visual.activeFrame =
false;
406 auto& visual = _textVisuals[element];
407 visual.activeFrame =
true;
409 if (!visual.entity) {
410 visual.entity =
new Entity();
411 visual.entity->setEngine(_engine.get());
412 visual.entity->setLocalPosition(0.0f, 0.0f, 5.0f);
417 visual.
material = std::make_shared<StandardMaterial>();
418 visual.material->setUseLighting(
false);
419 visual.material->setUseSkybox(
false);
420 visual.material->setTransparent(
true);
422 visual.material->setDiffuse(
Color(1.0f, 1.0f, 1.0f, 1.0f));
423 visual.material->setEmissive(
Color(1.0f, 1.0f, 1.0f, 1.0f));
425 visual.material->setBlendState(alphaBlend);
427 textDepth->setDepthTest(
false);
428 visual.material->setDepthState(textDepth);
432 visual.render->setMaterial(visual.material.get());
434 _engine->root()->addChild(visual.entity);
437 const bool needsRebuild = element->
textDirty() ||
438 visual.cachedText != element->
text() ||
439 visual.cachedFontSize != element->
fontSize() ||
441 visual.cachedWrap != element->
wrapLines() ||
443 std::abs(visual.cachedPivot.x - element->
pivot().
x) > 1e-4f ||
444 std::abs(visual.cachedPivot.y - element->
pivot().
y) > 1e-4f ||
445 std::abs(visual.cachedWidth - element->
width()) > 1e-4f ||
446 std::abs(visual.cachedHeight - element->
height()) > 1e-4f;
449 visual.mesh = buildTextMesh(_engine->graphicsDevice(), element);
451 auto& mi = visual.render->meshInstances();
452 for (
auto* entry : mi) {
459 mi.push_back(
new MeshInstance(visual.mesh.get(), visual.material.get(), visual.entity));
462 visual.cachedText = element->
text();
463 visual.cachedFontSize = element->
fontSize();
464 visual.cachedWidth = element->
width();
465 visual.cachedHeight = element->
height();
466 visual.cachedPivot = element->
pivot();
468 visual.cachedWrap = element->
wrapLines();
473 static std::unordered_set<const ElementComponent*> logged;
474 if (logged.find(element) == logged.end()) {
476 spdlog::info(
"UI text element '{}': glyphs={}, meshBuilt={}, size=({}, {}), worldPos=({}, {}, {})",
479 visual.mesh ?
"true" :
"false",
485 logged.insert(element);
489 visual.material->setDiffuse(c);
490 visual.material->setEmissive(c);
491 visual.material->setOpacity(element->
opacity());
495 const float worldX = pos.
getX() - uiWidth * 0.5f;
496 const float worldY = uiHeight * 0.5f - pos.
getY();
497 visual.entity->setLocalPosition(worldX, worldY, 5.0f);
502 std::vector<ElementComponent*> toRemove;
503 toRemove.reserve(_textVisuals.size());
504 for (
auto& [element, visual] : _textVisuals) {
505 if (!visual.activeFrame) {
507 visual.entity->remove();
509 toRemove.push_back(element);
512 for (
auto* element : toRemove) {
513 _textVisuals.erase(element);
static BlendState alphaBlend()
Axis-Aligned Bounding Box defined by center and half-extents.
void setCenter(const Vector3 ¢er)
virtual bool enabled() const
static DepthState noWrite()
FontResource * fontResource() const
static const std::vector< ElementComponent * > & instances()
ElementHorizontalAlign horizontalAlign() const
const std::string & text() const
const Color & color() const
const Vector2 & pivot() const
ECS entity — a GraphNode that hosts components defining its behavior.
EventHandler * fire(const std::string &name, Args &&... args)
Renderable instance of a Mesh with its own material, transform node, and optional GPU instancing.
Material * material() const
static const std::vector< ScreenComponent * > & instances()
RGBA color with floating-point components in [0, 1].
std::unordered_map< int, FontGlyph > glyphs
Describes how vertex and index data should be interpreted for a draw call.
2D vector for UV coordinates, screen positions, and 2D math.
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
std::vector< uint8_t > data