X-Git-Url: http://review.tizen.org/git/?a=blobdiff_plain;ds=sidebyside;f=dali-toolkit%2Finternal%2Ftext%2Frendering%2Fatlas%2Ftext-atlas-renderer.cpp;h=8653b435ed274327adde6e596d2dff8eab9b90e1;hb=33efb36d4d62bff6febf074ae94feb18ae619ee5;hp=58f7fc1b2ccea5a4545eecfb48e4797c86dfd279;hpb=005f449f3648e36e0cc997735b58b0ebb7a6390b;p=platform%2Fcore%2Fuifw%2Fdali-toolkit.git diff --git a/dali-toolkit/internal/text/rendering/atlas/text-atlas-renderer.cpp b/dali-toolkit/internal/text/rendering/atlas/text-atlas-renderer.cpp index 58f7fc1..1a1063c 100644 --- a/dali-toolkit/internal/text/rendering/atlas/text-atlas-renderer.cpp +++ b/dali-toolkit/internal/text/rendering/atlas/text-atlas-renderer.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Samsung Electronics Co., Ltd. + * Copyright (c) 2022 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,20 +19,23 @@ #include // EXTERNAL INCLUDES -#include -#include #include -#include -#include - +#include +#include +#include +#include +#include // INTERNAL INCLUDES -#include -#include +#include +#include +#include +#include #include -#include -#include -#include +#include +#include +#include +#include using namespace Dali; using namespace Dali::Toolkit; @@ -41,20 +44,19 @@ using namespace Dali::Toolkit::Text; namespace { #if defined(DEBUG_ENABLED) - Debug::Filter* gLogFilter = Debug::Filter::New(Debug::Concise, true, "LOG_TEXT_RENDERING"); +Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_RENDERING"); #endif - const float ZERO( 0.0f ); - const float HALF( 0.5f ); - const float ONE( 1.0f ); - const float TWO( 2.0f ); - const uint32_t DEFAULT_ATLAS_WIDTH = 512u; - const uint32_t DEFAULT_ATLAS_HEIGHT = 512u; -} +const float ZERO(0.0f); +const float HALF(0.5f); +const float ONE(1.0f); +const float ONE_AND_A_HALF(1.5f); +const uint32_t DOUBLE_PIXEL_PADDING = 4u; //Padding will be added twice to Atlas +const uint16_t NO_OUTLINE = 0u; +} // namespace -struct AtlasRenderer::Impl : public ConnectionTracker +struct AtlasRenderer::Impl { - enum Style { STYLE_NORMAL, @@ -63,305 +65,882 @@ struct AtlasRenderer::Impl : public ConnectionTracker struct MeshRecord { - Vector4 mColor; - uint32_t mAtlasId; - MeshData mMeshData; - FrameBufferImage mBuffer; - bool mIsUnderline; + MeshRecord() + : mAtlasId(0u) + { + } + + uint32_t mAtlasId; + AtlasManager::Mesh2D mMesh; }; + /** + * brief Struct used to generate the underline/striketthrough mesh. + * There is one Extent per line of text. + */ struct Extent { - float mBaseLine; - float mLeft; - float mRight; - float mUnderlinePosition; - float mUnderlineThickness; + Extent() + : mBaseLine(0.0f), + mLeft(0.0f), + mRight(0.0f), + mUnderlinePosition(0.0f), + mLineThickness(0.0f), + mMeshRecordIndex(0u), + mUnderlineChunkId(0u), + mStrikethroughPosition(0.0f), + mStrikethroughChunkId(0u) + { + } + + float mBaseLine; + float mLeft; + float mRight; + float mUnderlinePosition; + float mLineThickness; uint32_t mMeshRecordIndex; + uint32_t mUnderlineChunkId; + float mStrikethroughPosition; + uint32_t mStrikethroughChunkId; + }; + + struct MaxBlockSize + { + MaxBlockSize() + : mFontId(0), + mNeededBlockWidth(0), + mNeededBlockHeight(0) + { + } + + FontId mFontId; + uint32_t mNeededBlockWidth; + uint32_t mNeededBlockHeight; }; - struct AtlasRecord + struct CheckEntry { - uint32_t mImageId; + CheckEntry() + : mFontId(0), + mIndex(0) + { + } + + FontId mFontId; Text::GlyphIndex mIndex; }; - struct MaxBlockSize + struct TextCacheEntry { - FontId mFontId; - uint32_t mNeededBlockWidth; - uint32_t mNeededBlockHeight; + TextCacheEntry() + : mFontId{0u}, + mIndex{0u}, + mImageId{0u}, + mOutlineWidth{0u}, + isItalic{false}, + isBold{false} + { + } + + FontId mFontId; + Text::GlyphIndex mIndex; + uint32_t mImageId; + uint16_t mOutlineWidth; + bool isItalic : 1; + bool isBold : 1; }; Impl() + : mDepth(0) { mGlyphManager = AtlasGlyphManager::Get(); - mFontClient = TextAbstraction::FontClient::Get(); - mBasicShader = BasicShader::New(); - mBgraShader = BgraShader::New(); - mBasicShadowShader = BasicShadowShader::New(); - - mFace.reserve( 6u ); - mFace.push_back( 0 ); mFace.push_back( 2u ); mFace.push_back( 1u ); - mFace.push_back( 1u ); mFace.push_back( 2u ); mFace.push_back( 3u ); + mFontClient = TextAbstraction::FontClient::Get(); + + mQuadVertexFormat["aPosition"] = Property::VECTOR2; + mQuadVertexFormat["aTexCoord"] = Property::VECTOR2; + mQuadVertexFormat["aColor"] = Property::VECTOR4; } - void AddGlyphs( const std::vector& positions, - const Vector& glyphs, - const Vector4& textColor, - const Vector2& shadowOffset, - const Vector4& shadowColor, - bool underlineEnabled, - const Vector4& underlineColor, - float underlineHeight ) + void CacheGlyph(const GlyphInfo& glyph, FontId lastFontId, const AtlasGlyphManager::GlyphStyle& style, AtlasManager::AtlasSlot& slot) { - AtlasManager::AtlasSlot slot; - std::vector< MeshRecord > meshContainer; - Vector< Extent > extents; + const Size& defaultTextAtlasSize = mFontClient.GetDefaultTextAtlasSize(); //Retrieve default size of text-atlas-block from font-client. + const Size& maximumTextAtlasSize = mFontClient.GetMaximumTextAtlasSize(); //Retrieve maximum size of text-atlas-block from font-client. - float currentUnderlinePosition = ZERO; - float currentUnderlineThickness = underlineHeight; - uint32_t currentBlockSize = 0; - FontId lastFontId = 0; - Style style = STYLE_NORMAL; + const bool glyphNotCached = !mGlyphManager.IsCached(glyph.fontId, glyph.index, style, slot); // Check FontGlyphRecord vector for entry with glyph index and fontId - if ( fabsf( shadowOffset.x ) > Math::MACHINE_EPSILON_1 || fabsf( shadowOffset.y ) > Math::MACHINE_EPSILON_1 ) - { - style = STYLE_DROP_SHADOW; - } + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "AddGlyphs fontID[%u] glyphIndex[%u] [%s]\n", glyph.fontId, glyph.index, (glyphNotCached) ? "not cached" : "cached"); - if ( mImageIds.Size() ) + if(glyphNotCached) { - // Unreference any currently used glyphs - RemoveText(); - } + MaxBlockSize& blockSize = mBlockSizes[0u]; - CalculateBlocksSize( glyphs ); + if(lastFontId != glyph.fontId) + { + uint32_t index = 0u; + // Looks through all stored block sizes until finds the one which mataches required glyph font it. Ensures new atlas block size will match existing for same font id. + // CalculateBlocksSize() above ensures a block size entry exists. + for(std::vector::const_iterator it = mBlockSizes.begin(), + endIt = mBlockSizes.end(); + it != endIt; + ++it, ++index) + { + const MaxBlockSize& blockSizeEntry = *it; + if(blockSizeEntry.mFontId == glyph.fontId) + { + blockSize = mBlockSizes[index]; + } + } + } - for( uint32_t i = 0, glyphSize = glyphs.Size(); i < glyphSize; ++i ) - { - const GlyphInfo& glyph = glyphs[ i ]; + // Create a new image for the glyph + PixelData bitmap; - // No operation for white space - if ( glyph.width && glyph.height ) + // Whether the glyph is an outline. + const bool isOutline = 0u != style.outline; + + // Whether the current glyph is a color one. + const bool isColorGlyph = mFontClient.IsColorGlyph(glyph.fontId, glyph.index); + + if(!isOutline || (isOutline && !isColorGlyph)) { - // Are we still using the same fontId as previous - if ( glyph.fontId != lastFontId ) + // Retrieve the emoji's bitmap. + TextAbstraction::FontClient::GlyphBufferData glyphBufferData; + glyphBufferData.width = isColorGlyph ? glyph.width : 0; // Desired width and height. + glyphBufferData.height = isColorGlyph ? glyph.height : 0; + + mFontClient.CreateBitmap(glyph.fontId, + glyph.index, + glyph.isItalicRequired, + glyph.isBoldRequired, + glyphBufferData, + style.outline); + + uint32_t glyphBufferSize = glyphBufferData.width * glyphBufferData.height * Pixel::GetBytesPerPixel(glyphBufferData.format); + // If glyph buffer data don't have ownership, Or if we need to decompress, create new memory and replace ownership. + if(!glyphBufferData.isBufferOwned || glyphBufferData.compressionType != TextAbstraction::FontClient::GlyphBufferData::CompressionType::NO_COMPRESSION) { - // We need to fetch fresh font underline metrics - FontMetrics fontMetrics; - mFontClient.GetFontMetrics( glyph.fontId, fontMetrics ); - currentUnderlinePosition = ceil( fabsf( fontMetrics.underlinePosition ) ); - float descender = ceil( fabsf( fontMetrics.descender ) ); - - if ( underlineHeight == ZERO ) + uint8_t* newBuffer = (uint8_t*)malloc(glyphBufferSize); + if(DALI_LIKELY(newBuffer != nullptr)) { - currentUnderlineThickness = fontMetrics.underlineThickness; - - // Ensure underline will be at least a pixel high - if ( currentUnderlineThickness < ONE ) - { - currentUnderlineThickness = ONE; - } - else + TextAbstraction::FontClient::GlyphBufferData::Decompress(glyphBufferData, newBuffer); + if(glyphBufferData.isBufferOwned) { - currentUnderlineThickness = ceil( currentUnderlineThickness ); + // Release previous buffer + free(glyphBufferData.buffer); } + glyphBufferData.isBufferOwned = true; + glyphBufferData.buffer = newBuffer; + glyphBufferData.compressionType = TextAbstraction::FontClient::GlyphBufferData::CompressionType::NO_COMPRESSION; + } + } + + // Create the pixel data. + bitmap = PixelData::New(glyphBufferData.buffer, + glyphBufferSize, + glyphBufferData.width, + glyphBufferData.height, + glyphBufferData.format, + PixelData::FREE); + + // Change buffer ownership. + glyphBufferData.isBufferOwned = false; + + if(bitmap) + { + // Ensure that the next image will fit into the current block size + if(bitmap.GetWidth() > blockSize.mNeededBlockWidth) + { + blockSize.mNeededBlockWidth = bitmap.GetWidth(); } - // Clamp the underline position at the font descender and check for ( as EFL describes it ) a broken font - if ( currentUnderlinePosition > descender ) + if(bitmap.GetHeight() > blockSize.mNeededBlockHeight) { - currentUnderlinePosition = descender; + blockSize.mNeededBlockHeight = bitmap.GetHeight(); } - if ( ZERO == currentUnderlinePosition ) + + // If CheckAtlas in AtlasManager::Add can't fit the bitmap in the current atlas it will create a new atlas + + // Setting the block size and size of new atlas does not mean a new one will be created. An existing atlas may still surffice. + uint32_t default_width = defaultTextAtlasSize.width; + uint32_t default_height = defaultTextAtlasSize.height; + + while( + (blockSize.mNeededBlockWidth >= (default_width - (DOUBLE_PIXEL_PADDING + 1u)) || + blockSize.mNeededBlockHeight >= (default_height - (DOUBLE_PIXEL_PADDING + 1u))) && + (default_width < maximumTextAtlasSize.width && + default_height < maximumTextAtlasSize.height)) { - // Move offset down by one ( EFL behavior ) - currentUnderlinePosition = ONE; + default_width <<= 1u; + default_height <<= 1u; } + mGlyphManager.SetNewAtlasSize(default_width, + default_height, + blockSize.mNeededBlockWidth, + blockSize.mNeededBlockHeight); + + // Locate a new slot for our glyph + mGlyphManager.Add(glyph, style, bitmap, slot); // slot will be 0 is glyph not added } + } + } + else + { + // We have 2+ copies of the same glyph + mGlyphManager.AdjustReferenceCount(glyph.fontId, glyph.index, style, 1); //increment + } + } - const Vector2& position = positions[ i ]; - MeshData newMeshData; - mGlyphManager.Cached( glyph.fontId, glyph.index, slot ); + void GenerateMesh(const GlyphInfo& glyph, + const Vector2& position, + const Vector4& color, + uint16_t outline, + AtlasManager::AtlasSlot& slot, + bool decorationlineGlyph, + float currentUnderlinePosition, + float currentlineThickness, + std::vector& meshContainer, + Vector& newTextCache, + Vector& extents, + uint32_t underlineChunkId, + bool isGlyphCached, + uint32_t strikethroughChunkId) + { + // Generate mesh data for this quad, plugging in our supplied position + AtlasManager::Mesh2D newMesh; + mGlyphManager.GenerateMeshData(slot.mImageId, position, newMesh); - if ( slot.mImageId ) + if(!isGlyphCached) + { + TextCacheEntry textCacheEntry; + textCacheEntry.mFontId = glyph.fontId; + textCacheEntry.mImageId = slot.mImageId; + textCacheEntry.mIndex = glyph.index; + textCacheEntry.mOutlineWidth = outline; + textCacheEntry.isItalic = glyph.isItalicRequired; + textCacheEntry.isBold = glyph.isBoldRequired; + + newTextCache.PushBack(textCacheEntry); + } + + AtlasManager::Vertex2D* verticesBuffer = newMesh.mVertices.Begin(); + + for(unsigned int index = 0u, size = newMesh.mVertices.Count(); + index < size; + ++index) + { + AtlasManager::Vertex2D& vertex = *(verticesBuffer + index); + + // Set the color of the vertex. + vertex.mColor = color; + } + + // Since Free Type font doesn't contain the strikethrough-position property, + // strikethrough position will be calculated by moving the underline position upwards by half the value of the line height. + float strikethroughStartingYPosition = (position.y + glyph.yBearing + currentUnderlinePosition) - ((glyph.height) * HALF); + + // Find an existing mesh data object to attach to ( or create a new one, if we can't find one using the same atlas) + StitchTextMesh(meshContainer, + newMesh, + extents, + position.y + glyph.yBearing, + decorationlineGlyph, + currentUnderlinePosition, + currentlineThickness, + slot, + underlineChunkId, + strikethroughStartingYPosition, + strikethroughChunkId); + } + + void CreateActors(const std::vector& meshContainer, + const Size& textSize, + const Vector4& color, + const Vector4& shadowColor, + const Vector2& shadowOffset, + Actor textControl, + Property::Index animatablePropertyIndex, + bool drawShadow) + { + if(!mActor) + { + // Create a container actor to act as a common parent for text and shadow, to avoid color inheritence issues. + mActor = Actor::New(); + mActor.SetProperty(Actor::Property::PARENT_ORIGIN, ParentOrigin::TOP_LEFT); + mActor.SetProperty(Actor::Property::ANCHOR_POINT, AnchorPoint::TOP_LEFT); + mActor.SetProperty(Actor::Property::SIZE, textSize); + mActor.SetProperty(Actor::Property::COLOR_MODE, USE_OWN_MULTIPLY_PARENT_COLOR); + } + + for(std::vector::const_iterator it = meshContainer.begin(), + endIt = meshContainer.end(); + it != endIt; + ++it) + { + const MeshRecord& meshRecord = *it; + + Actor actor = CreateMeshActor(textControl, animatablePropertyIndex, color, meshRecord, textSize, STYLE_NORMAL); + + // Whether the actor has renderers. + const bool hasRenderer = actor.GetRendererCount() > 0u; + + // Create an effect if necessary + if(hasRenderer && + drawShadow) + { + // Change the color of the vertices. + for(Vector::Iterator vIt = meshRecord.mMesh.mVertices.Begin(), + vEndIt = meshRecord.mMesh.mVertices.End(); + vIt != vEndIt; + ++vIt) { - // This glyph already exists so generate mesh data plugging in our supplied position - mGlyphManager.GenerateMeshData( slot.mImageId, position, newMeshData ); - mImageIds.PushBack( slot.mImageId ); + AtlasManager::Vertex2D& vertex = *vIt; + + vertex.mColor = shadowColor; } - else - { - // Select correct size for new atlas if needed....? - if ( lastFontId != glyph.fontId ) + Actor shadowActor = CreateMeshActor(textControl, animatablePropertyIndex, color, meshRecord, textSize, STYLE_DROP_SHADOW); +#if defined(DEBUG_ENABLED) + shadowActor.SetProperty(Dali::Actor::Property::NAME, "Text Shadow renderable actor"); +#endif + // Offset shadow in x and y + shadowActor.RegisterProperty("uOffset", shadowOffset); + Dali::Renderer renderer(shadowActor.GetRendererAt(0)); + int depthIndex = renderer.GetProperty(Dali::Renderer::Property::DEPTH_INDEX); + renderer.SetProperty(Dali::Renderer::Property::DEPTH_INDEX, depthIndex - 1); + mActor.Add(shadowActor); + } + + if(hasRenderer) + { + mActor.Add(actor); + } + } + } + + void AddGlyphs(Text::ViewInterface& view, + Actor textControl, + Property::Index animatablePropertyIndex, + const Vector& positions, + const Vector& glyphs, + const Vector4& defaultColor, + const Vector4* const colorsBuffer, + const ColorIndex* const colorIndicesBuffer, + int depth, + float minLineOffset) + { + AtlasManager::AtlasSlot slot; + slot.mImageId = 0u; + slot.mAtlasId = 0u; + + AtlasManager::AtlasSlot slotOutline; + slotOutline.mImageId = 0u; + slotOutline.mAtlasId = 0u; + + std::vector meshContainer; + std::vector meshContainerOutline; + Vector extents; + Vector strikethroughExtents; + mDepth = depth; + + const Vector2& textSize(view.GetLayoutSize()); + const Vector2 halfTextSize(textSize * 0.5f); + const Vector2& shadowOffset(view.GetShadowOffset()); + const Vector4& shadowColor(view.GetShadowColor()); + const bool underlineEnabled = view.IsUnderlineEnabled(); + const uint16_t outlineWidth = view.GetOutlineWidth(); + const Vector4& outlineColor(view.GetOutlineColor()); + const bool isOutline = 0u != outlineWidth; + const GlyphInfo* hyphens = view.GetHyphens(); + const Length* hyphenIndices = view.GetHyphenIndices(); + const Length hyphensCount = view.GetHyphensCount(); + const bool strikethroughEnabled = view.IsStrikethroughEnabled(); + const float characterSpacing(view.GetCharacterSpacing()); + + // Elided text info. Indices according to elided text. + const auto startIndexOfGlyphs = view.GetStartIndexOfElidedGlyphs(); + const auto firstMiddleIndexOfElidedGlyphs = view.GetFirstMiddleIndexOfElidedGlyphs(); + const auto secondMiddleIndexOfElidedGlyphs = view.GetSecondMiddleIndexOfElidedGlyphs(); + + const bool useDefaultColor = (NULL == colorsBuffer); + + // Get a handle of the font client. Used to retrieve the bitmaps of the glyphs. + TextAbstraction::FontClient fontClient = TextAbstraction::FontClient::Get(); + + // Get the underline runs. + const Length numberOfUnderlineRuns = view.GetNumberOfUnderlineRuns(); + Vector underlineRuns; + underlineRuns.Resize(numberOfUnderlineRuns); + view.GetUnderlineRuns(underlineRuns.Begin(), + 0u, + numberOfUnderlineRuns); + + // Aggregate underline-style-properties from view + const UnderlineStyleProperties viewUnderlineProperties{view.GetUnderlineType(), + view.GetUnderlineColor(), + view.GetUnderlineHeight(), + view.GetDashedUnderlineGap(), + view.GetDashedUnderlineWidth(), + true, + true, + true, + true, + true}; + + float maxUnderlineHeight = viewUnderlineProperties.height; + + // Get the strikethrough runs. + const Length numberOfStrikethroughRuns = view.GetNumberOfStrikethroughRuns(); + Vector strikethroughRuns; + strikethroughRuns.Resize(numberOfStrikethroughRuns); + view.GetStrikethroughRuns(strikethroughRuns.Begin(), 0u, numberOfStrikethroughRuns); + + const StrikethroughStyleProperties viewStrikethroughProperties{view.GetStrikethroughColor(), + view.GetStrikethroughHeight(), + true, + true}; + + float maxStrikethroughHeight = viewStrikethroughProperties.height; + + FontId lastFontId = 0; + Style style = STYLE_NORMAL; + float currentUnderlinePosition = ZERO; + bool thereAreUnderlinedGlyphs = false; + bool thereAreStrikethroughGlyphs = false; + + if(fabsf(shadowOffset.x) > Math::MACHINE_EPSILON_1 || fabsf(shadowOffset.y) > Math::MACHINE_EPSILON_1) + { + style = STYLE_DROP_SHADOW; + } + + CalculateBlocksSize(glyphs); + + // Avoid emptying mTextCache (& removing references) until after incremented references for the new text + Vector newTextCache; + const GlyphInfo* const glyphsBuffer = glyphs.Begin(); + const Vector2* const positionsBuffer = positions.Begin(); + const Vector2 lineOffsetPosition(minLineOffset, 0.f); + uint32_t hyphenIndex = 0; + + //For septated underlined chunks. (this is for Markup case) + uint32_t underlineChunkId = 0u; // give id for each chunk. + bool isPreUnderlined = false; // status of underlined for previous glyph. + std::map mapUnderlineChunkIdWithProperties; // mapping underlineChunkId with UnderlineStyleProperties to get properties of underlined chunk + UnderlineStyleProperties preUnderlineProperties = viewUnderlineProperties; // the previous UnderlineStyleProperties + + //For septated strikethrough chunks. (this is for Markup case) + uint32_t strikethroughChunkId = 0u; // give id for each chunk. + bool isPreStrikethrough = false; // status of strikethrough for previous glyph. + std::map mapStrikethroughChunkIdWithProperties; // mapping strikethroughChunkId with StrikethroughStyleProperties to get properties of strikethrough chunk + StrikethroughStyleProperties preStrikethroughProperties = viewStrikethroughProperties; // the previous StrikethroughStyleProperties + + const Character* textBuffer = view.GetTextBuffer(); + float calculatedAdvance = 0.f; + const Vector& glyphToCharacterMap = view.GetGlyphsToCharacters(); + const CharacterIndex* glyphToCharacterMapBuffer = glyphToCharacterMap.Begin(); + + //Skip hyphenIndices less than startIndexOfGlyphs or between two middle of elided text + if(hyphenIndices) + { + while((hyphenIndex < hyphensCount) && (hyphenIndices[hyphenIndex] < startIndexOfGlyphs || + (hyphenIndices[hyphenIndex] > firstMiddleIndexOfElidedGlyphs && hyphenIndices[hyphenIndex] < secondMiddleIndexOfElidedGlyphs))) + { + ++hyphenIndex; + } + } + + //To keep the last fontMetrics of lastDecorativeLinesFontId + FontId lastDecorativeLinesFontId = 0; // DecorativeLines like Undeline and Strikethrough + FontMetrics lastDecorativeLinesFontMetrics; + fontClient.GetFontMetrics(lastDecorativeLinesFontId, lastDecorativeLinesFontMetrics); + + // Iteration on glyphs + for(uint32_t i = 0, glyphSize = glyphs.Size(); i < glyphSize; ++i) + { + GlyphInfo glyph; + bool addHyphen = ((hyphenIndex < hyphensCount) && hyphenIndices && ((i + startIndexOfGlyphs) == hyphenIndices[hyphenIndex])); + if(addHyphen && hyphens) + { + glyph = hyphens[hyphenIndex]; + i--; + } + else + { + glyph = *(glyphsBuffer + i); + } + + Vector::ConstIterator currentUnderlinedGlyphRunIt = underlineRuns.End(); + const bool isGlyphUnderlined = underlineEnabled || IsGlyphUnderlined(i, underlineRuns, currentUnderlinedGlyphRunIt); + const UnderlineStyleProperties currentUnderlineProperties = GetCurrentUnderlineProperties(i, isGlyphUnderlined, underlineRuns, currentUnderlinedGlyphRunIt, viewUnderlineProperties); + float currentUnderlineHeight = currentUnderlineProperties.height; + thereAreUnderlinedGlyphs = thereAreUnderlinedGlyphs || isGlyphUnderlined; + + Vector::ConstIterator currentStrikethroughGlyphRunIt = strikethroughRuns.End(); + const bool isGlyphStrikethrough = strikethroughEnabled || IsGlyphStrikethrough(i, strikethroughRuns, currentStrikethroughGlyphRunIt); + const StrikethroughStyleProperties currentStrikethroughProperties = GetCurrentStrikethroughProperties(i, isGlyphStrikethrough, strikethroughRuns, currentStrikethroughGlyphRunIt, viewStrikethroughProperties); + float currentStrikethroughHeight = currentStrikethroughProperties.height; + thereAreStrikethroughGlyphs = thereAreStrikethroughGlyphs || isGlyphStrikethrough; + + // No operation for white space + if(glyph.width && glyph.height) + { + // Check and update decorative-lines informations + if(isGlyphUnderlined || isGlyphStrikethrough) + { + bool isDecorativeLinesFontIdUpdated = false; + // Are we still using the same fontId as previous + if(glyph.fontId != lastDecorativeLinesFontId) { - for ( uint32_t j = 0; j < mBlockSizes.size(); ++j ) + // We need to fetch fresh font metrics + lastDecorativeLinesFontId = glyph.fontId; + isDecorativeLinesFontIdUpdated = true; + fontClient.GetFontMetrics(lastDecorativeLinesFontId, lastDecorativeLinesFontMetrics); + + if(isGlyphStrikethrough || isGlyphUnderlined) { - if ( mBlockSizes[ j ].mFontId == glyph.fontId ) - { - currentBlockSize = j; - mGlyphManager.SetNewAtlasSize( DEFAULT_ATLAS_WIDTH, - DEFAULT_ATLAS_HEIGHT, - mBlockSizes[ j ].mNeededBlockWidth, - mBlockSizes[ j ].mNeededBlockHeight ); - } + //The currentUnderlinePosition will be used for both Underline and/or Strikethrough + currentUnderlinePosition = FetchUnderlinePositionFromFontMetrics(lastDecorativeLinesFontMetrics); } } - // Create a new image for the glyph - BufferImage bitmap = mFontClient.CreateBitmap( glyph.fontId, glyph.index ); - if ( bitmap ) + if(isGlyphUnderlined && (isDecorativeLinesFontIdUpdated || !(currentUnderlineProperties.IsHeightEqualTo(preUnderlineProperties)))) { - // Ensure that the next image will fit into the current block size - bool setSize = false; - if ( bitmap.GetWidth() > mBlockSizes[ currentBlockSize ].mNeededBlockWidth ) - { - setSize = true; - mBlockSizes[ currentBlockSize ].mNeededBlockWidth = bitmap.GetWidth(); - } - if ( bitmap.GetHeight() > mBlockSizes[ currentBlockSize ].mNeededBlockHeight ) - { - setSize = true; - mBlockSizes[ currentBlockSize ].mNeededBlockHeight = bitmap.GetHeight(); - } - - if ( setSize ) + //If the Underline Height is changed then we need to recalculate height. + if(!(currentUnderlineProperties.IsHeightEqualTo(preUnderlineProperties))) { - mGlyphManager.SetNewAtlasSize( DEFAULT_ATLAS_WIDTH, - DEFAULT_ATLAS_HEIGHT, - mBlockSizes[ currentBlockSize ].mNeededBlockWidth, - mBlockSizes[ currentBlockSize ].mNeededBlockHeight ); + maxUnderlineHeight = currentUnderlineHeight; } - // Locate a new slot for our glyph - mGlyphManager.Add( glyph, bitmap, slot ); + CalcualteUnderlineHeight(lastDecorativeLinesFontMetrics, currentUnderlineHeight, maxUnderlineHeight); + } - // Generate mesh data for this quad, plugging in our supplied position - if ( slot.mImageId ) + if(isGlyphStrikethrough && (isDecorativeLinesFontIdUpdated || !(currentStrikethroughProperties.IsHeightEqualTo(preStrikethroughProperties)))) + { + //If the Strikethrough Height is changed then we need to recalculate height. + if(!(currentStrikethroughProperties.IsHeightEqualTo(preStrikethroughProperties))) { - mGlyphManager.GenerateMeshData( slot.mImageId, position, newMeshData ); - mImageIds.PushBack( slot.mImageId ); + maxStrikethroughHeight = currentStrikethroughHeight; } + + CalcualteStrikethroughHeight(currentStrikethroughHeight, maxStrikethroughHeight); } - } - // Find an existing mesh data object to attach to ( or create a new one, if we can't find one using the same atlas) - StitchTextMesh( meshContainer, - newMeshData, - extents, - textColor, - position.y + glyph.yBearing, - currentUnderlinePosition, - currentUnderlineThickness, - slot ); - lastFontId = glyph.fontId; - } - } + } // decorative-lines - if ( underlineEnabled ) - { - // Check to see if any of the text needs an underline - GenerateUnderlines( meshContainer, extents, underlineColor, textColor ); - } + AtlasGlyphManager::GlyphStyle style; + style.isItalic = glyph.isItalicRequired; + style.isBold = glyph.isBoldRequired; - // For each MeshData object, create a mesh actor and add to the renderable actor - if ( meshContainer.size() ) - { - for ( std::vector< MeshRecord >::iterator mIt = meshContainer.begin(); mIt != meshContainer.end(); ++mIt ) - { - MeshActor actor = MeshActor::New( Mesh::New( mIt->mMeshData ) ); - actor.SetColor( mIt->mColor ); + // Retrieves and caches the glyph's bitmap. + CacheGlyph(glyph, lastFontId, style, slot); - // Ensure that text rendering is unfiltered - actor.SetFilterMode( FilterMode::NEAREST, FilterMode::NEAREST ); - if ( mIt->mIsUnderline ) + // Retrieves and caches the outline glyph's bitmap. + if(isOutline) { - actor.SetColorMode( USE_OWN_COLOR ); + style.outline = outlineWidth; + CacheGlyph(glyph, lastFontId, style, slotOutline); } - else + + // Move the origin (0,0) of the mesh to the center of the actor + Vector2 position = *(positionsBuffer + i); + + if(addHyphen) { - actor.SetColorMode( USE_OWN_MULTIPLY_PARENT_COLOR ); + GlyphInfo tempInfo = *(glyphsBuffer + i); + calculatedAdvance = GetCalculatedAdvance(*(textBuffer + (*(glyphToCharacterMapBuffer + i))), characterSpacing, tempInfo.advance); + position.x = position.x + calculatedAdvance - tempInfo.xBearing + glyph.xBearing; + position.y += tempInfo.yBearing - glyph.yBearing; } - // Check to see what pixel format the shader should be - if ( mGlyphManager.GetPixelFormat( mIt->mAtlasId ) == Pixel::L8 ) + position = Vector2(roundf(position.x), position.y) - halfTextSize - lineOffsetPosition; // roundf() avoids pixel alignment issues. + + if(0u != slot.mImageId) // invalid slot id, glyph has failed to be added to atlas { - // Create an effect if necessary - if ( style == STYLE_DROP_SHADOW ) + Vector2 positionPlusOutlineOffset = position; + if(isOutline) { - actor.Add( GenerateShadow( *mIt, shadowOffset, shadowColor ) ); + // Add an offset to the text. + const float outlineWidthOffset = static_cast(outlineWidth); + positionPlusOutlineOffset += Vector2(outlineWidthOffset, outlineWidthOffset); } - actor.SetShaderEffect( mBasicShader ); - } - else - { - actor.SetShaderEffect( mBgraShader ); - } - if ( mActor ) - { - mActor.Add( actor ); + // Get the color of the character. + const ColorIndex colorIndex = useDefaultColor ? 0u : *(colorIndicesBuffer + i); + const Vector4& color = (useDefaultColor || (0u == colorIndex)) ? defaultColor : *(colorsBuffer + colorIndex - 1u); + + //The new underlined chunk. Add new id if they are not consecutive indices (this is for Markup case) + // Examples: "Hello World Hello World", "World Hello World", " World Hello World" + if((!isPreUnderlined && isGlyphUnderlined) || (isGlyphUnderlined && (preUnderlineProperties != currentUnderlineProperties))) + { + underlineChunkId++; + mapUnderlineChunkIdWithProperties.insert(std::pair(underlineChunkId, currentUnderlineProperties)); + } + + //Keep status of underlined for previous glyph to check consecutive indices + isPreUnderlined = isGlyphUnderlined; + preUnderlineProperties = currentUnderlineProperties; + + GenerateMesh(glyph, + positionPlusOutlineOffset, + color, + NO_OUTLINE, + slot, + isGlyphUnderlined, + currentUnderlinePosition, + maxUnderlineHeight, + meshContainer, + newTextCache, + extents, + underlineChunkId, + false, + 0u); + + if(isGlyphStrikethrough) + { + //The new strikethrough chunk. Add new id if they are not consecutive indices (this is for Markup case) + // Examples: "Hello World Hello World", "World Hello World", " World Hello World" + if((!isPreStrikethrough) || (preStrikethroughProperties != currentStrikethroughProperties)) + { + strikethroughChunkId++; + mapStrikethroughChunkIdWithProperties.insert(std::pair(strikethroughChunkId, currentStrikethroughProperties)); + } + + GenerateMesh(glyph, + positionPlusOutlineOffset, + color, + NO_OUTLINE, + slot, + isGlyphStrikethrough, + 0.0f, + maxStrikethroughHeight, + meshContainer, + newTextCache, + strikethroughExtents, + 0u, + true, + strikethroughChunkId); + } + + //Keep status of Strikethrough for previous glyph to check consecutive indices + isPreStrikethrough = isGlyphStrikethrough; + preStrikethroughProperties = currentStrikethroughProperties; + + lastFontId = glyph.fontId; // Prevents searching for existing blocksizes when string of the same fontId. } - else + + if(isOutline && (0u != slotOutline.mImageId)) // invalid slot id, glyph has failed to be added to atlas { - mActor = actor; + GenerateMesh(glyph, + position, + outlineColor, + outlineWidth, + slotOutline, + false, + currentUnderlinePosition, + maxUnderlineHeight, + meshContainerOutline, + newTextCache, + extents, + 0u, + false, + 0u); } } - mActor.OffStageSignal().Connect( this, &AtlasRenderer::Impl::OffStageDisconnect ); + + if(addHyphen) + { + hyphenIndex++; + } + } // glyphs + + // Now remove references for the old text + RemoveText(); + mTextCache.Swap(newTextCache); + + if(thereAreUnderlinedGlyphs) + { + // Check to see if any of the text needs an underline + GenerateUnderlines(meshContainer, extents, viewUnderlineProperties, mapUnderlineChunkIdWithProperties); } + + if(thereAreStrikethroughGlyphs) + { + // Check to see if any of the text needs a strikethrough + GenerateStrikethrough(meshContainer, strikethroughExtents, viewStrikethroughProperties, mapStrikethroughChunkIdWithProperties); + } + + // For each MeshData object, create a mesh actor and add to the renderable actor + bool isShadowDrawn = false; + if(!meshContainerOutline.empty()) + { + const bool drawShadow = STYLE_DROP_SHADOW == style; + CreateActors(meshContainerOutline, + textSize, + outlineColor, + shadowColor, + shadowOffset, + textControl, + animatablePropertyIndex, + drawShadow); + + isShadowDrawn = drawShadow; + } + + // For each MeshData object, create a mesh actor and add to the renderable actor + if(!meshContainer.empty()) + { + const bool drawShadow = !isShadowDrawn && (STYLE_DROP_SHADOW == style); + CreateActors(meshContainer, + textSize, + defaultColor, + shadowColor, + shadowOffset, + textControl, + animatablePropertyIndex, + drawShadow); + } + #if defined(DEBUG_ENABLED) Toolkit::AtlasGlyphManager::Metrics metrics = mGlyphManager.GetMetrics(); - DALI_LOG_INFO( gLogFilter, Debug::General, "TextAtlasRenderer::GlyphManager::GlyphCount: %i, AtlasCount: %i, TextureMemoryUse: %iK\n", - metrics.mGlyphCount, - metrics.mAtlasMetrics.mAtlasCount, - metrics.mAtlasMetrics.mTextureMemoryUsed / 1024 ); - for ( uint32_t i = 0; i < metrics.mAtlasMetrics.mAtlasCount; ++i ) + DALI_LOG_INFO(gLogFilter, Debug::General, "TextAtlasRenderer::GlyphManager::GlyphCount: %i, AtlasCount: %i, TextureMemoryUse: %iK\n", metrics.mGlyphCount, metrics.mAtlasMetrics.mAtlasCount, metrics.mAtlasMetrics.mTextureMemoryUsed / 1024); + + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "%s\n", metrics.mVerboseGlyphCounts.c_str()); + + for(uint32_t i = 0; gLogFilter->IsEnabledFor(Debug::Verbose) && i < metrics.mAtlasMetrics.mAtlasCount; ++i) + { + DALI_LOG_INFO(gLogFilter, Debug::Verbose, " Atlas [%i] %sPixels: %s Size: %ix%i, BlockSize: %ix%i, BlocksUsed: %i/%i\n", i + 1, i > 8 ? "" : " ", metrics.mAtlasMetrics.mAtlasMetrics[i].mPixelFormat == Pixel::L8 ? "L8 " : "BGRA", metrics.mAtlasMetrics.mAtlasMetrics[i].mSize.mWidth, metrics.mAtlasMetrics.mAtlasMetrics[i].mSize.mHeight, metrics.mAtlasMetrics.mAtlasMetrics[i].mSize.mBlockWidth, metrics.mAtlasMetrics.mAtlasMetrics[i].mSize.mBlockHeight, metrics.mAtlasMetrics.mAtlasMetrics[i].mBlocksUsed, metrics.mAtlasMetrics.mAtlasMetrics[i].mTotalBlocks); + } +#endif + } + + void RemoveText() + { + for(Vector::Iterator oldTextIter = mTextCache.Begin(); oldTextIter != mTextCache.End(); ++oldTextIter) + { + AtlasGlyphManager::GlyphStyle style; + style.outline = oldTextIter->mOutlineWidth; + style.isItalic = oldTextIter->isItalic; + style.isBold = oldTextIter->isBold; + mGlyphManager.AdjustReferenceCount(oldTextIter->mFontId, oldTextIter->mIndex, style, -1 /*decrement*/); + } + mTextCache.Resize(0); + } + + Actor CreateMeshActor(Actor textControl, Property::Index animatablePropertyIndex, const Vector4& defaultColor, const MeshRecord& meshRecord, const Vector2& actorSize, Style style) + { + VertexBuffer quadVertices = VertexBuffer::New(mQuadVertexFormat); + quadVertices.SetData(const_cast(&meshRecord.mMesh.mVertices[0]), meshRecord.mMesh.mVertices.Size()); + + Geometry quadGeometry = Geometry::New(); + quadGeometry.AddVertexBuffer(quadVertices); + quadGeometry.SetIndexBuffer(&meshRecord.mMesh.mIndices[0], meshRecord.mMesh.mIndices.Size()); + + TextureSet textureSet(mGlyphManager.GetTextures(meshRecord.mAtlasId)); + + // Choose the shader to use. + const bool isColorShader = (STYLE_DROP_SHADOW != style) && (Pixel::BGRA8888 == mGlyphManager.GetPixelFormat(meshRecord.mAtlasId)); + Shader shader; + if(isColorShader) + { + // The glyph is an emoji and is not a shadow. + if(!mShaderRgba) + { + mShaderRgba = Shader::New(SHADER_TEXT_ATLAS_SHADER_VERT, SHADER_TEXT_ATLAS_RGBA_SHADER_FRAG); + } + shader = mShaderRgba; + } + else + { + // The glyph is text or a shadow. + if(!mShaderL8) + { + mShaderL8 = Shader::New(SHADER_TEXT_ATLAS_SHADER_VERT, SHADER_TEXT_ATLAS_L8_SHADER_FRAG); + } + shader = mShaderL8; + } + + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "defaultColor[%f, %f, %f, %f ]\n", defaultColor.r, defaultColor.g, defaultColor.b, defaultColor.a); + + Dali::Property::Index shaderTextColorIndex = shader.RegisterProperty("textColorAnimatable", defaultColor); + + if(animatablePropertyIndex != Property::INVALID_INDEX) + { + // create constraint for the animatable text's color Property with textColorAnimatable in the shader. + if(shaderTextColorIndex) + { + Constraint constraint = Constraint::New(shader, shaderTextColorIndex, EqualToConstraint()); + constraint.AddSource(Source(textControl, animatablePropertyIndex)); + constraint.Apply(); + } + } + else { - DALI_LOG_INFO( gLogFilter, Debug::Verbose, "Atlas [%i] %sPixels: %s Size: %ix%i, BlockSize: %ix%i, BlocksUsed: %i/%i\n", - i + 1, i > 8 ? "" : " ", - metrics.mAtlasMetrics.mAtlasMetrics[ i ].mPixelFormat == Pixel::L8 ? "L8 " : "BGRA", - metrics.mAtlasMetrics.mAtlasMetrics[ i ].mSize.mWidth, - metrics.mAtlasMetrics.mAtlasMetrics[ i ].mSize.mHeight, - metrics.mAtlasMetrics.mAtlasMetrics[ i ].mSize.mBlockWidth, - metrics.mAtlasMetrics.mAtlasMetrics[ i ].mSize.mBlockHeight, - metrics.mAtlasMetrics.mAtlasMetrics[ i ].mBlocksUsed, - metrics.mAtlasMetrics.mAtlasMetrics[ i ].mTotalBlocks ); + // If not animating the text colour then set to 1's so shader uses the current vertex color + shader.RegisterProperty("textColorAnimatable", Vector4(1.0, 1.0, 1.0, 1.0)); } + + Dali::Renderer renderer = Dali::Renderer::New(quadGeometry, shader); + renderer.SetTextures(textureSet); + renderer.SetProperty(Dali::Renderer::Property::BLEND_MODE, BlendMode::ON); + renderer.SetProperty(Dali::Renderer::Property::DEPTH_INDEX, DepthIndex::CONTENT + mDepth); + + Actor actor = Actor::New(); +#if defined(DEBUG_ENABLED) + actor.SetProperty(Dali::Actor::Property::NAME, "Text renderable actor"); #endif + actor.AddRenderer(renderer); + // Keep all of the origins aligned + actor.SetProperty(Actor::Property::PARENT_ORIGIN, ParentOrigin::TOP_LEFT); + actor.SetProperty(Actor::Property::ANCHOR_POINT, AnchorPoint::TOP_LEFT); + actor.SetProperty(Actor::Property::SIZE, actorSize); + actor.RegisterProperty("uOffset", Vector2::ZERO); + actor.SetProperty(Actor::Property::COLOR_MODE, USE_OWN_MULTIPLY_PARENT_COLOR); + + return actor; } - void StitchTextMesh( std::vector< MeshRecord >& meshContainer, - MeshData& newMeshData, - Vector< Extent >& extents, - const Vector4& color, - float baseLine, - float underlinePosition, - float underlineThickness, - AtlasManager::AtlasSlot& slot ) + void StitchTextMesh(std::vector& meshContainer, + AtlasManager::Mesh2D& newMesh, + Vector& extents, + float baseLine, + bool decorationlineGlyph, + float underlinePosition, + float lineThickness, + AtlasManager::AtlasSlot& slot, + uint32_t underlineChunkId, + float strikethroughPosition, + uint32_t strikethroughChunkId) { - if ( slot.mImageId ) + if(slot.mImageId) { - MeshData::VertexContainer verts = newMeshData.GetVertices(); - float left = verts[ 0 ].x; - float right = verts[ 1 ].x; + float left = newMesh.mVertices[0].mPosition.x; + float right = newMesh.mVertices[1].mPosition.x; // Check to see if there's a mesh data object that references the same atlas ? uint32_t index = 0; - for ( std::vector< MeshRecord >::iterator mIt = meshContainer.begin(); mIt != meshContainer.end(); ++mIt, ++index ) + for(std::vector::iterator mIt = meshContainer.begin(), + mEndIt = meshContainer.end(); + mIt != mEndIt; + ++mIt, ++index) { - if ( slot.mAtlasId == mIt->mAtlasId && color == mIt->mColor ) + if(slot.mAtlasId == mIt->mAtlasId) { - // Stitch the mesh to the existing mesh and adjust any extents - mGlyphManager.StitchMesh( mIt->mMeshData, newMeshData ); - AdjustExtents( extents, - meshContainer, - index, - left, - right, - baseLine, - underlinePosition, - underlineThickness ); + // Append the mesh to the existing mesh and adjust any extents + Toolkit::Internal::AtlasMeshFactory::AppendMesh(mIt->mMesh, newMesh); + + if(decorationlineGlyph) + { + AdjustExtents(extents, + meshContainer, + index, + left, + right, + baseLine, + underlinePosition, + lineThickness, + underlineChunkId, + strikethroughPosition, + strikethroughChunkId); + } + return; } } @@ -369,360 +948,441 @@ struct AtlasRenderer::Impl : public ConnectionTracker // No mesh data object currently exists that references this atlas, so create a new one MeshRecord meshRecord; meshRecord.mAtlasId = slot.mAtlasId; - meshRecord.mMeshData = newMeshData; - meshRecord.mColor = color; - meshRecord.mIsUnderline = false; - meshContainer.push_back( meshRecord ); - - // Adjust extents for this new meshrecord - AdjustExtents( extents, - meshContainer, - meshContainer.size() - 1u, - left, - right, - baseLine, - underlinePosition, - underlineThickness ); + meshRecord.mMesh = newMesh; + meshContainer.push_back(meshRecord); + if(decorationlineGlyph) + { + // Adjust extents for this new meshrecord + AdjustExtents(extents, + meshContainer, + meshContainer.size() - 1u, + left, + right, + baseLine, + underlinePosition, + lineThickness, + underlineChunkId, + strikethroughPosition, + strikethroughChunkId); + } } } - void AdjustExtents( Vector< Extent >& extents, - std::vector< MeshRecord>& meshRecords, - uint32_t index, - float left, - float right, - float baseLine, - float underlinePosition, - float underlineThickness ) + void AdjustExtents(Vector& extents, + std::vector& meshRecords, + uint32_t index, + float left, + float right, + float baseLine, + float underlinePosition, + float lineThickness, + uint32_t underlineChunkId, + float strikethroughPosition, + uint32_t strikethroughChunkId) { bool foundExtent = false; - for ( Vector< Extent >::Iterator eIt = extents.Begin(); eIt != extents.End(); ++eIt ) + for(Vector::Iterator eIt = extents.Begin(), + eEndIt = extents.End(); + eIt != eEndIt; + ++eIt) { - if ( Equals( baseLine, eIt->mBaseLine ) ) + if(Equals(baseLine, eIt->mBaseLine) && underlineChunkId == eIt->mUnderlineChunkId && strikethroughChunkId == eIt->mStrikethroughChunkId) { foundExtent = true; - if ( left < eIt->mLeft ) + if(left < eIt->mLeft) { eIt->mLeft = left; } - if ( right > eIt->mRight ) + if(right > eIt->mRight) { eIt->mRight = right; } - if ( underlinePosition > eIt->mUnderlinePosition ) + if(underlinePosition > eIt->mUnderlinePosition) { eIt->mUnderlinePosition = underlinePosition; } - if ( underlineThickness > eIt->mUnderlineThickness ) + if(lineThickness > eIt->mLineThickness) { - eIt->mUnderlineThickness = underlineThickness; + eIt->mLineThickness = lineThickness; } } } - if ( !foundExtent ) + if(!foundExtent) { Extent extent; - extent.mLeft = left; - extent.mRight = right; - extent.mBaseLine = baseLine; - extent.mUnderlinePosition = underlinePosition; - extent.mUnderlineThickness = underlineThickness; - extent.mMeshRecordIndex = index; - extents.PushBack( extent ); + extent.mLeft = left; + extent.mRight = right; + extent.mBaseLine = baseLine; + extent.mUnderlinePosition = underlinePosition; + extent.mMeshRecordIndex = index; + extent.mUnderlineChunkId = underlineChunkId; + extent.mLineThickness = lineThickness; + extent.mStrikethroughPosition = strikethroughPosition; + extent.mStrikethroughChunkId = strikethroughChunkId; + extents.PushBack(extent); } } - // Unreference any glyphs that were used with this actor - void OffStageDisconnect( Dali::Actor actor ) - { - RemoveText(); - } - - void RemoveText() + void CalculateBlocksSize(const Vector& glyphs) { - for ( uint32_t i = 0; i < mImageIds.Size(); ++i ) + for(Vector::ConstIterator glyphIt = glyphs.Begin(), + glyphEndIt = glyphs.End(); + glyphIt != glyphEndIt; + ++glyphIt) { - mGlyphManager.Remove( mImageIds[ i ] ); - } - mImageIds.Resize( 0 ); - } + const FontId fontId = (*glyphIt).fontId; + bool foundFont = false; - void CalculateBlocksSize( const Vector& glyphs ) - { - MaxBlockSize maxBlockSize; - for ( uint32_t i = 0; i < glyphs.Size(); ++i ) - { - FontId fontId = glyphs[ i ].fontId; - bool foundFont = false; - for ( uint32_t j = 0; j < mBlockSizes.size(); ++j ) + for(std::vector::const_iterator blockIt = mBlockSizes.begin(), + blockEndIt = mBlockSizes.end(); + blockIt != blockEndIt; + ++blockIt) { - if ( mBlockSizes[ j ].mFontId == fontId ) + if((*blockIt).mFontId == fontId) // Different size fonts will have a different fontId { + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Text::AtlasRenderer::CalculateBlocksSize match found fontID(%u) glyphIndex(%u)\n", fontId, (*glyphIt).index); foundFont = true; + break; } } - if ( !foundFont ) + + if(!foundFont) { FontMetrics fontMetrics; - mFontClient.GetFontMetrics( fontId, fontMetrics ); - maxBlockSize.mNeededBlockWidth = static_cast< uint32_t >( fontMetrics.height ); - maxBlockSize.mNeededBlockHeight = static_cast< uint32_t >( fontMetrics.height ); - maxBlockSize.mFontId = fontId; - mBlockSizes.push_back( maxBlockSize ); + mFontClient.GetFontMetrics(fontId, fontMetrics); + + MaxBlockSize maxBlockSize; + maxBlockSize.mNeededBlockWidth = static_cast(fontMetrics.height); + maxBlockSize.mNeededBlockHeight = maxBlockSize.mNeededBlockWidth; + maxBlockSize.mFontId = fontId; + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Text::AtlasRenderer::CalculateBlocksSize New font with no matched blocksize, setting blocksize[%u]\n", maxBlockSize.mNeededBlockWidth); + mBlockSizes.push_back(maxBlockSize); } } } - void GenerateUnderlines( std::vector< MeshRecord>& meshRecords, - Vector< Extent >& extents, - const Vector4& underlineColor, - const Vector4& textColor ) + void GenerateUnderlines(std::vector& meshRecords, + Vector& extents, + const UnderlineStyleProperties& viewUnderlineProperties, + const std::map& mapUnderlineChunkIdWithProperties) { - MeshData newMeshData; - for ( Vector< Extent >::ConstIterator eIt = extents.Begin(); eIt != extents.End(); ++eIt ) - { - MeshData::VertexContainer newVerts; - newVerts.reserve( 4u ); - uint32_t index = eIt->mMeshRecordIndex; - Vector2 uv = mGlyphManager.GetAtlasSize( meshRecords[ index ].mAtlasId ); - - // Make sure we don't hit texture edge for single pixel texture ( filled pixel is in top left of every atlas ) - float u = HALF / uv.x; - float v = HALF / uv.y; - float thickness = eIt->mUnderlineThickness; - float baseLine = eIt->mBaseLine + eIt->mUnderlinePosition - ( thickness * HALF ); - float tlx = eIt->mLeft; - float brx = eIt->mRight; + AtlasManager::Mesh2D newMesh; + unsigned short faceIndex = 0; - newVerts.push_back( MeshData::Vertex( Vector3( tlx, baseLine, ZERO ), - Vector2::ZERO, - Vector3::ZERO ) ); - - newVerts.push_back( MeshData::Vertex( Vector3( brx, baseLine, ZERO ), - Vector2( u, ZERO ), - Vector3::ZERO ) ); + for(Vector::ConstIterator eIt = extents.Begin(), + eEndIt = extents.End(); + eIt != eEndIt; + ++eIt) + { + AtlasManager::Vertex2D vert; + uint32_t index = eIt->mMeshRecordIndex; + Vector2 uv = mGlyphManager.GetAtlasSize(meshRecords[index].mAtlasId); - newVerts.push_back( MeshData::Vertex( Vector3( tlx, baseLine + thickness, ZERO ), - Vector2( ZERO, v ), - Vector3::ZERO ) ); + auto pairUnderlineChunkIdWithProperties = mapUnderlineChunkIdWithProperties.find(eIt->mUnderlineChunkId); - newVerts.push_back( MeshData::Vertex( Vector3( brx, baseLine + thickness, ZERO ), - Vector2( u, v ), - Vector3::ZERO ) ); + const UnderlineStyleProperties underlineProperties = (pairUnderlineChunkIdWithProperties == mapUnderlineChunkIdWithProperties.end()) + ? viewUnderlineProperties + : pairUnderlineChunkIdWithProperties->second; - newMeshData.SetVertices( newVerts ); - newMeshData.SetFaceIndices( mFace ); + const Vector4& underlineColor = underlineProperties.colorDefined ? underlineProperties.color : viewUnderlineProperties.color; + const Text::Underline::Type& underlineType = underlineProperties.typeDefined ? underlineProperties.type : viewUnderlineProperties.type; + const float& dashedUnderlineGap = underlineProperties.dashGapDefined ? underlineProperties.dashGap : viewUnderlineProperties.dashGap; + const float& dashedUnderlineWidth = underlineProperties.dashWidthDefined ? underlineProperties.dashWidth : viewUnderlineProperties.dashWidth; - if ( underlineColor == textColor ) + // Make sure we don't hit texture edge for single pixel texture ( filled pixel is in top left of every atlas ) + float u = HALF / uv.x; + float v = HALF / uv.y; + float thickness = eIt->mLineThickness; + float ShiftLineBy = (underlineType == Text::Underline::Type::DOUBLE) ? (thickness * ONE_AND_A_HALF) : (thickness * HALF); + float baseLine = eIt->mBaseLine + eIt->mUnderlinePosition - ShiftLineBy; + float tlx = eIt->mLeft; + float brx = eIt->mRight; + + if(underlineType == Text::Underline::Type::DASHED) { - mGlyphManager.StitchMesh( meshRecords[ index ].mMeshData, newMeshData ); + float dashTlx = tlx; + float dashBrx = tlx; + + while((dashTlx >= tlx) && (dashTlx < brx) && ((dashTlx + dashedUnderlineWidth) <= brx)) + { + dashBrx = dashTlx + dashedUnderlineWidth; + + //The top left edge of the underline + vert.mPosition.x = dashTlx; + vert.mPosition.y = baseLine; + vert.mTexCoords.x = ZERO; + vert.mTexCoords.y = ZERO; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + //The top right edge of the underline + vert.mPosition.x = dashBrx; + vert.mPosition.y = baseLine; + vert.mTexCoords.x = u; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + //The bottom left edge of the underline + vert.mPosition.x = dashTlx; + vert.mPosition.y = baseLine + thickness; + vert.mTexCoords.x = ZERO; + vert.mTexCoords.y = v; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + //The bottom right edge of the underline + vert.mPosition.x = dashBrx; + vert.mPosition.y = baseLine + thickness; + vert.mTexCoords.x = u; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + dashTlx = dashBrx + dashedUnderlineGap; // The next dash will start at the right of the current dash plus the gap + + // Six indices in counter clockwise winding + newMesh.mIndices.PushBack(faceIndex + 1u); + newMesh.mIndices.PushBack(faceIndex); + newMesh.mIndices.PushBack(faceIndex + 2u); + newMesh.mIndices.PushBack(faceIndex + 2u); + newMesh.mIndices.PushBack(faceIndex + 3u); + newMesh.mIndices.PushBack(faceIndex + 1u); + + faceIndex += 4; + + Toolkit::Internal::AtlasMeshFactory::AppendMesh(meshRecords[index].mMesh, newMesh); + } } else { - MeshRecord record; - newMeshData.SetMaterial( meshRecords[ index ].mMeshData.GetMaterial() ); - newMeshData.SetHasNormals( true ); - newMeshData.SetHasColor( false ); - newMeshData.SetHasTextureCoords( true ); - record.mMeshData = newMeshData; - record.mAtlasId = meshRecords[ index ].mAtlasId; - record.mColor = underlineColor; - record.mIsUnderline = true; - meshRecords.push_back( record ); + // It's either SOLID or DOUBLE so we need to generate the first solid underline anyway. + vert.mPosition.x = tlx; + vert.mPosition.y = baseLine; + vert.mTexCoords.x = ZERO; + vert.mTexCoords.y = ZERO; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + vert.mPosition.x = brx; + vert.mPosition.y = baseLine; + vert.mTexCoords.x = u; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + vert.mPosition.x = tlx; + vert.mPosition.y = baseLine + thickness; + vert.mTexCoords.x = ZERO; + vert.mTexCoords.y = v; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + vert.mPosition.x = brx; + vert.mPosition.y = baseLine + thickness; + vert.mTexCoords.x = u; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + // Six indices in counter clockwise winding + newMesh.mIndices.PushBack(faceIndex + 1u); + newMesh.mIndices.PushBack(faceIndex); + newMesh.mIndices.PushBack(faceIndex + 2u); + newMesh.mIndices.PushBack(faceIndex + 2u); + newMesh.mIndices.PushBack(faceIndex + 3u); + newMesh.mIndices.PushBack(faceIndex + 1u); + faceIndex += 4; + + Toolkit::Internal::AtlasMeshFactory::AppendMesh(meshRecords[index].mMesh, newMesh); + + if(underlineType == Text::Underline::Type::DOUBLE) + { + baseLine += 2 * thickness; + + //The top left edge of the underline + vert.mPosition.x = tlx; + vert.mPosition.y = baseLine; // Vertical start of the second underline + vert.mTexCoords.x = ZERO; + vert.mTexCoords.y = ZERO; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + //The top right edge of the underline + vert.mPosition.x = brx; + vert.mPosition.y = baseLine; + vert.mTexCoords.x = u; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + //The bottom left edge of the underline + vert.mPosition.x = tlx; + vert.mPosition.y = baseLine + thickness; // Vertical End of the second underline + vert.mTexCoords.x = ZERO; + vert.mTexCoords.y = v; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + //The bottom right edge of the underline + vert.mPosition.x = brx; + vert.mPosition.y = baseLine + thickness; + vert.mTexCoords.x = u; + vert.mColor = underlineColor; + newMesh.mVertices.PushBack(vert); + + // Six indices in counter clockwise winding + newMesh.mIndices.PushBack(faceIndex + 1u); + newMesh.mIndices.PushBack(faceIndex); + newMesh.mIndices.PushBack(faceIndex + 2u); + newMesh.mIndices.PushBack(faceIndex + 2u); + newMesh.mIndices.PushBack(faceIndex + 3u); + newMesh.mIndices.PushBack(faceIndex + 1u); + + faceIndex += 4; + + Toolkit::Internal::AtlasMeshFactory::AppendMesh(meshRecords[index].mMesh, newMesh); + } } } } - MeshActor GenerateShadow( MeshRecord& meshRecord, - const Vector2& shadowOffset, - const Vector4& shadowColor ) + void GenerateStrikethrough(std::vector& meshRecords, + Vector& extents, + const StrikethroughStyleProperties& viewStrikethroughProperties, + const std::map& mapStrikethroughChunkIdWithProperties) { - // Scan vertex buffer to determine width and height of effect buffer needed - MeshData::VertexContainer verts = meshRecord.mMeshData.GetVertices(); - float tlx = verts[ 0 ].x; - float tly = verts[ 0 ].y; - float brx = ZERO; - float bry = ZERO; - - for ( uint32_t i = 0; i < verts.size(); ++i ) + AtlasManager::Mesh2D newMesh; + unsigned short faceIndex = 0; + for(Vector::ConstIterator eIt = extents.Begin(), + eEndIt = extents.End(); + eIt != eEndIt; + ++eIt) { - if ( verts[ i ].x < tlx ) - { - tlx = verts[ i ].x; - } - if ( verts[ i ].y < tly ) - { - tly = verts[ i ].y; - } - if ( verts[ i ].x > brx ) - { - brx = verts[ i ].x; - } - if ( verts[ i ].y > bry ) - { - bry = verts[ i ].y; - } - } + AtlasManager::Vertex2D vert; + uint32_t index = eIt->mMeshRecordIndex; + Vector2 uv = mGlyphManager.GetAtlasSize(meshRecords[index].mAtlasId); - float width = brx - tlx; - float height = bry - tly; - float divWidth = TWO / width; - float divHeight = TWO / height; - - // Create a buffer to render to - meshRecord.mBuffer = FrameBufferImage::New( width, height ); - - // Create a mesh actor to contain the post-effect render - MeshData::VertexContainer vertices; - MeshData::FaceIndices face; - - vertices.push_back( MeshData::Vertex( Vector3( tlx + shadowOffset.x, tly + shadowOffset.y, ZERO ), - Vector2::ZERO, - Vector3::ZERO ) ); - - vertices.push_back( MeshData::Vertex( Vector3( brx + shadowOffset.x, tly + shadowOffset.y, ZERO ), - Vector2( ONE, ZERO ), - Vector3::ZERO ) ); - - vertices.push_back( MeshData::Vertex( Vector3( tlx + shadowOffset.x, bry + shadowOffset.y, ZERO ), - Vector2( ZERO, ONE ), - Vector3::ZERO ) ); - - vertices.push_back( MeshData::Vertex( Vector3( brx + shadowOffset.x, bry + shadowOffset.y, ZERO ), - Vector2::ONE, - Vector3::ZERO ) ); - - MeshData meshData; - Material newMaterial = Material::New("effect buffer"); - newMaterial.SetDiffuseTexture( meshRecord.mBuffer ); - meshData.SetMaterial( newMaterial ); - meshData.SetVertices( vertices ); - meshData.SetFaceIndices( mFace ); - meshData.SetHasNormals( true ); - meshData.SetHasColor( false ); - meshData.SetHasTextureCoords( true ); - MeshActor actor = MeshActor::New( Mesh::New( meshData ) ); - actor.SetColorMode( USE_OWN_MULTIPLY_PARENT_COLOR ); - actor.SetShaderEffect( mBgraShader ); - actor.SetFilterMode( FilterMode::LINEAR, FilterMode::LINEAR ); - actor.SetSortModifier( 0.1f ); // force behind main text - - // Create a sub actor to render once with normalized vertex positions - MeshData newMeshData; - MeshData::VertexContainer newVerts; - MeshData::FaceIndices newFaces; - MeshData::FaceIndices faces = meshRecord.mMeshData.GetFaces(); - for ( uint32_t i = 0; i < verts.size(); ++i ) - { - MeshData::Vertex vertex = verts[ i ]; - vertex.x = ( ( vertex.x - tlx ) * divWidth ) - ONE; - vertex.y = ( ( vertex.y - tly ) * divHeight ) - ONE; - newVerts.push_back( vertex ); - } - - // Reverse triangle winding order - uint32_t faceCount = faces.size() / 3; - for ( uint32_t i = 0; i < faceCount; ++i ) - { - uint32_t index = i * 3; - newFaces.push_back( faces[ index + 2 ] ); - newFaces.push_back( faces[ index + 1 ] ); - newFaces.push_back( faces[ index ] ); - } + auto pairStrikethroughChunkIdWithProperties = mapStrikethroughChunkIdWithProperties.find(eIt->mStrikethroughChunkId); - newMeshData.SetMaterial( meshRecord.mMeshData.GetMaterial() ); - newMeshData.SetVertices( newVerts ); - newMeshData.SetFaceIndices( newFaces ); - newMeshData.SetHasNormals( true ); - newMeshData.SetHasColor( false ); - newMeshData.SetHasTextureCoords( true ); - - MeshActor subActor = MeshActor::New( Mesh::New( newMeshData ) ); - subActor.SetColorMode( USE_OWN_MULTIPLY_PARENT_COLOR ); - subActor.SetColor( shadowColor ); - subActor.SetShaderEffect( mBasicShadowShader ); - subActor.SetFilterMode( FilterMode::NEAREST, FilterMode::NEAREST ); - - // Create a render task to render the effect - RenderTask task = Stage::GetCurrent().GetRenderTaskList().CreateTask(); - task.SetTargetFrameBuffer( meshRecord.mBuffer ); - task.SetSourceActor( subActor ); - task.SetClearEnabled( true ); - task.SetClearColor( Vector4::ZERO ); - task.SetExclusive( true ); - task.SetRefreshRate( RenderTask::REFRESH_ONCE ); - task.FinishedSignal().Connect( this, &AtlasRenderer::Impl::RenderComplete ); - actor.Add( subActor ); - return actor; - } + const StrikethroughStyleProperties strikethroughProperties = (pairStrikethroughChunkIdWithProperties == mapStrikethroughChunkIdWithProperties.end()) + ? viewStrikethroughProperties + : pairStrikethroughChunkIdWithProperties->second; - void RenderComplete( RenderTask& renderTask ) - { - // Disconnect and remove this single shot render task - renderTask.FinishedSignal().Disconnect( this, &AtlasRenderer::Impl::RenderComplete ); - Stage::GetCurrent().GetRenderTaskList().RemoveTask( renderTask ); + const Vector4& strikethroughColor = strikethroughProperties.colorDefined ? strikethroughProperties.color : viewStrikethroughProperties.color; - // Get the actor used for render to buffer and remove it from the parent - Actor renderActor = renderTask.GetSourceActor(); - if ( renderActor ) - { - Actor parent = renderActor.GetParent(); - if ( parent ) - { - parent.Remove( renderActor ); - } + // Make sure we don't hit texture edge for single pixel texture ( filled pixel is in top left of every atlas ) + float u = HALF / uv.x; + float v = HALF / uv.y; + float thickness = eIt->mLineThickness; + float tlx = eIt->mLeft; + float brx = eIt->mRight; + float strikethroughPosition = eIt->mStrikethroughPosition; + + vert.mPosition.x = tlx; + vert.mPosition.y = strikethroughPosition; + vert.mTexCoords.x = ZERO; + vert.mTexCoords.y = ZERO; + vert.mColor = strikethroughColor; + newMesh.mVertices.PushBack(vert); + + vert.mPosition.x = brx; + vert.mPosition.y = strikethroughPosition; + vert.mTexCoords.x = u; + vert.mColor = strikethroughColor; + newMesh.mVertices.PushBack(vert); + + vert.mPosition.x = tlx; + vert.mPosition.y = strikethroughPosition + thickness; + vert.mTexCoords.x = ZERO; + vert.mTexCoords.y = v; + vert.mColor = strikethroughColor; + newMesh.mVertices.PushBack(vert); + + vert.mPosition.x = brx; + vert.mPosition.y = strikethroughPosition + thickness; + vert.mTexCoords.x = u; + vert.mColor = strikethroughColor; + newMesh.mVertices.PushBack(vert); + + // Six indices in counter clockwise winding + newMesh.mIndices.PushBack(faceIndex + 1u); + newMesh.mIndices.PushBack(faceIndex); + newMesh.mIndices.PushBack(faceIndex + 2u); + newMesh.mIndices.PushBack(faceIndex + 2u); + newMesh.mIndices.PushBack(faceIndex + 3u); + newMesh.mIndices.PushBack(faceIndex + 1u); + faceIndex += 4; + + Toolkit::Internal::AtlasMeshFactory::AppendMesh(meshRecords[index].mMesh, newMesh); } } - RenderableActor mActor; ///< The actor parent which renders the text - AtlasGlyphManager mGlyphManager; ///< Glyph Manager to handle upload and caching - Vector< uint32_t > mImageIds; ///< A list of imageIDs used by the renderer - TextAbstraction::FontClient mFontClient; ///> The font client used to supply glyph information - ShaderEffect mBasicShader; ///> Shader used to render L8 glyphs - ShaderEffect mBgraShader; ///> Shader used to render BGRA glyphs - ShaderEffect mBasicShadowShader; ///> Shader used to render drop shadow into buffer - std::vector< MaxBlockSize > mBlockSizes; ///> Maximum size needed to contain a glyph in a block within a new atlas - std::vector< MeshData::FaceIndex > mFace; ///> Face indices for a quad + Actor mActor; ///< The actor parent which renders the text + AtlasGlyphManager mGlyphManager; ///< Glyph Manager to handle upload and caching + TextAbstraction::FontClient mFontClient; ///< The font client used to supply glyph information + Shader mShaderL8; ///< The shader for glyphs and emoji's shadows. + Shader mShaderRgba; ///< The shader for emojis. + std::vector mBlockSizes; ///< Maximum size needed to contain a glyph in a block within a new atlas + Vector mTextCache; ///< Caches data from previous render + Property::Map mQuadVertexFormat; ///< Describes the vertex format for text + int mDepth; ///< DepthIndex passed by control when connect to stage }; Text::RendererPtr AtlasRenderer::New() { - DALI_LOG_INFO( gLogFilter, Debug::Verbose, "Text::AtlasRenderer::New()\n" ); + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Text::AtlasRenderer::New()\n"); - return Text::RendererPtr( new AtlasRenderer() ); + return Text::RendererPtr(new AtlasRenderer()); } -RenderableActor AtlasRenderer::Render( Text::ViewInterface& view ) +Actor AtlasRenderer::Render(Text::ViewInterface& view, + Actor textControl, + Property::Index animatablePropertyIndex, + float& alignmentOffset, + int depth) { - UnparentAndReset( mImpl->mActor ); + DALI_LOG_INFO(gLogFilter, Debug::General, "Text::AtlasRenderer::Render()\n"); + + UnparentAndReset(mImpl->mActor); Length numberOfGlyphs = view.GetNumberOfGlyphs(); - if( numberOfGlyphs > 0u ) + if(numberOfGlyphs > 0u) { Vector glyphs; - glyphs.Resize( numberOfGlyphs ); - - std::vector positions; - positions.resize( numberOfGlyphs ); - - numberOfGlyphs = view.GetGlyphs( glyphs.Begin(), - &positions[0], - 0u, - numberOfGlyphs ); - glyphs.Resize( numberOfGlyphs ); - positions.resize( numberOfGlyphs ); - - mImpl->AddGlyphs( positions, - glyphs, - view.GetTextColor(), - view.GetShadowOffset(), - view.GetShadowColor(), - view.IsUnderlineEnabled(), - view.GetUnderlineColor(), - view.GetUnderlineHeight() ); + glyphs.Resize(numberOfGlyphs); + + Vector positions; + positions.Resize(numberOfGlyphs); + + numberOfGlyphs = view.GetGlyphs(glyphs.Begin(), + positions.Begin(), + alignmentOffset, + 0u, + numberOfGlyphs); + + glyphs.Resize(numberOfGlyphs); + positions.Resize(numberOfGlyphs); + + const Vector4* const colorsBuffer = view.GetColors(); + const ColorIndex* const colorIndicesBuffer = view.GetColorIndices(); + const Vector4& defaultColor = view.GetTextColor(); + + mImpl->AddGlyphs(view, + textControl, + animatablePropertyIndex, + positions, + glyphs, + defaultColor, + colorsBuffer, + colorIndicesBuffer, + depth, + alignmentOffset); + + /* In the case where AddGlyphs does not create a renderable Actor for example when glyphs are all whitespace create a new Actor. */ + /* This renderable actor is used to position the text, other "decorations" can rely on there always being an Actor regardless of it is whitespace or regular text. */ + if(!mImpl->mActor) + { + mImpl->mActor = Actor::New(); + } } return mImpl->mActor; @@ -731,10 +1391,10 @@ RenderableActor AtlasRenderer::Render( Text::ViewInterface& view ) AtlasRenderer::AtlasRenderer() { mImpl = new Impl(); - } AtlasRenderer::~AtlasRenderer() { + mImpl->RemoveText(); delete mImpl; }