diff --git a/Source/Engine/Render2D/FallbackFonts.cpp b/Source/Engine/Render2D/FallbackFonts.cpp new file mode 100644 index 000000000..b79a0db4e --- /dev/null +++ b/Source/Engine/Render2D/FallbackFonts.cpp @@ -0,0 +1,11 @@ +#include "FallbackFonts.h" +#include "FontManager.h" +#include "Engine/Core/Math/Math.h" + +FallbackFonts::FallbackFonts(const Array& fonts) + : ManagedScriptingObject(SpawnParams(Guid::New(), Font::TypeInitializer)), + _fontAssets(fonts) +{ + +} + diff --git a/Source/Engine/Render2D/FallbackFonts.h b/Source/Engine/Render2D/FallbackFonts.h new file mode 100644 index 000000000..4b97e1c11 --- /dev/null +++ b/Source/Engine/Render2D/FallbackFonts.h @@ -0,0 +1,94 @@ +#pragma once + +#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Collections/Dictionary.h" +#include "Font.h" +#include "FontAsset.h" + +struct TextRange; +class Font; +class FontAsset; + +API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API FallbackFonts : public ManagedScriptingObject +{ + DECLARE_SCRIPTING_TYPE_NO_SPAWN(FallbackFonts); +private: + /// + /// The list of fallback fonts, ordered by priority. + /// The first element is reserved for the primary font, fallback fonts starts from the second element. + /// + Array _fontAssets; + + Dictionary*> _cache; + +public: + FallbackFonts(const Array& fonts); + + API_FUNCTION() FORCE_INLINE static FallbackFonts* Create(const Array& fonts) { + return New(fonts); + } + + API_PROPERTY() FORCE_INLINE Array& GetFonts() { + return _fontAssets; + } + + API_PROPERTY() FORCE_INLINE void SetFonts(const Array& val) { + _fontAssets = val; + } + + /// + /// Combine the primary fonts with the fallback fonts to get a font list + /// + API_PROPERTY() FORCE_INLINE Array& GetFontList(float size) { + Array* result; + if (_cache.TryGet(size, result)) { + return *result; + } + + result = New>(_fontAssets.Count()); + auto& arr = *result; + for (int32 i = 0; i < _fontAssets.Count(); i++) + { + arr.Add(_fontAssets[i]->CreateFont(size)); + } + + _cache[size] = result; + return *result; + } + + + /// + /// Gets the index of the fallback font that should be used to render the char + /// + /// The char. + /// -1 if char can be rendered with primary font, index if it matches a fallback font. + API_FUNCTION() FORCE_INLINE int32 GetCharFallbackIndex(Char c, Font* primaryFont = nullptr, int32 missing = -1) { + if (primaryFont && primaryFont->GetAsset()->ContainsChar(c)) { + return -1; + } + + int32 fontIndex = 0; + while (fontIndex < _fontAssets.Count() && _fontAssets[fontIndex] && !_fontAssets[fontIndex]->ContainsChar(c)) + { + fontIndex++; + } + + if (fontIndex < _fontAssets.Count()) { + return fontIndex; + + } + + return missing; + } + + API_FUNCTION() FORCE_INLINE bool Verify() { + for (int32 i = 0; i < _fontAssets.Count(); i++) + { + if (!_fontAssets[i]) { + return false; + } + } + + return true; + } +}; diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index b33b10899..4cf1f73a2 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -6,7 +6,7 @@ #include "Engine/Core/Log.h" #include "Engine/Threading/Threading.h" #include "IncludeFreeType.h" -#include "MultiFont.h" +#include "FallbackFonts.h" Font::Font(FontAsset* parentAsset, float size) : ManagedScriptingObject(SpawnParams(Guid::New(), Font::TypeInitializer)) @@ -293,6 +293,261 @@ void Font::ProcessText(const StringView& text, Array& outputLines } } +void Font::ProcessText(FallbackFonts* fallbacks, const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout) +{ + const Array& fallbackFonts = fallbacks->GetFontList(GetSize()); + float cursorX = 0; + int32 kerning; + BlockedTextLineCache tmpLine; + FontBlockCache tmpBlock; + FontCharacterEntry entry; + FontCharacterEntry previous; + int32 textLength = text.Length(); + float scale = layout.Scale / FontManager::FontScale; + float boundsWidth = layout.Bounds.GetWidth(); + float baseLinesDistanceScale = layout.BaseLinesGapScale * scale; + + tmpBlock.Location = Float2::Zero; + tmpBlock.Size = Float2::Zero; + tmpBlock.FirstCharIndex = 0; + tmpBlock.LastCharIndex = -1; + + tmpLine.Location = Float2::Zero; + tmpLine.Size = Float2::Zero; + tmpLine.Blocks = Array(); + + if (textLength == 0) { + return; + } + + int32 lastWrapCharIndex = INVALID_INDEX; + float lastWrapCharX = 0; + bool lastMoveLine = false; + // The index of the font used by the current block + int32 currentFontIndex = fallbacks->GetCharFallbackIndex(text[0], this); + // The maximum font height of the current line + float maxHeight = 0; + float maxAscender = 0; + float lastCursorX = 0; + + auto getFont = [&](int32 index)->Font* { + return index >= 0 ? fallbackFonts[index] : this; + }; + + // Process each character to split text into single blocks + for (int32 currentIndex = 0; currentIndex < textLength;) + { + bool moveLine = false; + bool moveBlock = false; + float xAdvance = 0; + int32 nextCharIndex = currentIndex + 1; + + // Submit line and block if text ends + if (nextCharIndex == textLength) { + moveLine = moveBlock = true; + } + + // Cache current character + const Char currentChar = text[currentIndex]; + const bool isWhitespace = StringUtils::IsWhitespace(currentChar); + + // Check if character can wrap words + const bool isWrapChar = !StringUtils::IsAlnum(currentChar) || isWhitespace || StringUtils::IsUpper(currentChar) || (currentChar >= 0x3040 && currentChar <= 0x9FFF); + if (isWrapChar && currentIndex != 0) + { + lastWrapCharIndex = currentIndex; + lastWrapCharX = cursorX; + } + + int32 nextFontIndex = currentFontIndex; + // Check if it's a newline character + if (currentChar == '\n') + { + // Break line + moveLine = moveBlock = true; + tmpBlock.LastCharIndex++; + } + else + { + // Get character entry + if (nextCharIndex < textLength) { + nextFontIndex = fallbacks->GetCharFallbackIndex(text[nextCharIndex], this, currentFontIndex); + } + + // Get character entry + getFont(currentFontIndex)->GetCharacter(currentChar, entry); + + maxHeight = Math::Max(maxHeight, + static_cast(getFont(currentFontIndex)->GetHeight())); + maxAscender = Math::Max(maxAscender, + static_cast(getFont(currentFontIndex)->GetAscender())); + + // Move block if the font changes or text ends + if (nextFontIndex != currentFontIndex || nextCharIndex == textLength) { + moveBlock = true; + } + + // Get kerning, only when the font hasn't changed + if (!isWhitespace && previous.IsValid && !moveBlock) + { + kerning = getFont(currentFontIndex)->GetKerning(previous.Character, entry.Character); + } + else + { + kerning = 0; + } + previous = entry; + xAdvance = (kerning + entry.AdvanceX) * scale; + + // Check if character fits the line or skip wrapping + if (cursorX + xAdvance <= boundsWidth || layout.TextWrapping == TextWrapping::NoWrap) + { + // Move character + cursorX += xAdvance; + tmpBlock.LastCharIndex++; + } + else if (layout.TextWrapping == TextWrapping::WrapWords) + { + if (lastWrapCharIndex != INVALID_INDEX) + { + // Skip moving twice for the same character + int32 lastLineLastCharIndex = outputLines.HasItems() && outputLines.Last().Blocks.HasItems() ? outputLines.Last().Blocks.Last().LastCharIndex : -10000; + if (lastLineLastCharIndex == lastWrapCharIndex || lastLineLastCharIndex == lastWrapCharIndex - 1 || lastLineLastCharIndex == lastWrapCharIndex - 2) + { + currentIndex = nextCharIndex; + lastMoveLine = moveLine; + continue; + } + + // Move line + const Char wrapChar = text[lastWrapCharIndex]; + moveLine = true; + moveBlock = tmpBlock.FirstCharIndex < lastWrapCharIndex; + + cursorX = lastWrapCharX; + if (StringUtils::IsWhitespace(wrapChar)) + { + // Skip whitespaces + tmpBlock.LastCharIndex = lastWrapCharIndex - 1; + nextCharIndex = currentIndex = lastWrapCharIndex + 1; + } + else + { + tmpBlock.LastCharIndex = lastWrapCharIndex - 1; + nextCharIndex = currentIndex = lastWrapCharIndex; + } + } + } + else if (layout.TextWrapping == TextWrapping::WrapChars) + { + // Move line + moveLine = true; + moveBlock = tmpBlock.FirstCharIndex < currentChar; + nextCharIndex = currentIndex; + + // Skip moving twice for the same character + if (lastMoveLine) + break; + } + } + + if (moveBlock) { + // Add block + tmpBlock.Size.X = lastCursorX - cursorX; + tmpBlock.Size.Y = baseLinesDistanceScale * getFont(currentFontIndex)->GetHeight(); + tmpBlock.LastCharIndex = Math::Max(tmpBlock.LastCharIndex, tmpBlock.FirstCharIndex); + tmpBlock.FallbackFontIndex = currentFontIndex; + tmpLine.Blocks.Add(tmpBlock); + + // Reset block + tmpBlock.Location.X = cursorX; + tmpBlock.FirstCharIndex = nextCharIndex; + tmpBlock.LastCharIndex = nextCharIndex - 1; + + currentFontIndex = nextFontIndex; + lastCursorX = cursorX; + } + + // Check if move to another line + if (moveLine) + { + // Add line + tmpLine.Size.X = cursorX; + tmpLine.Size.Y = baseLinesDistanceScale * maxHeight; + tmpLine.MaxAscender = maxAscender; + outputLines.Add(tmpLine); + + // Reset line + tmpLine.Blocks.Clear(); + tmpLine.Location.Y += baseLinesDistanceScale * maxHeight; + cursorX = 0; + tmpBlock.Location.X = cursorX; + lastWrapCharIndex = INVALID_INDEX; + lastWrapCharX = 0; + previous.IsValid = false; + + // Reset max font height + maxHeight = 0; + maxAscender = 0; + lastCursorX = 0; + } + + currentIndex = nextCharIndex; + lastMoveLine = moveLine; + } + + // Check if an additional line should be created + if (text[textLength - 1] == '\n') + { + // Add line + tmpLine.Size.X = cursorX; + tmpLine.Size.Y = baseLinesDistanceScale * maxHeight; + outputLines.Add(tmpLine); + + tmpLine.Location.Y += baseLinesDistanceScale * maxHeight; + } + + // Check amount of lines + if (outputLines.IsEmpty()) + return; + + float totalHeight = tmpLine.Location.Y; + + Float2 offset = Float2::Zero; + if (layout.VerticalAlignment == TextAlignment::Center) + { + offset.Y += (layout.Bounds.GetHeight() - totalHeight) * 0.5f; + } + else if (layout.VerticalAlignment == TextAlignment::Far) + { + offset.Y += layout.Bounds.GetHeight() - totalHeight; + } + for (int32 i = 0; i < outputLines.Count(); i++) + { + BlockedTextLineCache& line = outputLines[i]; + Float2 rootPos = line.Location + offset; + + // Fix upper left line corner to match desire text alignment + if (layout.HorizontalAlignment == TextAlignment::Center) + { + rootPos.X += (layout.Bounds.GetWidth() - line.Size.X) * 0.5f; + } + else if (layout.HorizontalAlignment == TextAlignment::Far) + { + rootPos.X += layout.Bounds.GetWidth() - line.Size.X; + } + + line.Location = rootPos; + + // Align all blocks to center in case they have different heights + for (int32 j = 0; j < line.Blocks.Count(); j++) + { + FontBlockCache& block = line.Blocks[j]; + block.Location.Y += (line.MaxAscender - getFont(block.FallbackFontIndex)->GetAscender()) / 2; + } + } +} + Float2 Font::MeasureText(const StringView& text, const TextLayoutOptions& layout) { // Check if there is no need to do anything @@ -314,6 +569,27 @@ Float2 Font::MeasureText(const StringView& text, const TextLayoutOptions& layout return max; } +Float2 Font::MeasureText(FallbackFonts* fallbacks, const StringView& text, const TextLayoutOptions& layout) +{ + // Check if there is no need to do anything + if (text.IsEmpty()) + return Float2::Zero; + + // Process text + Array lines; + ProcessText(fallbacks, text, lines, layout); + + // Calculate bounds + Float2 max = Float2::Zero; + for (int32 i = 0; i < lines.Count(); i++) + { + const BlockedTextLineCache& line = lines[i]; + max = Float2::Max(max, line.Location + line.Size); + } + + return max; +} + int32 Font::HitTestText(const StringView& text, const Float2& location, const TextLayoutOptions& layout) { // Check if there is no need to do anything @@ -388,6 +664,106 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te return smallestIndex; } +int32 Font::HitTestText(FallbackFonts* fallbacks, const StringView& text, const Float2& location, const TextLayoutOptions& layout) +{ + // Check if there is no need to do anything + if (text.Length() <= 0) + return 0; + + // Process text + const Array& fallbackFonts = fallbacks->GetFontList(GetSize()); + Array lines; + ProcessText(fallbacks, text, lines, layout); + ASSERT(lines.HasItems()); + float scale = layout.Scale / FontManager::FontScale; + + // Offset position to match lines origin space + Float2 rootOffset = layout.Bounds.Location + lines.First().Location; + Float2 testPoint = location - rootOffset; + + // Get block which may intersect with the position (it's possible because lines have fixed height) + int32 lineIndex = 0; + while (lineIndex < lines.Count()) + { + if (lines[lineIndex].Location.Y + lines[lineIndex].Size.Y >= location.Y) { + break; + } + + lineIndex++; + } + lineIndex = Math::Clamp(lineIndex, 0, lines.Count() - 1); + const BlockedTextLineCache& line = lines[lineIndex]; + + int32 blockIndex = 0; + while (blockIndex < line.Blocks.Count() - 1) + { + if (line.Location.X + line.Blocks[blockIndex + 1].Location.X >= location.X) { + break; + } + + blockIndex++; + } + const FontBlockCache& block = line.Blocks[blockIndex]; + float x = line.Location.X; + + // Check all characters in the line to find hit point + FontCharacterEntry previous; + FontCharacterEntry entry; + int32 smallestIndex = INVALID_INDEX; + float dst, smallestDst = MAX_float; + + auto getFont = [&](int32 index)->Font* { + return index >= 0 ? fallbackFonts[index] : this; + }; + + for (int32 currentIndex = block.FirstCharIndex; currentIndex <= block.LastCharIndex; currentIndex++) + { + // Cache current character + const Char currentChar = text[currentIndex]; + + getFont(block.FallbackFontIndex)->GetCharacter(currentChar, entry); + const bool isWhitespace = StringUtils::IsWhitespace(currentChar); + + // Apply kerning + if (!isWhitespace && previous.IsValid) + { + x += getFont(block.FallbackFontIndex)->GetKerning(previous.Character, entry.Character); + } + previous = entry; + + // Test + dst = Math::Abs(testPoint.X - x); + if (dst < smallestDst) + { + // Found closer character + smallestIndex = currentIndex; + smallestDst = dst; + } + else if (dst > smallestDst) + { + // Current char is worse so return the best result + return smallestIndex; + } + + // Move + x += entry.AdvanceX * scale; + } + + // Test line end edge + dst = Math::Abs(testPoint.X - x); + if (dst < smallestDst) + { + // Pointer is behind the last character in the line + smallestIndex = block.LastCharIndex; + + // Fix for last line + if (lineIndex == lines.Count() - 1) + smallestIndex++; + } + + return smallestIndex; +} + Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayoutOptions& layout) { // Check if there is no need to do anything @@ -442,9 +818,68 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo return rootOffset + Float2(lines.Last().Location.X + lines.Last().Size.X, static_cast((lines.Count() - 1) * baseLinesDistance)); } -bool Font::ContainsChar(Char c) const +Float2 Font::GetCharPosition(FallbackFonts* fallbacks, const StringView& text, int32 index, const TextLayoutOptions& layout) { - return FT_Get_Char_Index(GetAsset()->GetFTFace(), c) > 0; + // Check if there is no need to do anything + if (text.IsEmpty()) + return layout.Bounds.Location; + + // Process text + const Array& fallbackFonts = fallbacks->GetFontList(GetSize()); + Array lines; + ProcessText(fallbacks, text, lines, layout); + ASSERT(lines.HasItems()); + float scale = layout.Scale / FontManager::FontScale; + float baseLinesDistance = layout.BaseLinesGapScale * scale; + Float2 rootOffset = layout.Bounds.Location; + + // Find line with that position + FontCharacterEntry previous; + FontCharacterEntry entry; + + auto getFont = [&](int32 index)->Font* { + return index >= 0 ? fallbackFonts[index] : this; + }; + + for (int32 lineIndex = 0; lineIndex < lines.Count(); lineIndex++) + { + const BlockedTextLineCache& line = lines[lineIndex]; + for (int32 blockIndex = 0; blockIndex < line.Blocks.Count(); blockIndex++) + { + const FontBlockCache& block = line.Blocks[blockIndex]; + // Check if desire position is somewhere inside characters in line range + if (Math::IsInRange(index, block.FirstCharIndex, block.LastCharIndex)) + { + float x = line.Location.X + block.Location.X; + float y = line.Location.Y + block.Location.Y; + + // Check all characters in the line + for (int32 currentIndex = block.FirstCharIndex; currentIndex < index; currentIndex++) + { + // Cache current character + const Char currentChar = text[currentIndex]; + getFont(block.FallbackFontIndex)->GetCharacter(currentChar, entry); + const bool isWhitespace = StringUtils::IsWhitespace(currentChar); + + // Apply kerning + if (!isWhitespace && previous.IsValid) + { + x += getFont(block.FallbackFontIndex)->GetKerning(previous.Character, entry.Character); + } + previous = entry; + + // Move + x += entry.AdvanceX * scale; + } + + // Upper left corner of the character + return rootOffset + Float2(x, y); + } + } + } + + // Position after last character in the last line + return rootOffset + Float2(lines.Last().Location.X + lines.Last().Size.X, lines.Last().Location.Y); } void Font::FlushFaceSize() const diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index 453872582..d932888fb 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -10,8 +10,9 @@ #include "TextLayoutOptions.h" class FontAsset; +class FallbackFonts; struct FontTextureAtlasSlot; -struct MultiFontLineCache; +struct BlockedTextLineCache; // The default DPI that engine is using #define DefaultDPI 96 @@ -120,6 +121,73 @@ struct TIsPODType enum { Value = true }; }; +/// +/// The font block info generated during text processing. +/// +API_STRUCT(NoDefault) struct FontBlockCache +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(FontBlockCache); + + /// + /// The root position of the block (upper left corner), relative to line. + /// + API_FIELD() Float2 Location; + + /// + /// The height of the current block + /// + API_FIELD() Float2 Size; + + /// + /// The first character index (from the input text). + /// + API_FIELD() int32 FirstCharIndex; + + /// + /// The last character index (from the input text), inclusive. + /// + API_FIELD() int32 LastCharIndex; + + /// + /// Indicates the fallback font to render this block with, -1 if doesn't require fallback. + /// + API_FIELD() int32 FallbackFontIndex; +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; +}; + +/// +/// Line of font blocks info generated during text processing. +/// +API_STRUCT(NoDefault) struct BlockedTextLineCache +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(BlockedTextLineCache); + + /// + /// The root position of the line (upper left corner). + /// + API_FIELD() Float2 Location; + + /// + /// The line bounds (width and height). + /// + API_FIELD() Float2 Size; + + /// + /// The maximum ascender of the line. + /// + API_FIELD() float MaxAscender; + + /// + /// The index of the font to render with + /// + API_FIELD() Array Blocks; +}; + // Font glyph metrics: // // xmin xmax @@ -388,6 +456,62 @@ public: return ProcessText(textRange.Substring(text), TextLayoutOptions()); } + /// + /// Processes text to get cached lines for rendering. + /// + /// The input text. + /// The layout properties. + /// The output lines list. + void ProcessText(FallbackFonts* fallbacks, const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout); + + /// + /// Processes text to get cached lines for rendering. + /// + /// The input text. + /// The layout properties. + /// The output lines list. + API_FUNCTION() Array ProcessText(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout) + { + Array lines; + ProcessText(fallbacks, text, lines, layout); + return lines; + } + + /// + /// Processes text to get cached lines for rendering. + /// + /// The input text. + /// The input text range (substring range of the input text parameter). + /// The layout properties. + /// The output lines list. + API_FUNCTION() Array ProcessText(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, API_PARAM(Ref) const TextLayoutOptions& layout) + { + Array lines; + ProcessText(fallbacks, textRange.Substring(text), lines, layout); + return lines; + } + + /// + /// Processes text to get cached lines for rendering. + /// + /// The input text. + /// The output lines list. + API_FUNCTION() FORCE_INLINE Array ProcessText(FallbackFonts* fallbacks, const StringView& text) + { + return ProcessText(fallbacks, text, TextLayoutOptions()); + } + + /// + /// Processes text to get cached lines for rendering. + /// + /// The input text. + /// The input text range (substring range of the input text parameter). + /// The output lines list. + API_FUNCTION() FORCE_INLINE Array ProcessText(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange) + { + return ProcessText(fallbacks, textRange.Substring(text), TextLayoutOptions()); + } + /// /// Measures minimum size of the rectangle that will be needed to draw given text. /// @@ -429,6 +553,56 @@ public: return MeasureText(textRange.Substring(text), TextLayoutOptions()); } + /// + /// Measures minimum size of the rectangle that will be needed to draw given text. + /// + /// The input text to test. + /// The layout properties. + /// The minimum size for that text and fot to render properly. + API_FUNCTION() Float2 MeasureText(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout); + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text. + /// + /// The input text to test. + /// The input text range (substring range of the input text parameter). + /// The layout properties. + /// The minimum size for that text and fot to render properly. + API_FUNCTION() Float2 MeasureText(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, API_PARAM(Ref) const TextLayoutOptions& layout) + { + return MeasureText(fallbacks, textRange.Substring(text), layout); + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text + /// . + /// The input text to test. + /// The minimum size for that text and fot to render properly. + API_FUNCTION() FORCE_INLINE Float2 MeasureText(FallbackFonts* fallbacks, const StringView& text) + { + return MeasureText(fallbacks, text, TextLayoutOptions()); + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text + /// . + /// The input text to test. + /// The input text range (substring range of the input text parameter). + /// The minimum size for that text and fot to render properly. + API_FUNCTION() FORCE_INLINE Float2 MeasureText(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange) + { + return MeasureText(fallbacks, textRange.Substring(text), TextLayoutOptions()); + } + + /// + /// Calculates hit character index at given location. + /// + /// The input text to test. + /// The input location to test. + /// The text layout properties. + /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). + API_FUNCTION() int32 HitTestText(const StringView& text, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout); + /// /// Calculates hit character index at given location. /// @@ -442,15 +616,6 @@ public: return HitTestText(textRange.Substring(text), location, layout); } - /// - /// Calculates hit character index at given location. - /// - /// The input text to test. - /// The input location to test. - /// The text layout properties. - /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). - API_FUNCTION() int32 HitTestText(const StringView& text, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout); - /// /// Calculates hit character index at given location. /// @@ -474,6 +639,51 @@ public: return HitTestText(textRange.Substring(text), location, TextLayoutOptions()); } + /// + /// Calculates hit character index at given location. + /// + /// The input text to test. + /// The input location to test. + /// The text layout properties. + /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). + API_FUNCTION() int32 HitTestText(FallbackFonts* fallbacks, const StringView& text, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout); + + /// + /// Calculates hit character index at given location. + /// + /// The input text to test. + /// The input text range (substring range of the input text parameter). + /// The input location to test. + /// The text layout properties. + /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). + API_FUNCTION() int32 HitTestText(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout) + { + return HitTestText(fallbacks, textRange.Substring(text), location, layout); + } + + /// + /// Calculates hit character index at given location. + /// + /// The input text to test. + /// The input location to test. + /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). + API_FUNCTION() FORCE_INLINE int32 HitTestText(FallbackFonts* fallbacks, const StringView& text, const Float2& location) + { + return HitTestText(fallbacks, text, location, TextLayoutOptions()); + } + + /// + /// Calculates hit character index at given location. + /// + /// The input text to test. + /// The input text range (substring range of the input text parameter). + /// The input location to test. + /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). + API_FUNCTION() FORCE_INLINE int32 HitTestText(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Float2& location) + { + return HitTestText(fallbacks, textRange.Substring(text), location, TextLayoutOptions()); + } + /// /// Calculates character position for given text and character index. /// @@ -520,11 +730,49 @@ public: } /// - /// Check if the font contains the glyph of a char + /// Calculates character position for given text and character index. /// - /// The char to test. - /// True if the font contains the glyph of the char, otherwise false. - API_FUNCTION() FORCE_INLINE bool ContainsChar(Char c) const; + /// The input text to test. + /// The text position to get coordinates of. + /// The text layout properties. + /// The character position (upper left corner which can be used for a caret position). + API_FUNCTION() Float2 GetCharPosition(FallbackFonts* fallbacks, const StringView& text, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout); + + /// + /// Calculates character position for given text and character index. + /// + /// The input text to test. + /// The input text range (substring range of the input text parameter). + /// The text position to get coordinates of. + /// The text layout properties. + /// The character position (upper left corner which can be used for a caret position). + API_FUNCTION() Float2 GetCharPosition(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout) + { + return GetCharPosition(fallbacks, textRange.Substring(text), index, layout); + } + + /// + /// Calculates character position for given text and character index + /// + /// The input text to test. + /// The text position to get coordinates of. + /// The character position (upper left corner which can be used for a caret position). + API_FUNCTION() FORCE_INLINE Float2 GetCharPosition(FallbackFonts* fallbacks, const StringView& text, int32 index) + { + return GetCharPosition(fallbacks, text, index, TextLayoutOptions()); + } + + /// + /// Calculates character position for given text and character index + /// + /// The input text to test. + /// The input text range (substring range of the input text parameter). + /// The text position to get coordinates of. + /// The character position (upper left corner which can be used for a caret position). + API_FUNCTION() FORCE_INLINE Float2 GetCharPosition(FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index) + { + return GetCharPosition(fallbacks, textRange.Substring(text), index, TextLayoutOptions()); + } /// /// Flushes the size of the face with the Free Type library backend. diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index a477e5f4d..1e94b19b1 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -199,6 +199,10 @@ bool FontAsset::Save(const StringView& path) #endif +bool FontAsset::ContainsChar(Char c) const { + return FT_Get_Char_Index(GetFTFace(), c) > 0; +} + void FontAsset::Invalidate() { ScopeLock lock(Locker); diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index 4dea84a5b..a773a8ad6 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -174,6 +174,13 @@ public: API_FUNCTION() bool Save(const StringView& path = StringView::Empty); #endif + /// + /// Check if the font contains the glyph of a char + /// + /// The char to test. + /// True if the font contains the glyph of the char, otherwise false. + API_FUNCTION() FORCE_INLINE bool ContainsChar(Char c) const; + /// /// Invalidates all cached dynamic font atlases using this font. Can be used to reload font characters after changing font asset options. /// diff --git a/Source/Engine/Render2D/MultiFont.cpp b/Source/Engine/Render2D/MultiFont.cpp deleted file mode 100644 index 4a510a9f8..000000000 --- a/Source/Engine/Render2D/MultiFont.cpp +++ /dev/null @@ -1,431 +0,0 @@ -#include "MultiFont.h" -#include "FontManager.h" -#include "Engine/Core/Math/Math.h" - -MultiFont::MultiFont(const Array& fonts) - : ManagedScriptingObject(SpawnParams(Guid::New(), Font::TypeInitializer)), - _fonts(fonts) -{ - -} - -void MultiFont::ProcessText(const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout) -{ - float cursorX = 0; - int32 kerning; - MultiFontLineCache tmpLine; - MultiFontBlockCache tmpBlock; - FontCharacterEntry entry; - FontCharacterEntry previous; - int32 textLength = text.Length(); - float scale = layout.Scale / FontManager::FontScale; - float boundsWidth = layout.Bounds.GetWidth(); - float baseLinesDistanceScale = layout.BaseLinesGapScale * scale; - - tmpBlock.Location = Float2::Zero; - tmpBlock.Size = Float2::Zero; - tmpBlock.FirstCharIndex = 0; - tmpBlock.LastCharIndex = -1; - - tmpLine.Location = Float2::Zero; - tmpLine.Size = Float2::Zero; - tmpLine.Blocks = Array(); - - if (textLength == 0) { - return; - } - - int32 lastWrapCharIndex = INVALID_INDEX; - float lastWrapCharX = 0; - bool lastMoveLine = false; - // The index of the font used by the current block - int32 currentFontIndex = GetCharFontIndex(text[0], 0); - // The maximum font height of the current line - float maxHeight = 0; - float maxAscender = 0; - float lastCursorX = 0; - - // Process each character to split text into single lines - for (int32 currentIndex = 0; currentIndex < textLength;) - { - bool moveLine = false; - bool moveBlock = false; - float xAdvance = 0; - int32 nextCharIndex = currentIndex + 1; - - // Submit line and block if text ends - if (nextCharIndex == textLength) { - moveLine = moveBlock = true; - } - - // Cache current character - const Char currentChar = text[currentIndex]; - const bool isWhitespace = StringUtils::IsWhitespace(currentChar); - - // Check if character can wrap words - const bool isWrapChar = !StringUtils::IsAlnum(currentChar) || isWhitespace || StringUtils::IsUpper(currentChar) || (currentChar >= 0x3040 && currentChar <= 0x9FFF); - if (isWrapChar && currentIndex != 0) - { - lastWrapCharIndex = currentIndex; - lastWrapCharX = cursorX; - } - - int32 nextFontIndex = currentFontIndex; - // Check if it's a newline character - if (currentChar == '\n') - { - // Break line - moveLine = moveBlock = true; - tmpBlock.LastCharIndex++; - } - else - { - // Get character entry - if (nextCharIndex < textLength) { - nextFontIndex = GetCharFontIndex(text[nextCharIndex], currentFontIndex); - } - - // Get character entry - _fonts[currentFontIndex]->GetCharacter(currentChar, entry); - maxHeight = Math::Max(maxHeight, static_cast(_fonts[currentFontIndex]->GetHeight())); - maxAscender = Math::Max(maxAscender, static_cast(_fonts[currentFontIndex]->GetAscender())); - - // Move block if the font changes or text ends - if (nextFontIndex != currentFontIndex || nextCharIndex == textLength) { - moveBlock = true; - } - - // Get kerning, only when the font hasn't changed - if (!isWhitespace && previous.IsValid && !moveBlock) - { - kerning = _fonts[currentFontIndex]->GetKerning(previous.Character, entry.Character); - } - else - { - kerning = 0; - } - previous = entry; - xAdvance = (kerning + entry.AdvanceX) * scale; - - // Check if character fits the line or skip wrapping - if (cursorX + xAdvance <= boundsWidth || layout.TextWrapping == TextWrapping::NoWrap) - { - // Move character - cursorX += xAdvance; - tmpBlock.LastCharIndex++; - } - else if (layout.TextWrapping == TextWrapping::WrapWords) - { - if (lastWrapCharIndex != INVALID_INDEX) - { - // Skip moving twice for the same character - int32 lastLineLastCharIndex = outputLines.HasItems() && outputLines.Last().Blocks.HasItems() ? outputLines.Last().Blocks.Last().LastCharIndex : -10000; - if (lastLineLastCharIndex == lastWrapCharIndex || lastLineLastCharIndex == lastWrapCharIndex - 1 || lastLineLastCharIndex == lastWrapCharIndex - 2) - { - currentIndex = nextCharIndex; - lastMoveLine = moveLine; - continue; - } - - // Move line - const Char wrapChar = text[lastWrapCharIndex]; - moveLine = true; - moveBlock = tmpBlock.FirstCharIndex < lastWrapCharIndex; - - cursorX = lastWrapCharX; - if (StringUtils::IsWhitespace(wrapChar)) - { - // Skip whitespaces - tmpBlock.LastCharIndex = lastWrapCharIndex - 1; - nextCharIndex = currentIndex = lastWrapCharIndex + 1; - } - else - { - tmpBlock.LastCharIndex = lastWrapCharIndex - 1; - nextCharIndex = currentIndex = lastWrapCharIndex; - } - } - } - else if (layout.TextWrapping == TextWrapping::WrapChars) - { - // Move line - moveLine = true; - moveBlock = tmpBlock.FirstCharIndex < currentChar; - nextCharIndex = currentIndex; - - // Skip moving twice for the same character - if (lastMoveLine) - break; - } - } - - if (moveBlock) { - // Add block - tmpBlock.Size.X = lastCursorX - cursorX; - tmpBlock.Size.Y = baseLinesDistanceScale * _fonts[currentFontIndex]->GetHeight(); - tmpBlock.LastCharIndex = Math::Max(tmpBlock.LastCharIndex, tmpBlock.FirstCharIndex); - tmpBlock.FontIndex = currentFontIndex; - tmpLine.Blocks.Add(tmpBlock); - - // Reset block - tmpBlock.Location.X = cursorX; - tmpBlock.FirstCharIndex = nextCharIndex; - tmpBlock.LastCharIndex = nextCharIndex - 1; - - currentFontIndex = nextFontIndex; - lastCursorX = cursorX; - } - - // Check if move to another line - if (moveLine) - { - // Add line - tmpLine.Size.X = cursorX; - tmpLine.Size.Y = baseLinesDistanceScale * maxHeight; - tmpLine.MaxAscender = maxAscender; - outputLines.Add(tmpLine); - - // Reset line - tmpLine.Blocks.Clear(); - tmpLine.Location.Y += baseLinesDistanceScale * maxHeight; - cursorX = 0; - tmpBlock.Location.X = cursorX; - lastWrapCharIndex = INVALID_INDEX; - lastWrapCharX = 0; - previous.IsValid = false; - - // Reset max font height - maxHeight = 0; - maxAscender = 0; - lastCursorX = 0; - } - - currentIndex = nextCharIndex; - lastMoveLine = moveLine; - } - - // Check if an additional line should be created - if (text[textLength - 1] == '\n') - { - // Add line - tmpLine.Size.X = cursorX; - tmpLine.Size.Y = baseLinesDistanceScale * maxHeight; - outputLines.Add(tmpLine); - - tmpLine.Location.Y += baseLinesDistanceScale * maxHeight; - } - - // Check amount of lines - if (outputLines.IsEmpty()) - return; - - float totalHeight = tmpLine.Location.Y; - - Float2 offset = Float2::Zero; - if (layout.VerticalAlignment == TextAlignment::Center) - { - offset.Y += (layout.Bounds.GetHeight() - totalHeight) * 0.5f; - } - else if (layout.VerticalAlignment == TextAlignment::Far) - { - offset.Y += layout.Bounds.GetHeight() - totalHeight; - } - for (int32 i = 0; i < outputLines.Count(); i++) - { - MultiFontLineCache& line = outputLines[i]; - Float2 rootPos = line.Location + offset; - - // Fix upper left line corner to match desire text alignment - if (layout.HorizontalAlignment == TextAlignment::Center) - { - rootPos.X += (layout.Bounds.GetWidth() - line.Size.X) * 0.5f; - } - else if (layout.HorizontalAlignment == TextAlignment::Far) - { - rootPos.X += layout.Bounds.GetWidth() - line.Size.X; - } - - line.Location = rootPos; - - // Align all blocks to center in case they have different heights - for (int32 j = 0; j < line.Blocks.Count(); j++) - { - MultiFontBlockCache& block = line.Blocks[j]; - block.Location.Y += (line.MaxAscender - _fonts[block.FontIndex]->GetAscender()) / 2; - } - } -} - -Float2 MultiFont::GetCharPosition(const StringView& text, int32 index, const TextLayoutOptions& layout) -{ - // Check if there is no need to do anything - if (text.IsEmpty()) - return layout.Bounds.Location; - - // Process text - Array lines; - ProcessText(text, lines, layout); - ASSERT(lines.HasItems()); - float scale = layout.Scale / FontManager::FontScale; - float baseLinesDistance = layout.BaseLinesGapScale * scale; - Float2 rootOffset = layout.Bounds.Location; - - // Find line with that position - FontCharacterEntry previous; - FontCharacterEntry entry; - for (int32 lineIndex = 0; lineIndex < lines.Count(); lineIndex++) - { - const MultiFontLineCache& line = lines[lineIndex]; - for (int32 blockIndex = 0; blockIndex < line.Blocks.Count(); blockIndex++) - { - const MultiFontBlockCache& block = line.Blocks[blockIndex]; - // Check if desire position is somewhere inside characters in line range - if (Math::IsInRange(index, block.FirstCharIndex, block.LastCharIndex)) - { - float x = line.Location.X + block.Location.X; - float y = line.Location.Y + block.Location.Y; - - // Check all characters in the line - for (int32 currentIndex = block.FirstCharIndex; currentIndex < index; currentIndex++) - { - // Cache current character - const Char currentChar = text[currentIndex]; - _fonts[block.FontIndex]->GetCharacter(currentChar, entry); - const bool isWhitespace = StringUtils::IsWhitespace(currentChar); - - // Apply kerning - if (!isWhitespace && previous.IsValid) - { - x += _fonts[block.FontIndex]->GetKerning(previous.Character, entry.Character); - } - previous = entry; - - // Move - x += entry.AdvanceX * scale; - } - - // Upper left corner of the character - return rootOffset + Float2(x, y); - } - } - } - - // Position after last character in the last line - return rootOffset + Float2(lines.Last().Location.X + lines.Last().Size.X, lines.Last().Location.Y); -} - -int32 MultiFont::HitTestText(const StringView& text, const Float2& location, const TextLayoutOptions& layout) -{ - // Check if there is no need to do anything - if (text.Length() <= 0) - return 0; - - // Process text - Array lines; - ProcessText(text, lines, layout); - ASSERT(lines.HasItems()); - float scale = layout.Scale / FontManager::FontScale; - - // Offset position to match lines origin space - Float2 rootOffset = layout.Bounds.Location + lines.First().Location; - Float2 testPoint = location - rootOffset; - - // Get block which may intersect with the position (it's possible because lines have fixed height) - int32 lineIndex = 0; - while (lineIndex < lines.Count()) - { - if (lines[lineIndex].Location.Y + lines[lineIndex].Size.Y >= location.Y) { - break; - } - - lineIndex++; - } - lineIndex = Math::Clamp(lineIndex, 0, lines.Count() - 1); - const MultiFontLineCache& line = lines[lineIndex]; - - int32 blockIndex = 0; - while (blockIndex < line.Blocks.Count() - 1) - { - if (line.Location.X + line.Blocks[blockIndex + 1].Location.X >= location.X) { - break; - } - - blockIndex++; - } - const MultiFontBlockCache& block = line.Blocks[blockIndex]; - float x = line.Location.X; - - // Check all characters in the line to find hit point - FontCharacterEntry previous; - FontCharacterEntry entry; - int32 smallestIndex = INVALID_INDEX; - float dst, smallestDst = MAX_float; - for (int32 currentIndex = block.FirstCharIndex; currentIndex <= block.LastCharIndex; currentIndex++) - { - // Cache current character - const Char currentChar = text[currentIndex]; - - _fonts[block.FontIndex]->GetCharacter(currentChar, entry); - const bool isWhitespace = StringUtils::IsWhitespace(currentChar); - - // Apply kerning - if (!isWhitespace && previous.IsValid) - { - x += _fonts[block.FontIndex]->GetKerning(previous.Character, entry.Character); - } - previous = entry; - - // Test - dst = Math::Abs(testPoint.X - x); - if (dst < smallestDst) - { - // Found closer character - smallestIndex = currentIndex; - smallestDst = dst; - } - else if (dst > smallestDst) - { - // Current char is worse so return the best result - return smallestIndex; - } - - // Move - x += entry.AdvanceX * scale; - } - - // Test line end edge - dst = Math::Abs(testPoint.X - x); - if (dst < smallestDst) - { - // Pointer is behind the last character in the line - smallestIndex = block.LastCharIndex; - - // Fix for last line - if (lineIndex == lines.Count() - 1) - smallestIndex++; - } - - return smallestIndex; -} - -Float2 MultiFont::MeasureText(const StringView& text, const TextLayoutOptions& layout) -{ - // Check if there is no need to do anything - if (text.IsEmpty()) - return Float2::Zero; - - // Process text - Array lines; - ProcessText(text, lines, layout); - - // Calculate bounds - Float2 max = Float2::Zero; - for (int32 i = 0; i < lines.Count(); i++) - { - const MultiFontLineCache& line = lines[i]; - max = Float2::Max(max, line.Location + line.Size); - } - - return max; -} - diff --git a/Source/Engine/Render2D/MultiFont.h b/Source/Engine/Render2D/MultiFont.h deleted file mode 100644 index 870b8e668..000000000 --- a/Source/Engine/Render2D/MultiFont.h +++ /dev/null @@ -1,353 +0,0 @@ -#pragma once - -#include "Engine/Core/Collections/Array.h" -#include "Engine/Core/Collections/Dictionary.h" -#include "Font.h" -#include "FontAsset.h" - -struct TextRange; -class Font; -class FontAsset; - -/// -/// The font block info generated during text processing. -/// -API_STRUCT(NoDefault) struct MultiFontBlockCache -{ - DECLARE_SCRIPTING_TYPE_MINIMAL(MultiFontBlockCache); - - /// - /// The root position of the block (upper left corner), relative to line. - /// - API_FIELD() Float2 Location; - - /// - /// The height of the current block - /// - API_FIELD() Float2 Size; - - /// - /// The first character index (from the input text). - /// - API_FIELD() int32 FirstCharIndex; - - /// - /// The last character index (from the input text), inclusive. - /// - API_FIELD() int32 LastCharIndex; - - /// - /// The index of the font to render with - /// - API_FIELD() int32 FontIndex; -}; - -template<> -struct TIsPODType -{ - enum { Value = true }; -}; - -/// -/// Line of font blocks info generated during text processing. -/// -API_STRUCT(NoDefault) struct MultiFontLineCache -{ - DECLARE_SCRIPTING_TYPE_MINIMAL(MultiFontLineCache); - - /// - /// The root position of the line (upper left corner). - /// - API_FIELD() Float2 Location; - - /// - /// The line bounds (width and height). - /// - API_FIELD() Float2 Size; - - /// - /// The maximum ascender of the line. - /// - API_FIELD() float MaxAscender; - - /// - /// The index of the font to render with - /// - API_FIELD() Array Blocks; -}; - -API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API MultiFont : public ManagedScriptingObject -{ - DECLARE_SCRIPTING_TYPE_NO_SPAWN(MultiFont); -private: - Array _fonts; - -public: - MultiFont(const Array& fonts); - - API_FUNCTION() FORCE_INLINE static MultiFont* Create(const Array& fonts) { - return New(fonts); - } - - API_FUNCTION() FORCE_INLINE static MultiFont* Create(const Array& fontAssets, float size) { - Array fonts; - fonts.Resize(fontAssets.Count()); - for (int32 i = 0; i < fontAssets.Count(); i++) - { - fonts[i] = fontAssets[i]->CreateFont(size); - } - - return New(fonts); - } - - API_PROPERTY() FORCE_INLINE Array& GetFonts() { - return _fonts; - } - - API_PROPERTY() FORCE_INLINE void SetFonts(const Array& val) { - _fonts = val; - } - - API_PROPERTY() FORCE_INLINE int32 GetMaxHeight() { - int32 maxHeight = 0; - for (int32 i = 0; i < _fonts.Count(); i++) - { - if (_fonts[i]) { - maxHeight = Math::Max(maxHeight, _fonts[i]->GetHeight()); - } - } - - return maxHeight; - } - - API_PROPERTY() FORCE_INLINE int32 GetMaxAscender() { - int32 maxAsc = 0; - for (int32 i = 0; i < _fonts.Count(); i++) - { - if (_fonts[i]) { - maxAsc = Math::Max(maxAsc, _fonts[i]->GetAscender()); - } - } - - return maxAsc; - } - - /// - /// Processes text to get cached lines for rendering. - /// - /// The input text. - /// The layout properties. - /// The output lines list. - void ProcessText(const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout); - - /// - /// Processes text to get cached lines for rendering. - /// - /// The input text. - /// The layout properties. - /// The output lines list. - API_FUNCTION() Array ProcessText(const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout) - { - Array lines; - ProcessText(text, lines, layout); - return lines; - } - - /// - /// Processes text to get cached lines for rendering. - /// - /// The input text. - /// The input text range (substring range of the input text parameter). - /// The layout properties. - /// The output lines list. - API_FUNCTION() Array ProcessText(const StringView& text, API_PARAM(Ref) const TextRange& textRange, API_PARAM(Ref) const TextLayoutOptions& layout) - { - Array lines; - ProcessText(textRange.Substring(text), lines, layout); - return lines; - } - - /// - /// Processes text to get cached lines for rendering. - /// - /// The input text. - /// The output lines list. - API_FUNCTION() FORCE_INLINE Array ProcessText(const StringView& text) - { - return ProcessText(text, TextLayoutOptions()); - } - - /// - /// Processes text to get cached lines for rendering. - /// - /// The input text. - /// The input text range (substring range of the input text parameter). - /// The output lines list. - API_FUNCTION() FORCE_INLINE Array ProcessText(const StringView& text, API_PARAM(Ref) const TextRange& textRange) - { - return ProcessText(textRange.Substring(text), TextLayoutOptions()); - } - - /// - /// Measures minimum size of the rectangle that will be needed to draw given text. - /// - /// The input text to test. - /// The layout properties. - /// The minimum size for that text and fot to render properly. - API_FUNCTION() Float2 MeasureText(const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout); - - /// - /// Measures minimum size of the rectangle that will be needed to draw given text. - /// - /// The input text to test. - /// The input text range (substring range of the input text parameter). - /// The layout properties. - /// The minimum size for that text and fot to render properly. - API_FUNCTION() Float2 MeasureText(const StringView& text, API_PARAM(Ref) const TextRange& textRange, API_PARAM(Ref) const TextLayoutOptions& layout) - { - return MeasureText(textRange.Substring(text), layout); - } - - /// - /// Measures minimum size of the rectangle that will be needed to draw given text - /// . - /// The input text to test. - /// The minimum size for that text and fot to render properly. - API_FUNCTION() FORCE_INLINE Float2 MeasureText(const StringView& text) - { - return MeasureText(text, TextLayoutOptions()); - } - - /// - /// Measures minimum size of the rectangle that will be needed to draw given text - /// . - /// The input text to test. - /// The input text range (substring range of the input text parameter). - /// The minimum size for that text and fot to render properly. - API_FUNCTION() FORCE_INLINE Float2 MeasureText(const StringView& text, API_PARAM(Ref) const TextRange& textRange) - { - return MeasureText(textRange.Substring(text), TextLayoutOptions()); - } - - /// - /// Calculates hit character index at given location. - /// - /// The input text to test. - /// The input text range (substring range of the input text parameter). - /// The input location to test. - /// The text layout properties. - /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). - API_FUNCTION() int32 HitTestText(const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout) - { - return HitTestText(textRange.Substring(text), location, layout); - } - - /// - /// Calculates hit character index at given location. - /// - /// The input text to test. - /// The input location to test. - /// The text layout properties. - /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). - API_FUNCTION() int32 HitTestText(const StringView& text, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout); - - /// - /// Calculates hit character index at given location. - /// - /// The input text to test. - /// The input location to test. - /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). - API_FUNCTION() FORCE_INLINE int32 HitTestText(const StringView& text, const Float2& location) - { - return HitTestText(text, location, TextLayoutOptions()); - } - - /// - /// Calculates hit character index at given location. - /// - /// The input text to test. - /// The input text range (substring range of the input text parameter). - /// The input location to test. - /// The selected character position index (can be equal to text length if location is outside of the layout rectangle). - API_FUNCTION() FORCE_INLINE int32 HitTestText(const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Float2& location) - { - return HitTestText(textRange.Substring(text), location, TextLayoutOptions()); - } - - /// - /// Calculates character position for given text and character index. - /// - /// The input text to test. - /// The text position to get coordinates of. - /// The text layout properties. - /// The character position (upper left corner which can be used for a caret position). - API_FUNCTION() Float2 GetCharPosition(const StringView& text, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout); - - /// - /// Calculates character position for given text and character index. - /// - /// The input text to test. - /// The input text range (substring range of the input text parameter). - /// The text position to get coordinates of. - /// The text layout properties. - /// The character position (upper left corner which can be used for a caret position). - API_FUNCTION() Float2 GetCharPosition(const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout) - { - return GetCharPosition(textRange.Substring(text), index, layout); - } - - /// - /// Calculates character position for given text and character index - /// - /// The input text to test. - /// The text position to get coordinates of. - /// The character position (upper left corner which can be used for a caret position). - API_FUNCTION() FORCE_INLINE Float2 GetCharPosition(const StringView& text, int32 index) - { - return GetCharPosition(text, index, TextLayoutOptions()); - } - - /// - /// Calculates character position for given text and character index - /// - /// The input text to test. - /// The input text range (substring range of the input text parameter). - /// The text position to get coordinates of. - /// The character position (upper left corner which can be used for a caret position). - API_FUNCTION() FORCE_INLINE Float2 GetCharPosition(const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index) - { - return GetCharPosition(textRange.Substring(text), index, TextLayoutOptions()); - } - - /// - /// Gets the index of the font that should be used to render the char - /// - /// The font list. - /// The char. - /// Number to return if char cannot be found. - /// - API_FUNCTION() FORCE_INLINE int32 GetCharFontIndex(Char c, int32 missing = -1) { - int32 fontIndex = 0; - while (fontIndex < _fonts.Count() && _fonts[fontIndex] && !_fonts[fontIndex]->ContainsChar(c)) - { - fontIndex++; - } - - if (fontIndex == _fonts.Count()) { - return missing; - } - - return fontIndex; - } - - API_FUNCTION() FORCE_INLINE bool Verify() { - for (int32 i = 0; i < _fonts.Count(); i++) - { - if (!_fonts[i]) { - return false; - } - } - - return true; - } -}; diff --git a/Source/Engine/Render2D/Render2D.cpp b/Source/Engine/Render2D/Render2D.cpp index c68c2445d..4e0cacb44 100644 --- a/Source/Engine/Render2D/Render2D.cpp +++ b/Source/Engine/Render2D/Render2D.cpp @@ -3,7 +3,7 @@ #include "Render2D.h" #include "Font.h" #include "FontManager.h" -#include "MultiFont.h" +#include "FallbackFonts.h" #include "FontTextureAtlas.h" #include "RotatedRectangle.h" #include "SpriteAtlas.h" @@ -195,7 +195,7 @@ namespace // Drawing Array DrawCalls; Array Lines; - Array MultiFontLines; + Array BlockedTextLines; Array Lines2; bool IsScissorsRectEmpty; bool IsScissorsRectEnabled; @@ -1370,16 +1370,16 @@ void Render2D::DrawText(Font* font, const StringView& text, const TextRange& tex DrawText(font, textRange.Substring(text), color, layout, customMaterial); } -void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial) +void Render2D::DrawText(Font* font, FallbackFonts* fallbacks, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial) { RENDER2D_CHECK_RENDERING_STATE; - const Array& fonts = multiFont->GetFonts(); // Check if there is no need to do anything - if (fonts.IsEmpty() || text.Length() < 0) + if (font == nullptr || text.Length() < 0) return; // Temporary data + const Array& fallbackFonts = fallbacks->GetFontList(font->GetSize()); uint32 fontAtlasIndex = 0; FontTextureAtlas* fontAtlas = nullptr; Float2 invAtlasSize = Float2::One; @@ -1406,11 +1406,19 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo int32 lineIndex = 0; maxAscenders.Add(0); + + auto getFont = [&](int32 index)->Font* { + return index >= 0 ? fallbackFonts[index] : font; + }; + + // Preprocess the text to determine vertical offset of blocks for (int32 currentIndex = 0; currentIndex < text.Length(); currentIndex++) { - if (text[currentIndex] != '\n') { - int32 fontIndex = multiFont->GetCharFontIndex(text[currentIndex], 0); - maxAscenders[lineIndex] = Math::Max(maxAscenders[lineIndex], static_cast(fonts[fontIndex]->GetAscender())); + const Char c = text[currentIndex]; + if (c != '\n') { + int32 fontIndex = fallbacks->GetCharFallbackIndex(c, font); + maxAscenders[lineIndex] = Math::Max(maxAscenders[lineIndex], + static_cast(getFont(fontIndex)->GetAscender())); } else { lineIndex++; @@ -1424,7 +1432,7 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo // The starting index of the current block int32 startIndex = 0; // The index of the font used by the current block - int32 currentFontIndex = multiFont->GetCharFontIndex(text[0], 0); + int32 currentFontIndex = fallbacks->GetCharFallbackIndex(text[0], font); // The maximum font height of the current line float maxHeight = 0; for (int32 currentIndex = 0; currentIndex < text.Length(); currentIndex++) @@ -1446,7 +1454,7 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo { // Get character entry if (nextCharIndex < text.Length()) { - nextFontIndex = multiFont->GetCharFontIndex(text[nextCharIndex], currentFontIndex); + nextFontIndex = fallbacks->GetCharFallbackIndex(text[nextCharIndex], font); } if (nextFontIndex != currentFontIndex) { @@ -1461,13 +1469,13 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo if (moveBlock) { // Render the pending block before beginning the new block - auto fontHeight = fonts[currentFontIndex]->GetHeight(); + auto fontHeight = getFont(currentFontIndex)->GetHeight(); maxHeight = Math::Max(maxHeight, static_cast(fontHeight)); - auto fontDescender = fonts[currentFontIndex]->GetDescender(); + auto fontDescender = getFont(currentFontIndex)->GetDescender(); for (int32 renderIndex = startIndex; renderIndex <= currentIndex; renderIndex++) { // Get character entry - fonts[currentFontIndex]->GetCharacter(text[renderIndex], entry); + getFont(currentFontIndex)->GetCharacter(text[renderIndex], entry); // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != fontAtlasIndex) @@ -1494,7 +1502,7 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo // Get kerning if (!isWhitespace && previous.IsValid) { - kerning = fonts[currentFontIndex]->GetKerning(previous.Character, entry.Character); + kerning = getFont(currentFontIndex)->GetKerning(previous.Character, entry.Character); } else { @@ -1510,7 +1518,7 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo const float x = pointer.X + entry.OffsetX * scale; const float y = pointer.Y + (fontHeight + fontDescender - entry.OffsetY) * scale; - Rectangle charRect(x, y + (maxAscenders[lineIndex] - fonts[currentFontIndex]->GetAscender()) / 2, entry.UVSize.X * scale, entry.UVSize.Y * scale); + Rectangle charRect(x, y + (maxAscenders[lineIndex] - getFont(currentFontIndex)->GetAscender()) / 2, entry.UVSize.X * scale, entry.UVSize.Y * scale); Float2 upperLeftUV = entry.UV * invAtlasSize; Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; @@ -1541,21 +1549,21 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo } } -void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial) +void Render2D::DrawText(Font* font, FallbackFonts* fallbacks, const StringView& text, const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial) { - DrawText(multiFont, textRange.Substring(text), color, location, customMaterial); + DrawText(font, fallbacks, textRange.Substring(text), color, location, customMaterial); } -void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) +void Render2D::DrawText(Font* font, FallbackFonts* fallbacks, const StringView& text, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) { RENDER2D_CHECK_RENDERING_STATE; - const Array& fonts = multiFont->GetFonts(); // Check if there is no need to do anything - if (fonts.IsEmpty() || text.IsEmpty() || layout.Scale <= ZeroTolerance) + if (font == nullptr || text.IsEmpty() || layout.Scale <= ZeroTolerance) return; // Temporary data + const Array& fallbackFonts = fallbacks->GetFontList(font->GetSize()); uint32 fontAtlasIndex = 0; FontTextureAtlas* fontAtlas = nullptr; Float2 invAtlasSize = Float2::One; @@ -1564,8 +1572,8 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo float scale = layout.Scale / FontManager::FontScale; // Process text to get lines - MultiFontLines.Clear(); - multiFont->ProcessText(text, MultiFontLines, layout); + BlockedTextLines.Clear(); + font->ProcessText(fallbacks, text, BlockedTextLines, layout); // Render all lines FontCharacterEntry entry; @@ -1581,14 +1589,18 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo drawCall.AsChar.Mat = nullptr; } - for (int32 lineIndex = 0; lineIndex < MultiFontLines.Count(); lineIndex++) + auto getFont = [&](int32 index)->Font* { + return index >= 0 ? fallbackFonts[index] : font; + }; + + for (int32 lineIndex = 0; lineIndex < BlockedTextLines.Count(); lineIndex++) { - const MultiFontLineCache& line = MultiFontLines[lineIndex]; + const BlockedTextLineCache& line = BlockedTextLines[lineIndex]; for (int32 blockIndex = 0; blockIndex < line.Blocks.Count(); blockIndex++) { - const MultiFontBlockCache& block = MultiFontLines[lineIndex].Blocks[blockIndex]; - auto fontHeight = fonts[block.FontIndex]->GetHeight(); - auto fontDescender = fonts[block.FontIndex]->GetDescender(); + const FontBlockCache& block = BlockedTextLines[lineIndex].Blocks[blockIndex]; + auto fontHeight = getFont(block.FallbackFontIndex)->GetHeight(); + auto fontDescender = getFont(block.FallbackFontIndex)->GetDescender(); Float2 pointer = line.Location + block.Location; for (int32 charIndex = block.FirstCharIndex; charIndex <= block.LastCharIndex; charIndex++) @@ -1600,7 +1612,7 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo } // Get character entry - fonts[block.FontIndex]->GetCharacter(c, entry); + getFont(block.FallbackFontIndex)->GetCharacter(c, entry); // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != fontAtlasIndex) @@ -1625,7 +1637,7 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo const bool isWhitespace = StringUtils::IsWhitespace(c); if (!isWhitespace && previous.IsValid) { - kerning = fonts[block.FontIndex]->GetKerning(previous.Character, entry.Character); + kerning = getFont(block.FallbackFontIndex)->GetKerning(previous.Character, entry.Character); } else { @@ -1661,9 +1673,9 @@ void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const Colo } } -void Render2D::DrawText(MultiFont* multiFont, const StringView& text, const TextRange& textRange, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) +void Render2D::DrawText(Font* font, FallbackFonts* fallbacks, const StringView& text, const TextRange& textRange, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) { - DrawText(multiFont, textRange.Substring(text), color, layout, customMaterial); + DrawText(font, fallbacks, textRange.Substring(text), color, layout, customMaterial); } FORCE_INLINE bool NeedAlphaWithTint(const Color& color) diff --git a/Source/Engine/Render2D/Render2D.h b/Source/Engine/Render2D/Render2D.h index 5050171b2..c99d38104 100644 --- a/Source/Engine/Render2D/Render2D.h +++ b/Source/Engine/Render2D/Render2D.h @@ -15,7 +15,7 @@ struct Matrix3x3; struct Viewport; struct TextRange; class Font; -class MultiFont; +class FallbackFonts; class GPUPipelineState; class GPUTexture; class GPUTextureView; @@ -225,7 +225,7 @@ public: /// The text color. /// The text location. /// The custom material for font characters rendering. It must contain texture parameter named Font used to sample font texture. - API_FUNCTION() static void DrawText(MultiFont* multiFont, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); + API_FUNCTION() static void DrawText(Font* font, FallbackFonts* fallbacks, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); /// /// Draws a text with formatting. @@ -235,7 +235,7 @@ public: /// The text color. /// The text layout properties. /// The custom material for font characters rendering. It must contain texture parameter named Font used to sample font texture. - API_FUNCTION() static void DrawText(MultiFont* multiFont, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); + API_FUNCTION() static void DrawText(Font* font, FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); /// /// Draws a text with formatting. @@ -246,7 +246,7 @@ public: /// The text color. /// The text layout properties. /// The custom material for font characters rendering. It must contain texture parameter named Font used to sample font texture. - API_FUNCTION() static void DrawText(MultiFont* multiFont, const StringView& text, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); + API_FUNCTION() static void DrawText(Font* font, FallbackFonts* fallbacks, const StringView& text, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); /// /// Draws a text with formatting. @@ -257,7 +257,7 @@ public: /// The text color. /// The text layout properties. /// The custom material for font characters rendering. It must contain texture parameter named Font used to sample font texture. - API_FUNCTION() static void DrawText(MultiFont* multiFont, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); + API_FUNCTION() static void DrawText(Font* font, FallbackFonts* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); /// /// Fills a rectangle area.