// SPDX-License-Identifier: MIT // // ES-DE Frontend // Font.h // // Loading, unloading, caching and rendering of fonts. // Also functions for text wrapping and similar. // #include "resources/Font.h" #include "Log.h" #include "renderers/Renderer.h" #include "utils/FileSystemUtil.h" #include "utils/PlatformUtil.h" #include "utils/StringUtil.h" Font::Font(float size, const std::string& path) : mRenderer {Renderer::getInstance()} , mPath(path) , mFontSize {size} , mLetterHeight {0.0f} , mMaxGlyphHeight {static_cast(std::round(size))} { if (mFontSize < 3.0f) { mFontSize = 3.0f; LOG(LogWarning) << "Requested font size too small, changing to minimum supported size"; } else if (mFontSize > Renderer::getScreenHeight() * 1.5f) { mFontSize = Renderer::getScreenHeight() * 1.5f; LOG(LogWarning) << "Requested font size too large, changing to maximum supported size"; } if (!sLibrary) initLibrary(); // Always initialize ASCII characters. for (unsigned int i {32}; i < 127; ++i) getGlyph(i); clearFaceCache(); } Font::~Font() { unload(ResourceManager::getInstance()); auto fontEntry = sFontMap.find(std::tuple(mFontSize, mPath)); if (fontEntry != sFontMap.cend()) sFontMap.erase(fontEntry); if (sFontMap.empty() && sLibrary) { FT_Done_FreeType(sLibrary); sLibrary = nullptr; } } std::shared_ptr Font::get(float size, const std::string& path) { const std::string canonicalPath {Utils::FileSystem::getCanonicalPath(path)}; const std::tuple def {size, canonicalPath.empty() ? getDefaultPath() : canonicalPath}; auto foundFont = sFontMap.find(def); if (foundFont != sFontMap.cend()) { if (!foundFont->second.expired()) return foundFont->second.lock(); } std::shared_ptr font {new Font(std::get<0>(def), std::get<1>(def))}; sFontMap[def] = std::weak_ptr(font); ResourceManager::getInstance().addReloadable(font); return font; } glm::vec2 Font::sizeText(std::string text, float lineSpacing) { const float lineHeight {getHeight(lineSpacing)}; float lineWidth {0.0f}; float highestWidth {0.0f}; float y {lineHeight}; size_t i {0}; while (i < text.length()) { unsigned int character {Utils::String::chars2Unicode(text, i)}; // Advances i. if (character == '\n') { if (lineWidth > highestWidth) highestWidth = lineWidth; lineWidth = 0.0f; y += lineHeight; } Glyph* glyph {getGlyph(character)}; if (glyph) lineWidth += glyph->advance.x; } if (lineWidth > highestWidth) highestWidth = lineWidth; return glm::vec2 {highestWidth, y}; } int Font::loadGlyphs(const std::string& text) { mMaxGlyphHeight = static_cast(std::round(mFontSize)); for (size_t i {0}; i < text.length();) { unsigned int character {Utils::String::chars2Unicode(text, i)}; // Advances i. Glyph* glyph {getGlyph(character)}; if (glyph->rows > mMaxGlyphHeight) mMaxGlyphHeight = glyph->rows; } return mMaxGlyphHeight; } TextCache* Font::buildTextCache(const std::string& text, float offsetX, float offsetY, unsigned int color, float lineSpacing, bool noTopMargin) { return buildTextCache(text, glm::vec2 {offsetX, offsetY}, color, 0.0f, ALIGN_LEFT, lineSpacing, noTopMargin); } TextCache* Font::buildTextCache(const std::string& text, glm::vec2 offset, unsigned int color, float xLen, Alignment alignment, float lineSpacing, bool noTopMargin) { float x {offset.x + (xLen != 0 ? getNewlineStartOffset(text, 0, xLen, alignment) : 0)}; int yTop {0}; float yBot {0.0f}; if (noTopMargin) { yTop = 0; yBot = getHeight(1.5); } else { yTop = getGlyph('S')->bearing.y; yBot = getHeight(lineSpacing); } float y {offset.y + ((yBot + yTop) / 2.0f)}; // Vertices by texture. std::map> vertMap; size_t cursor {0}; while (cursor < text.length()) { // Also advances cursor. unsigned int character {Utils::String::chars2Unicode(text, cursor)}; Glyph* glyph {nullptr}; // Invalid character. if (character == 0) continue; if (character == '\n') { y += getHeight(lineSpacing); x = offset[0] + (xLen != 0 ? getNewlineStartOffset(text, static_cast( cursor) /* cursor is already advanced */, xLen, alignment) : 0); continue; } glyph = getGlyph(character); if (glyph == nullptr) continue; std::vector& verts {vertMap[glyph->texture]}; size_t oldVertSize {verts.size()}; verts.resize(oldVertSize + 6); Renderer::Vertex* vertices {verts.data() + oldVertSize}; const float glyphStartX {x + glyph->bearing.x}; const glm::ivec2& textureSize {glyph->texture->textureSize}; vertices[1] = { {glyphStartX, y - glyph->bearing.y}, {glyph->texPos.x, glyph->texPos.y}, color}; vertices[2] = {{glyphStartX, y - glyph->bearing.y + (glyph->texSize.y * textureSize.y)}, {glyph->texPos.x, glyph->texPos.y + glyph->texSize.y}, color}; vertices[3] = {{glyphStartX + glyph->texSize.x * textureSize.x, y - glyph->bearing.y}, {glyph->texPos.x + glyph->texSize.x, glyph->texPos.y}, color}; vertices[4] = {{glyphStartX + glyph->texSize.x * textureSize.x, y - glyph->bearing.y + (glyph->texSize.y * textureSize.y)}, {glyph->texPos.x + glyph->texSize.x, glyph->texPos.y + glyph->texSize.y}, color}; // Round vertices. for (int i {1}; i < 5; ++i) vertices[i].position = glm::round(vertices[i].position); // Make duplicates of first and last vertex so this can be rendered as a triangle strip. vertices[0] = vertices[1]; vertices[5] = vertices[4]; // Advance. x += glyph->advance.x; } TextCache* cache {new TextCache()}; cache->vertexLists.resize(vertMap.size()); cache->metrics.size = {sizeText(text, lineSpacing)}; cache->metrics.maxGlyphHeight = mMaxGlyphHeight; cache->clipRegion = {0.0f, 0.0f, 0.0f, 0.0f}; size_t i {0}; for (auto it = vertMap.cbegin(); it != vertMap.cend(); ++it) { TextCache::VertexList& vertList {cache->vertexLists.at(i)}; vertList.textureIdPtr = &it->first->textureId; vertList.verts = it->second; ++i; } clearFaceCache(); return cache; } void Font::renderTextCache(TextCache* cache) { if (cache == nullptr) { LOG(LogError) << "Attempted to draw nullptr TextCache"; return; } const bool clipRegion {cache->clipRegion != glm::vec4 {0.0f, 0.0f, 0.0f, 0.0f}}; for (auto it = cache->vertexLists.begin(); it != cache->vertexLists.end(); ++it) { assert(*it->textureIdPtr != 0); it->verts[0].shaderFlags = Renderer::ShaderFlags::FONT_TEXTURE; if (clipRegion) { it->verts[0].shaderFlags |= Renderer::ShaderFlags::CLIPPING; it->verts[0].clipRegion = cache->clipRegion; } mRenderer->bindTexture(*it->textureIdPtr, 0); mRenderer->drawTriangleStrips( &it->verts[0], static_cast(it->verts.size()), Renderer::BlendFactor::SRC_ALPHA, Renderer::BlendFactor::ONE_MINUS_SRC_ALPHA); } } std::string Font::wrapText(const std::string& text, const float maxLength, const float maxHeight, const float lineSpacing, const bool multiLine) { assert(maxLength > 0.0f); const float lineHeight {getHeight(lineSpacing)}; const float dotsWidth {sizeText("...").x}; float accumHeight {lineHeight}; float lineWidth {0.0f}; float charWidth {0.0f}; float lastSpacePos {0.0f}; unsigned int charID {0}; size_t cursor {0}; size_t lastSpace {0}; size_t spaceAccum {0}; size_t byteCount {0}; std::string wrappedText; std::string charEntry; std::vector> dotsSection; bool addDots {false}; for (size_t i {0}; i < text.length(); ++i) { if (text[i] == '\n') { if (!multiLine) { addDots = true; break; } accumHeight += lineHeight; if (maxHeight != 0.0f && accumHeight > maxHeight) { addDots = true; break; } wrappedText.append("\n"); lineWidth = 0.0f; lastSpace = 0; continue; } cursor = i; // Needed to handle multi-byte Unicode characters. charID = Utils::String::chars2Unicode(text, cursor); charEntry = text.substr(i, cursor - i); Glyph* glyph {getGlyph(charID)}; if (glyph != nullptr) { charWidth = static_cast(glyph->advance.x); byteCount = cursor - i; } else { // Missing glyph. continue; } if (multiLine && (charEntry == " " || charEntry == "\t")) { lastSpace = i; lastSpacePos = lineWidth; } if (lineWidth + charWidth <= maxLength) { if (lineWidth + charWidth + dotsWidth > maxLength) dotsSection.emplace_back(std::make_pair(byteCount, charWidth)); lineWidth += charWidth; wrappedText.append(charEntry); } else if (!multiLine) { addDots = true; break; } else { if (maxHeight == 0.0f || accumHeight < maxHeight) { // New row. float spaceOffset {0.0f}; if (lastSpace == wrappedText.size()) { wrappedText.append("\n"); } else if (lastSpace != 0) { if (lastSpace + spaceAccum == wrappedText.size()) wrappedText.append("\n"); else wrappedText[lastSpace + spaceAccum] = '\n'; spaceOffset = lineWidth - lastSpacePos; } else { if (lastSpace == 0) ++spaceAccum; wrappedText.append("\n"); } if (charEntry != " " && charEntry != "\t") { wrappedText.append(charEntry); lineWidth = charWidth; } else { lineWidth = 0.0f; } accumHeight += lineHeight; lineWidth += spaceOffset; lastSpacePos = 0.0f; lastSpace = 0; } else { if (multiLine) addDots = true; break; } } i = cursor - 1; } if (addDots) { if (!wrappedText.empty() && wrappedText.back() == ' ') { lineWidth -= sizeText(" ").x; wrappedText.pop_back(); } else if (!wrappedText.empty() && wrappedText.back() == '\t') { lineWidth -= sizeText("\t").x; wrappedText.pop_back(); } while (!wrappedText.empty() && !dotsSection.empty() && lineWidth + dotsWidth > maxLength) { lineWidth -= dotsSection.back().second; wrappedText.erase(wrappedText.length() - dotsSection.back().first); dotsSection.pop_back(); } if (!wrappedText.empty() && wrappedText.back() == ' ') wrappedText.pop_back(); wrappedText.append("..."); } return wrappedText; } glm::vec2 Font::getWrappedTextCursorOffset(const std::string& wrappedText, const size_t stop, const float lineSpacing) { float lineWidth {0.0f}; float yPos {0.0f}; size_t cursor {0}; while (cursor < stop) { unsigned int character {Utils::String::chars2Unicode(wrappedText, cursor)}; if (character == '\n') { lineWidth = 0.0f; yPos += getHeight(lineSpacing); continue; } Glyph* glyph {getGlyph(character)}; if (glyph) lineWidth += glyph->advance.x; } return glm::vec2 {lineWidth, yPos}; } float Font::getLetterHeight() { if (mLetterHeight == 0.0f) return mFontSize * 0.737f; // Only needed if face does not contain the letter 'S'. else return mLetterHeight; } std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig, const float maxHeight, const float sizeMultiplier, const bool fontSizeDimmed) { using namespace ThemeFlags; if (!(properties & FONT_PATH) && !(properties & FONT_SIZE)) return orig; float size {static_cast(orig ? orig->mFontSize : FONT_SIZE_MEDIUM_FIXED)}; std::string path {orig ? orig->mPath : getDefaultPath()}; const float screenSize {Renderer::getIsVerticalOrientation() ? static_cast(Renderer::getScreenWidth()) : static_cast(Renderer::getScreenHeight())}; if (fontSizeDimmed && properties & FONT_SIZE && elem->has("fontSizeDimmed")) { size = glm::clamp(screenSize * elem->get("fontSizeDimmed"), screenSize * 0.001f, screenSize * 1.5f); } else if (properties & FONT_SIZE && elem->has("fontSize")) { size = glm::clamp(screenSize * elem->get("fontSize"), screenSize * 0.001f, screenSize * 1.5f); // This is used by the carousel where the itemScale property also scales the font size. size *= sizeMultiplier; } if (maxHeight != 0.0f && size > maxHeight) size = maxHeight; if (properties & FONT_PATH && elem->has("fontPath")) path = elem->get("fontPath"); if (!((path[0] == ':') && (path[1] == '/')) && !Utils::FileSystem::exists(path)) { LOG(LogError) << "Font file \"" << path << "\" defined by the theme does not exist, " "falling back to \"" << getDefaultPath() << "\""; path = getDefaultPath(); } return get(size, path); } size_t Font::getMemUsage() const { size_t memUsage {0}; for (auto it = mTextures.cbegin(); it != mTextures.cend(); ++it) memUsage += (*it)->textureSize.x * (*it)->textureSize.y * 4; for (auto it = mFaceCache.cbegin(); it != mFaceCache.cend(); ++it) memUsage += it->second->data.length; return memUsage; } size_t Font::getTotalMemUsage() { size_t total {0}; auto it = sFontMap.cbegin(); while (it != sFontMap.cend()) { if (it->second.expired()) { it = sFontMap.erase(it); continue; } total += it->second.lock()->getMemUsage(); ++it; } return total; } std::vector Font::getFallbackFontPaths() { std::vector fontPaths; // Default application fonts. ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-Regular.ttf"); ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-SemiBold.ttf"); ResourceManager::getInstance().getResourcePath(":/fonts/Akrobat-Bold.ttf"); // Vera sans Unicode. fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/DejaVuSans.ttf")); // GNU FreeFont monospaced. fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/FreeMono.ttf")); // Various languages, such as Japanese and Chinese. fontPaths.push_back( ResourceManager::getInstance().getResourcePath(":/fonts/DroidSansFallbackFull.ttf")); // Korean. fontPaths.push_back( ResourceManager::getInstance().getResourcePath(":/fonts/NanumMyeongjo.ttf")); // Font Awesome icon glyphs, used for various special symbols like stars, folders etc. fontPaths.push_back( ResourceManager::getInstance().getResourcePath(":/fonts/fontawesome-webfont.ttf")); // This is only needed for some really rare special characters. fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/Ubuntu-C.ttf")); // Google Noto Emoji. fontPaths.push_back(ResourceManager::getInstance().getResourcePath(":/fonts/NotoEmoji.ttf")); return fontPaths; } Font::FontTexture::FontTexture(const int mFontSize) { textureId = 0; rowHeight = 0; writePos = glm::ivec2 {1, 1}; // Set the texture atlas to a reasonable size, if we run out of space for adding glyphs then // more textures will be created dynamically. textureSize = glm::ivec2 {mFontSize * 6, mFontSize * 6}; } Font::FontTexture::~FontTexture() { // Deinit the texture when destroyed. deinitTexture(); } bool Font::FontTexture::findEmpty(const glm::ivec2& size, glm::ivec2& cursorOut) { if (size.x > textureSize.x || size.y > textureSize.y) return false; if (writePos.x + size.x + 1 > textureSize.x && writePos.y + rowHeight + size.y + 1 < textureSize.y) { // Row is full, but the glyph should fit on the next row so move the cursor there. // Leave 1 pixel of space between glyphs so that pixels from adjacent glyphs will not // get sampled during scaling and interpolation, which would lead to edge artifacts. writePos = glm::ivec2 {1, writePos.y + rowHeight + 1}; rowHeight = 0; } if (writePos.x + size.x + 1 > textureSize.x || writePos.y + size.y + 1 > textureSize.y) return false; // No it still won't fit. cursorOut = writePos; // Leave 1 pixel of space between glyphs. writePos.x += size.x + 1; if (size.y > rowHeight) rowHeight = size.y; return true; } void Font::FontTexture::initTexture() { assert(textureId == 0); // Create a black texture with zero alpha value so that the single-pixel spaces between the // glyphs will not be visible. That would otherwise lead to edge artifacts as these pixels // would get sampled during scaling. std::vector texture(textureSize.x * textureSize.y * 4, 0); textureId = Renderer::getInstance()->createTexture(0, Renderer::TextureType::RED, true, true, false, false, textureSize.x, textureSize.y, &texture[0]); } void Font::FontTexture::deinitTexture() { if (textureId != 0) { Renderer::getInstance()->destroyTexture(textureId); textureId = 0; } } Font::FontFace::FontFace(ResourceData&& d, float size, const std::string& path) : data {d} { if (FT_New_Memory_Face(sLibrary, d.ptr.get(), static_cast(d.length), 0, &face) != 0) { LOG(LogError) << "Couldn't load font file \"" << path << "\""; Utils::Platform::emergencyShutdown(); } // Even though a fractional font size can be requested, the glyphs will always be rounded // to integers. It's not useless to call FT_Set_Char_Size() instead of FT_Set_Pixel_Sizes() // though as the glyphs will still be much more evenely sized across different resolutions. FT_Set_Char_Size(face, static_cast(0.0f), static_cast(size * 64.0f), 0, 0); } Font::FontFace::~FontFace() { if (face) FT_Done_Face(face); } void Font::initLibrary() { assert(sLibrary == nullptr); if (FT_Init_FreeType(&sLibrary)) { sLibrary = nullptr; LOG(LogError) << "Couldn't initialize FreeType"; } } void Font::rebuildTextures() { // Recreate OpenGL textures. for (auto it = mTextures.begin(); it != mTextures.end(); ++it) (*it)->initTexture(); // Re-upload the texture data. for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); ++it) { FT_Face face {getFaceForChar(it->first)}; FT_GlyphSlot glyphSlot {face->glyph}; // Load the glyph bitmap through FT. FT_Load_Char(face, it->first, FT_LOAD_RENDER); const glm::ivec2 glyphSize {glyphSlot->bitmap.width, glyphSlot->bitmap.rows}; const glm::ivec2 cursor { static_cast(it->second.texPos.x * it->second.texture->textureSize.x), static_cast(it->second.texPos.y * it->second.texture->textureSize.y)}; // Upload glyph bitmap to texture. if (glyphSize.x > 0 && glyphSize.y > 0) { mRenderer->updateTexture(it->second.texture->textureId, 0, Renderer::TextureType::RED, cursor.x, cursor.y, glyphSize.x, glyphSize.y, glyphSlot->bitmap.buffer); } } } void Font::unloadTextures() { for (auto it = mTextures.begin(); it != mTextures.end(); ++it) (*it)->deinitTexture(); } void Font::getTextureForNewGlyph(const glm::ivec2& glyphSize, FontTexture*& texOut, glm::ivec2& cursorOut) { if (mTextures.size()) { // Check if the most recent texture has space available for the glyph. texOut = mTextures.back().get(); // Will this one work? if (texOut->findEmpty(glyphSize, cursorOut)) return; // Yes. } mTextures.emplace_back(std::make_unique(static_cast(std::round(mFontSize)))); texOut = mTextures.back().get(); texOut->initTexture(); if (!texOut->findEmpty(glyphSize, cursorOut)) { LOG(LogError) << "Glyph too big to fit on a new texture (glyph size > " << texOut->textureSize.x << ", " << texOut->textureSize.y << ")"; texOut = nullptr; } } FT_Face Font::getFaceForChar(unsigned int id) { static const std::vector fallbackFonts {getFallbackFontPaths()}; // Look for the glyph in our current font and then in the fallback fonts if needed. for (unsigned int i {0}; i < fallbackFonts.size() + 1; ++i) { auto fit = mFaceCache.find(i); if (fit == mFaceCache.cend()) { const std::string& path {i == 0 ? mPath : fallbackFonts.at(i - 1)}; ResourceData data {ResourceManager::getInstance().getFileData(path)}; mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), mFontSize, mPath)); fit = mFaceCache.find(i); } if (FT_Get_Char_Index(fit->second->face, id) != 0) return fit->second->face; } // Couldn't find a valid glyph, return the "real" face so we get a "missing" character. return mFaceCache.cbegin()->second->face; } Font::Glyph* Font::getGlyph(const unsigned int id) { // Check if the glyph has already been loaded. auto it = mGlyphMap.find(id); if (it != mGlyphMap.cend()) return &it->second; // We need to create a new entry. FT_Face face {getFaceForChar(id)}; if (!face) { LOG(LogError) << "Couldn't find appropriate font face for character " << id << " for font " << mPath; return nullptr; } const FT_GlyphSlot glyphSlot {face->glyph}; // If the font does not contain hinting information then force the use of the automatic // hinter that is built into FreeType. Note: Using font-supplied hints generally looks worse // than using the auto-hinter so it's disabled for now. // const bool hasHinting {static_cast(glyphSlot->face->face_flags & FT_FACE_FLAG_HINTER)}; const bool hasHinting {true}; if (FT_Load_Char(face, id, (hasHinting ? FT_LOAD_RENDER : FT_LOAD_RENDER | FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_LIGHT))) { LOG(LogError) << "Couldn't find glyph for character " << id << " for font " << mPath << ", size " << mFontSize; return nullptr; } FontTexture* tex {nullptr}; glm::ivec2 cursor {0, 0}; const glm::ivec2 glyphSize {glyphSlot->bitmap.width, glyphSlot->bitmap.rows}; getTextureForNewGlyph(glyphSize, tex, cursor); // This should (hopefully) never occur as size constraints are enforced earlier on. if (tex == nullptr) { LOG(LogError) << "Couldn't create glyph for character " << id << " for font " << mPath << ", size " << mFontSize << " (no suitable texture found)"; return nullptr; } // Use the letter 'S' as a size reference. if (mLetterHeight == 0 && id == 'S') mLetterHeight = static_cast(glyphSize.y); // Create glyph. Glyph& glyph {mGlyphMap[id]}; glyph.texture = tex; glyph.texPos = {cursor.x / static_cast(tex->textureSize.x), cursor.y / static_cast(tex->textureSize.y)}; glyph.texSize = {glyphSize.x / static_cast(tex->textureSize.x), glyphSize.y / static_cast(tex->textureSize.y)}; glyph.advance = {glyphSlot->metrics.horiAdvance >> 6, glyphSlot->metrics.vertAdvance >> 6}; glyph.bearing = {glyphSlot->metrics.horiBearingX >> 6, glyphSlot->metrics.horiBearingY >> 6}; glyph.rows = glyphSize.y; // Upload glyph bitmap to texture. if (glyphSize.x > 0 && glyphSize.y > 0) { mRenderer->updateTexture(tex->textureId, 0, Renderer::TextureType::RED, cursor.x, cursor.y, glyphSize.x, glyphSize.y, glyphSlot->bitmap.buffer); } return &glyph; } float Font::getNewlineStartOffset(const std::string& text, const unsigned int& charStart, const float& xLen, const Alignment& alignment) { switch (alignment) { case ALIGN_LEFT: { return 0; } case ALIGN_CENTER: { int endChar {0}; endChar = static_cast(text.find('\n', charStart)); return (xLen - sizeText(text.substr(charStart, static_cast(endChar) != std::string::npos ? endChar - charStart : endChar)) .x) / 2.0f; } case ALIGN_RIGHT: { int endChar = static_cast(text.find('\n', charStart)); return xLen - (sizeText(text.substr(charStart, static_cast(endChar) != std::string::npos ? endChar - charStart : endChar)) .x); } default: return 0; } } void TextCache::setColor(unsigned int color) { for (auto it = vertexLists.begin(); it != vertexLists.end(); ++it) for (auto it2 = it->verts.begin(); it2 != it->verts.end(); ++it2) it2->color = color; } void TextCache::setOpacity(float opacity) { for (auto it = vertexLists.begin(); it != vertexLists.end(); ++it) { for (auto it2 = it->verts.begin(); it2 != it->verts.end(); ++it2) it2->opacity = opacity; } } void TextCache::setSaturation(float saturation) { for (auto it = vertexLists.begin(); it != vertexLists.end(); ++it) { for (auto it2 = it->verts.begin(); it2 != it->verts.end(); ++it2) it2->saturation = saturation; } } void TextCache::setDimming(float dimming) { for (auto it = vertexLists.begin(); it != vertexLists.end(); ++it) { for (auto it2 = it->verts.begin(); it2 != it->verts.end(); ++it2) it2->dimming = dimming; } }