// Copyright (c) 2023 Dominic Masters // // This software is released under the MIT License. // https://opensource.org/licenses/MIT #include "UILabel.hpp" #include "game/DawnGame.hpp" using namespace Dawn; UILabel::UILabel(SceneItem *item) : UIComponentRenderable(item), lineHeight(1.0f) { } void UILabel::onStart() { UIComponentRenderable::onStart(); this->shaderBuffer.init(); useEvent([&]{ this->rebufferQuads(this->texts); }, eventAlignmentUpdated); useEffect([&]{ this->rebufferQuads(this->texts); }, lineHeight); } std::vector UILabel::getUIRenderPasses() { if(this->textsBuffered.empty()) return {}; auto canvas = this->getCanvas(); auto shader = getGame()->renderManager.fontShader; glm::mat4 model = transform->getWorldTransform(); // Translate model = glm::translate(model, glm::vec3(this->textOffset, 0.0f)); struct ShaderPassItem item; item.shader = shader; item.mesh = &this->mesh; item.matrixValues[shader->paramModel] = model; item.parameterBuffers[shader->bufferUiCanvas] = &canvas->shaderBuffer; item.parameterBuffers[shader->bufferFont] = &this->shaderBuffer; item.renderFlags = RENDER_MANAGER_RENDER_FLAG_BLEND; item.start = quadStart * QUAD_INDICE_COUNT; item.count = quadCount == -1 ? -1 : quadCount * QUAD_INDICE_COUNT; // Map texture slots auto it = textureMap.begin(); while(it != textureMap.end()) { shaderparameter_t param; switch(it->second) { case 0: param = shader->paramTexture0; break; case 1: param = shader->paramTexture1; break; case 2: param = shader->paramTexture2; break; case 3: param = shader->paramTexture3; break; default: assertUnreachable(); } item.textureSlots[it->second] = &it->first->texture; item.textureValues[param] = it->second; ++it; } return { item }; } float_t UILabel::getContentWidth() { float_t w = 0; auto it = lines.begin(); while(it != lines.end()) { w = mathMax(w, it->width); ++it; } return w; } float_t UILabel::getContentHeight() { float_t h = 0; auto it = lines.begin(); while(it != lines.end()) { h += it->height; ++it; } return h; } void UILabel::rebufferQuads(const std::vector newTexts) { if(this->ignoreAlignmentUpdate) { this->ignoreAlignmentUpdate = false; return; } assertTrue(newTexts.size() <= FONT_SHADER_PARTS_MAX); int32_t nextTexture = 0; glm::vec2 position(0, 0); int32_t partIndex = 0; std::vector> vertices; struct FontShaderBufferData fontData; quadCountTotal = 0; std::vector realNewTexts; float_t maxWidth = this->width; // Reset lines.clear(); // Determine font dimensions. auto itText = newTexts.begin(); while(itText != newTexts.end()) { position.y = mathMax(position.y, itText->style.size/* this->lineHeight - THIS PART WOULD TAKE THE LINE HEIGHT INTO CONSIDERATION ON THE FIRST/INITIAL LINE */); ++itText; } // Prepare values shared across all text parts/styles float_t lineWidth = 0; struct UILabelLine currentLine; currentLine.quadStart = quadCountTotal; currentLine.position = position; // Now generate quads itText = newTexts.begin(); while(itText != newTexts.end()) { auto text = *itText; struct UILabelText realText; // Clone values realText.style = text.style; // Lock the font assertNotNull(text.style.font); realText.lockId = text.style.font->lock(TrueTypeFaceTextureStyle{ text.style.size, text.style.style }); assertTrue(realText.lockId != -1); realText.texture = text.style.font->getTexture(realText.lockId); // Map texture if(textureMap.find(realText.texture) == textureMap.end()) { assertTrue(nextTexture < FONT_SHADER_TEXTURE_MAX); textureMap[realText.texture] = nextTexture++; } // Buffer shader values fontData.textures[partIndex].value = textureMap[realText.texture]; fontData.colors[partIndex] = realText.style.color; // Get some texture info glm::vec2 wh = glm::vec2( realText.texture->texture.getWidth(), realText.texture->texture.getHeight() ); // Prepare loop properties and shorthands auto len = text.text.length(); float_t wordWidth = 0; int32_t lastSpaceCharacter = -1; currentLine.height = mathMax(currentLine.height, realText.style.size); std::function fnInsertNewline = [&](int32_t i){ if(i != len) { // Update text. realText.text += '\n'; // Insert dummy quad glm::vec4 uvs(0, 0, 1, 1); glm::vec4 vert(0, 0, 0, 0); vertices.push_back(std::make_pair(vert, uvs)); fontData.quadMappings[quadCountTotal].value = partIndex; quadCountTotal++; } // Finalize current line lineWidth += wordWidth; currentLine.width = lineWidth; currentLine.quadCount += quadCountTotal - currentLine.quadStart; // Move to next line if(i != len) { position.x = 0; position.y += realText.style.size * this->lineHeight; lines.push_back(currentLine); } // Reset line lastSpaceCharacter = i; wordWidth = 0.0f; if(i != len) lineWidth = 0.0f; if(i != len) { currentLine = UILabelLine(); currentLine.quadStart = quadCountTotal; currentLine.position = position; currentLine.height = realText.style.size * this->lineHeight; // Here I subtract line height from the line position because we start // by moving the text down by the initial line height, so we need to // compensate for that. currentLine.position.y -= realText.style.size * this->lineHeight; } }; // Now, iterate each character for(int32_t i = 0; i < len; i++) { std::u32string::value_type ch = text.text[i]; // FT_ULong ch = text.text[i]; char c = text.text[i]; // Handle special characters if(ch == '\n') { fnInsertNewline(i); continue; } else if(ch == ' ') { lastSpaceCharacter = i; } // Validate characters assertTrue(ch >= TRUE_TYPE_CHAR_BEGIN && ch < TRUE_TYPE_CHAR_END); assertTrue(ch != '\r'); assertTrue(ch != '\t'); assertTrue(ch != '\n'); // Get font data. auto charInfo = realText.texture->getCharacterData(ch); // Word wrapping if( ch != ' ' && lastSpaceCharacter != -1 && maxWidth > charInfo.bitmapSize.x && (position.x + charInfo.advanceX) > maxWidth ) { // Basically this rewinds everything we've done to the last space char, // changes it to a newline, and then moves the position along. int32_t diff = i - lastSpaceCharacter; for(int32_t k = 0; k < diff; k++) vertices.pop_back(); text.text[lastSpaceCharacter] = '\n'; i = lastSpaceCharacter; lastSpaceCharacter = -1; quadCountTotal -= diff; // Now we've rewound to the space, treat it like a newline instead. fnInsertNewline(i); continue; } // Buffer coordinates glm::vec4 uvs; uvs.x = 0.0f; uvs.y = charInfo.textureY / wh.y; uvs.w = charInfo.bitmapSize.x / wh.x; uvs.z = uvs.y + (charInfo.bitmapSize.y / wh.y); glm::vec4 vert; vert.x = position.x + charInfo.bitmapPosition.x; vert.y = position.y + charInfo.bitmapPosition.y; vert.w = vert.x + charInfo.bitmapSize.x; vert.z = vert.y + charInfo.bitmapSize.y; vertices.push_back(std::make_pair(vert, uvs)); // Move the current position along. position.x += charInfo.advanceX; position.y += charInfo.advanceY; // Update the continuous dimensions if(ch == ' ') { lineWidth += wordWidth; lineWidth += charInfo.advanceX; wordWidth = 0.0f; } else { wordWidth += charInfo.advanceX; } // Set the part index to the quad mappings fontData.quadMappings[quadCountTotal].value = partIndex; quadCountTotal++; realText.text += ch; } // Now we insert a line. We do this because there is no newline at the end // of the text, so we need to insert the last line manually. fnInsertNewline(len); // Next ++partIndex; ++itText; realNewTexts.push_back(realText); } lines.push_back(currentLine); // Create mesh if(!vertices.empty()) { this->mesh.createBuffers( QUAD_VERTICE_COUNT * vertices.size(), QUAD_INDICE_COUNT * vertices.size() ); } // Now buffer the quads. int32_t j = 0; auto itQuad = vertices.begin(); while(itQuad != vertices.end()) { auto vert = itQuad->first; auto uvs = itQuad->second; QuadMesh::bufferQuadMeshWithZ(&this->mesh, glm::vec2(vert.x, vert.y), glm::vec2(uvs.x, uvs.y), glm::vec2(vert.w, vert.z), glm::vec2(uvs.w, uvs.z), 0.0f, j * QUAD_VERTICE_COUNT, j * QUAD_INDICE_COUNT ); j++; ++itQuad; } // Buffer data shaderBuffer.buffer(&fontData); // Finally, release the old locks itText = textsBuffered.begin(); while(itText != textsBuffered.end()) { assertTrue(itText->lockId != -1); assertNotNull(itText->style.font); itText->style.font->unlock(itText->lockId); ++itText; } // Update textsBuffered = realNewTexts; texts = newTexts; this->ignoreAlignmentUpdate = true; this->alignmentNeedsUpdating = true; // Event this->eventTextChanged.invoke(); }