// 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(std::weak_ptr item) : UIComponentRenderable(item), lineHeight(1.0f), textAlign(UI_LABEL_TEXT_ALIGN_LEFT) { } void UILabel::onStart() { UIComponentRenderable::onStart(); this->shaderBuffer.init(); useEvent([&]{ // TODO: I believe I only need to rebuffer here if maxWidth is not -1 this->rebufferQuads(this->texts); }, eventAlignmentUpdated); useEffect([&]{ this->rebufferQuads(this->texts); }, lineHeight); useEvent([&]{ this->updateTextAlignments(); }, eventTextChanged); useEffect([&]{ this->updateTextAlignments(); }, this->textAlign); } void UILabel::updateTextAlignments() { struct FontShaderBufferData data; switch(this->textAlign) { case UI_LABEL_TEXT_ALIGN_LEFT: { auto itLine = lines.begin(); int32_t i = 0; while(itLine != lines.end()) { data.linePositions[i] = glm::vec2(0, 0); ++itLine; } break; } case UI_LABEL_TEXT_ALIGN_RIGHT: { float_t widthBased = this->getWidth(); auto itLine = lines.begin(); int32_t i = 0; while(itLine != lines.end()) { data.linePositions[i] = glm::vec2(widthBased - itLine->width, 0); ++itLine; } break; } case UI_LABEL_TEXT_ALIGN_CENTER: { float_t widthBased = this->getWidth(); auto itLine = lines.begin(); int32_t i = 0; while(itLine != lines.end()) { float_t x = (widthBased - itLine->width) / 2.0f; data.linePositions[i] = glm::vec2(x, 0); ++itLine; } break; } default: assertUnreachable("UILabel::updateTextAlignments: TextAlign invalid"); } this->shaderBuffer.buffer(&data, &data.linePositions); } std::vector UILabel::getUIRenderPasses() { if(this->textsBuffered.empty()) return {}; auto canvas = this->getCanvas(); auto shader = getGame()->renderManager->fontShader; // Translate glm::mat4 model = item.lock()->getWorldTransform(); 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("UILabel::getUIRenderPasses: Texture slot not implemented"); } item.textureSlots[it->second] = &it->first->texture; item.textureValues[param] = it->second; ++it; } std::vector items; items.push_back(item); if(this->hasDecorations) { struct ShaderPassItem itemDecorations = item; itemDecorations.mesh = &this->meshDecorations; items.push_back(itemDecorations); } return items; } 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, "UILabel::rebufferQuads: Too many parts (not supported)"); int32_t nextTexture = 0; glm::vec2 position(0, 0); int32_t partIndex = 0; std::vector> vertices; std::vector> decorations; struct FontShaderBufferData fontData; quadCountTotal = 0; quadCount = -1; std::vector realNewTexts; float_t maxWidth = this->width; float_t canvasScale = this->getCanvas()->getScale(); // Reset lines.clear(); textureMap.clear(); hasDecorations = false; // Determine font dimensions. auto itText = newTexts.begin(); while(itText != newTexts.end()) { position.y = mathMax(position.y, itText->style.size); ++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, "UILabel::rebufferQuads: Font cannot be null"); realText.lockId = text.style.font->lock(TrueTypeFaceTextureStyle{ (uint32_t)(text.style.size * canvasScale),// Scale for resolution. text.style.style }); assertTrue(realText.lockId != -1, "UILabel::rebufferQuads: Failed to lock font"); realText.texture = text.style.font->getTexture(realText.lockId); // Map texture if(textureMap.find(realText.texture) == textureMap.end()) { assertTrue(nextTexture < FONT_SHADER_TEXTURE_MAX, "UILabel::rebufferQuads: Too many textures (not supported)"); 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)); decorations.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, "UILabel::rebufferQuads: Character out of range"); assertTrue(ch != '\r', "UILabel::rebufferQuads: Character cannot be a carriage return"); assertTrue(ch != '\t', "UILabel::rebufferQuads: Character cannot be a tab"); assertTrue(ch != '\n', "UILabel::rebufferQuads: Character cannot be a newline"); // Get font data. auto charInfo = realText.texture->getCharacterData(ch); // Now we scale down the char info here. This is because we fetch the // texture of the font based on the canvas scale, but the sizes that we // render out need to be shrunk to match the original sizes. glm::vec2 charSize = glm::vec2( charInfo.bitmapSize.x / canvasScale, charInfo.bitmapSize.y / canvasScale ); glm::vec2 charAdvance = glm::vec2( charInfo.advanceX / canvasScale, charInfo.advanceY / canvasScale ); glm::vec2 charPos = glm::vec2( charInfo.bitmapPosition.x / canvasScale, charInfo.bitmapPosition.y / canvasScale ); // Word wrapping if( ch != ' ' && lastSpaceCharacter != -1 && maxWidth > charSize.x && (position.x + charAdvance.x) > 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(); decorations.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, use original (non scaled) values. 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 + charPos.x; vert.y = position.y + charPos.y; vert.w = vert.x + charSize.x; vert.z = vert.y + charSize.y; vertices.push_back(std::make_pair(vert, uvs)); // Decorations if(text.style.decorations != 0) { auto charInfo2 = realText.texture->getCharacterData('-'); uvs.y = charInfo2.textureY / wh.y; uvs.w = charInfo2.bitmapSize.x / wh.x; uvs.z = uvs.y + (charInfo2.bitmapSize.y / wh.y); vert.x = position.x + charInfo2.bitmapPosition.x; vert.y = position.y + charInfo2.bitmapPosition.y; vert.w = vert.x + charInfo.advanceX; vert.z = vert.y + charInfo2.bitmapSize.y; decorations.push_back(std::make_pair(vert, uvs)); hasDecorations = true; // TODO: Finish } else { uvs = glm::vec4(0, 0, 1, 1); vert = glm::vec4(0, 0, 0, 0); decorations.push_back(std::make_pair(vert, uvs)); } // Move the current position along. position.x += charAdvance.x; position.y += charAdvance.y; // Update the continuous dimensions if(ch == ' ') { lineWidth += wordWidth; lineWidth += charAdvance.x; wordWidth = 0.0f; } else { wordWidth += charAdvance.x; } // 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. bool_t isLast = itText == (newTexts.end() - 1); if(isLast) 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() ); if(hasDecorations) { assertTrue(vertices.size() == decorations.size(), "UILabel::rebufferQuads: Decoration count mismatch"); this->meshDecorations.createBuffers( QUAD_VERTICE_COUNT * decorations.size(), QUAD_INDICE_COUNT * decorations.size() ); } } // Now buffer the quads. Can be optimized probably. 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; } // Now buffer decorations if(hasDecorations) { j = 0; itQuad = decorations.begin(); while(itQuad != decorations.end()) { auto vert = itQuad->first; auto uvs = itQuad->second; QuadMesh::bufferQuadMeshWithZ(this->meshDecorations, 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, "UILabel::rebufferQuads: Lock ID cannot be -1"); assertNotNull(itText->style.font, "UILabel::rebufferQuads: Font cannot be null"); itText->style.font->unlock(itText->lockId); ++itText; } // Update textsBuffered = realNewTexts; texts = newTexts; this->ignoreAlignmentUpdate = true; this->alignmentNeedsUpdating = true; // Event this->eventTextChanged.invoke(); }