426 lines
12 KiB
C++
426 lines
12 KiB
C++
// 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<struct ShaderPassItem> UILabel::getUIRenderPasses() {
|
|
if(this->textsBuffered.empty()) return {};
|
|
|
|
auto canvas = this->getCanvas();
|
|
auto shader = getGame()->renderManager.fontShader;
|
|
|
|
// Translate
|
|
glm::mat4 model = transform->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<struct ShaderPassItem> 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<float_t>(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<struct UILabelText> 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<std::pair<glm::vec4, glm::vec4>> vertices;
|
|
std::vector<std::pair<glm::vec4, glm::vec4>> decorations;
|
|
struct FontShaderBufferData fontData;
|
|
quadCountTotal = 0;
|
|
quadCount = -1;
|
|
std::vector<struct UILabelText> realNewTexts;
|
|
|
|
float_t maxWidth = this->width;
|
|
|
|
// Reset
|
|
lines.clear();
|
|
textureMap.clear();
|
|
hasDecorations = false;
|
|
|
|
// Determine font dimensions.
|
|
auto itText = newTexts.begin();
|
|
while(itText != newTexts.end()) {
|
|
position.y = mathMax<float_t>(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, "UILabel::rebufferQuads: Font cannot be null");
|
|
realText.lockId = text.style.font->lock(TrueTypeFaceTextureStyle{
|
|
text.style.size,
|
|
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<float_t>(currentLine.height, realText.style.size);
|
|
|
|
std::function<void(int32_t)> 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);
|
|
|
|
// 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();
|
|
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
|
|
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));
|
|
|
|
// 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 += 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.
|
|
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();
|
|
} |