From a3bc394e4e0279c51b3acb133a6ad8ef5a2f27bf Mon Sep 17 00:00:00 2001 From: ExMatics HydrogenC <33123710+HydrogenC@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:04:29 +0800 Subject: [PATCH] Fix a bunch of rendering bugs --- .gitignore | 1 + Source/Editor/EditorAssets.cs | 2 +- Source/Engine/Render2D/Font.cpp | 133 ++++++++++---- Source/Engine/Render2D/Font.h | 39 +++- Source/Engine/Render2D/MultiFont.cpp | 1 + Source/Engine/Render2D/MultiFont.h | 77 ++++++++ Source/Engine/Render2D/Render2D.cpp | 266 +++++++++++++-------------- Source/Engine/UI/GUI/Common/Label.cs | 2 +- 8 files changed, 332 insertions(+), 189 deletions(-) create mode 100644 Source/Engine/Render2D/MultiFont.cpp create mode 100644 Source/Engine/Render2D/MultiFont.h diff --git a/.gitignore b/.gitignore index b7e11e554..b653b7f77 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,4 @@ obj/ .idea/ *.code-workspace omnisharp.json +Content/Editor/Fonts/NotoSansSC-Regular.flax diff --git a/Source/Editor/EditorAssets.cs b/Source/Editor/EditorAssets.cs index 237908a0c..0fe5ee47e 100644 --- a/Source/Editor/EditorAssets.cs +++ b/Source/Editor/EditorAssets.cs @@ -54,7 +54,7 @@ namespace FlaxEditor /// public static string PrimaryFont = "Editor/Fonts/Roboto-Regular"; - public static string CJKFont = "Editor/Fonts/NotoSansSC-Medium"; + public static string CJKFont = "Editor/Fonts/NotoSansSC-Regular"; /// /// The Inconsolata Regular font. diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index 13b17197c..277ad8ddd 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -6,6 +6,7 @@ #include "Engine/Core/Log.h" #include "Engine/Threading/Threading.h" #include "IncludeFreeType.h" +#include "MultiFont.h" Font::Font(FontAsset* parentAsset, float size) : ManagedScriptingObject(SpawnParams(Guid::New(), Font::TypeInitializer)) @@ -118,6 +119,10 @@ void Font::ProcessText(const StringView& text, Array& outputLines tmpLine.FirstCharIndex = 0; tmpLine.LastCharIndex = -1; + if (textLength == 0) { + return; + } + int32 lastWrapCharIndex = INVALID_INDEX; float lastWrapCharX = 0; bool lastMoveLine = false; @@ -129,6 +134,11 @@ void Font::ProcessText(const StringView& text, Array& outputLines float xAdvance = 0; int32 nextCharIndex = currentIndex + 1; + // Submit line if text ends + if (nextCharIndex == textLength) { + moveLine = true; + } + // Cache current character const Char currentChar = text[currentIndex]; const bool isWhitespace = StringUtils::IsWhitespace(currentChar); @@ -146,7 +156,6 @@ void Font::ProcessText(const StringView& text, Array& outputLines { // Break line moveLine = true; - currentIndex++; tmpLine.LastCharIndex++; } else @@ -178,8 +187,8 @@ void Font::ProcessText(const StringView& text, Array& outputLines if (lastWrapCharIndex != INVALID_INDEX) { // Skip moving twice for the same character - int32 lastLineLasCharIndex = outputLines.HasItems() ? outputLines.Last().LastCharIndex : -10000; - if (lastLineLasCharIndex == lastWrapCharIndex || lastLineLasCharIndex == lastWrapCharIndex - 1 || lastLineLasCharIndex == lastWrapCharIndex - 2) + int32 lastLineLastCharIndex = outputLines.HasItems() ? outputLines.Last().LastCharIndex : -10000; + if (lastLineLastCharIndex == lastWrapCharIndex || lastLineLastCharIndex == lastWrapCharIndex - 1 || lastLineLastCharIndex == lastWrapCharIndex - 2) { currentIndex = nextCharIndex; lastMoveLine = moveLine; @@ -226,8 +235,8 @@ void Font::ProcessText(const StringView& text, Array& outputLines // Reset line tmpLine.Location.Y += baseLinesDistance; - tmpLine.FirstCharIndex = currentIndex; - tmpLine.LastCharIndex = currentIndex - 1; + tmpLine.FirstCharIndex = nextCharIndex; + tmpLine.LastCharIndex = nextCharIndex - 1; cursorX = 0; lastWrapCharIndex = INVALID_INDEX; lastWrapCharX = 0; @@ -238,7 +247,8 @@ void Font::ProcessText(const StringView& text, Array& outputLines lastMoveLine = moveLine; } - if (textLength != 0 && (tmpLine.LastCharIndex >= tmpLine.FirstCharIndex || text[textLength - 1] == '\n')) + // Check if an additional line should be created + if (text[textLength - 1] == '\n') { // Add line tmpLine.Size.X = cursorX; @@ -283,84 +293,101 @@ void Font::ProcessText(const StringView& text, Array& outputLines } } -void Font::ProcessText(const Array& fonts, const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout) +void Font::ProcessText(const Array& fonts, const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout) { float cursorX = 0; int32 kerning; - FontLineCache tmpLine; + MultiFontLineCache tmpLine; + MultiFontSegmentCache tmpSegment; FontCharacterEntry entry; FontCharacterEntry previous; int32 textLength = text.Length(); float scale = layout.Scale / FontManager::FontScale; float boundsWidth = layout.Bounds.GetWidth(); float baseLinesDistanceScale = layout.BaseLinesGapScale * scale; + + tmpSegment.Location = Float2::Zero; + tmpSegment.Height = 0; + tmpSegment.FirstCharIndex = 0; + tmpSegment.LastCharIndex = -1; + tmpLine.Location = Float2::Zero; tmpLine.Size = Float2::Zero; - tmpLine.FirstCharIndex = 0; - tmpLine.LastCharIndex = -1; + tmpLine.Segments = Array(); + + if (textLength == 0) { + return; + } int32 lastWrapCharIndex = INVALID_INDEX; float lastWrapCharX = 0; bool lastMoveLine = false; - - int32 previousFontIndex = -1; + // The index of the font used by the current segment + int32 currentFontIndex = GetCharFontIndex(fonts, text[0], 0); // The maximum font height of the current line float maxHeight = 0; + float maxAscender = 0; + // Process each character to split text into single lines for (int32 currentIndex = 0; currentIndex < textLength;) { bool moveLine = false; + bool moveSegment = false; float xAdvance = 0; int32 nextCharIndex = currentIndex + 1; + // Submit line and segment if text ends + if (nextCharIndex == textLength) { + moveLine = moveSegment = 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); + 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 = true; - currentIndex++; - tmpLine.LastCharIndex++; + moveLine = moveSegment = true; + tmpSegment.LastCharIndex++; } else { // Get character entry - int32 fontIndex = 0; - while (fontIndex < fonts.Count() && !fonts[fontIndex]->ContainsChar(currentChar)) - { - fontIndex++; + if (nextCharIndex < textLength) { + nextFontIndex = GetCharFontIndex(fonts, text[nextCharIndex], currentFontIndex); } - // If no font can match the char, then use the first font - if (fontIndex == fonts.Count()) { - fontIndex = 0; - } // Get character entry - fonts[fontIndex]->GetCharacter(currentChar, entry); - maxHeight = Math::Max(maxHeight, static_cast(fonts[fontIndex]->GetHeight())); + fonts[currentFontIndex]->GetCharacter(currentChar, entry); + maxHeight = Math::Max(maxHeight, static_cast(fonts[currentFontIndex]->GetHeight())); + maxAscender = Math::Max(maxAscender, static_cast(fonts[currentFontIndex]->GetAscender())); + + // Move segment if the font changes or text ends + if (nextFontIndex != currentFontIndex || nextCharIndex == textLength) { + moveSegment = true; + } // Get kerning, only when the font hasn't changed - if (!isWhitespace && previous.IsValid && previousFontIndex == fontIndex) + if (!isWhitespace && previous.IsValid && !moveSegment) { - kerning = fonts[fontIndex]->GetKerning(previous.Character, entry.Character); + kerning = fonts[currentFontIndex]->GetKerning(previous.Character, entry.Character); } else { kerning = 0; } previous = entry; - previousFontIndex = fontIndex; xAdvance = (kerning + entry.AdvanceX) * scale; // Check if character fits the line or skip wrapping @@ -368,15 +395,15 @@ void Font::ProcessText(const Array& fonts, const StringView& text, Array< { // Move character cursorX += xAdvance; - tmpLine.LastCharIndex++; + tmpSegment.LastCharIndex++; } else if (layout.TextWrapping == TextWrapping::WrapWords) { if (lastWrapCharIndex != INVALID_INDEX) { // Skip moving twice for the same character - int32 lastLineLasCharIndex = outputLines.HasItems() ? outputLines.Last().LastCharIndex : -10000; - if (lastLineLasCharIndex == lastWrapCharIndex || lastLineLasCharIndex == lastWrapCharIndex - 1 || lastLineLasCharIndex == lastWrapCharIndex - 2) + int32 lastLineLastCharIndex = outputLines.HasItems() && outputLines.Last().Segments.HasItems() ? outputLines.Last().Segments.Last().LastCharIndex : -10000; + if (lastLineLastCharIndex == lastWrapCharIndex || lastLineLastCharIndex == lastWrapCharIndex - 1 || lastLineLastCharIndex == lastWrapCharIndex - 2) { currentIndex = nextCharIndex; lastMoveLine = moveLine; @@ -386,16 +413,18 @@ void Font::ProcessText(const Array& fonts, const StringView& text, Array< // Move line const Char wrapChar = text[lastWrapCharIndex]; moveLine = true; + moveSegment = tmpSegment.FirstCharIndex < lastWrapCharIndex; + cursorX = lastWrapCharX; if (StringUtils::IsWhitespace(wrapChar)) { // Skip whitespaces - tmpLine.LastCharIndex = lastWrapCharIndex - 1; + tmpSegment.LastCharIndex = lastWrapCharIndex - 1; nextCharIndex = currentIndex = lastWrapCharIndex + 1; } else { - tmpLine.LastCharIndex = lastWrapCharIndex - 1; + tmpSegment.LastCharIndex = lastWrapCharIndex - 1; nextCharIndex = currentIndex = lastWrapCharIndex; } } @@ -404,6 +433,7 @@ void Font::ProcessText(const Array& fonts, const StringView& text, Array< { // Move line moveLine = true; + moveSegment = tmpSegment.FirstCharIndex < currentChar; nextCharIndex = currentIndex; // Skip moving twice for the same character @@ -412,38 +442,54 @@ void Font::ProcessText(const Array& fonts, const StringView& text, Array< } } + if (moveSegment) { + // Add segment + tmpSegment.Height = baseLinesDistanceScale * fonts[currentFontIndex]->GetHeight(); + tmpSegment.LastCharIndex = Math::Max(tmpSegment.LastCharIndex, tmpSegment.FirstCharIndex); + tmpSegment.FontIndex = currentFontIndex; + tmpLine.Segments.Add(tmpSegment); + + // Reset segment + tmpSegment.Location.X = cursorX; + tmpSegment.FirstCharIndex = nextCharIndex; + tmpSegment.LastCharIndex = nextCharIndex - 1; + + currentFontIndex = nextFontIndex; + } + // Check if move to another line if (moveLine) { // Add line tmpLine.Size.X = cursorX; tmpLine.Size.Y = baseLinesDistanceScale * maxHeight; - tmpLine.LastCharIndex = Math::Max(tmpLine.LastCharIndex, tmpLine.FirstCharIndex); + tmpLine.MaxAscender = maxAscender; outputLines.Add(tmpLine); // Reset line + tmpLine.Segments.Clear(); tmpLine.Location.Y += baseLinesDistanceScale * maxHeight; - tmpLine.FirstCharIndex = currentIndex; - tmpLine.LastCharIndex = currentIndex - 1; cursorX = 0; + tmpSegment.Location.X = cursorX; lastWrapCharIndex = INVALID_INDEX; lastWrapCharX = 0; previous.IsValid = false; // Reset max font height maxHeight = 0; + maxAscender = 0; } currentIndex = nextCharIndex; lastMoveLine = moveLine; } - if (textLength != 0 && (tmpLine.LastCharIndex >= tmpLine.FirstCharIndex || text[textLength - 1] == '\n')) + // Check if an additional line should be created + if (text[textLength - 1] == '\n') { // Add line tmpLine.Size.X = cursorX; tmpLine.Size.Y = baseLinesDistanceScale * maxHeight; - tmpLine.LastCharIndex = textLength - 1; outputLines.Add(tmpLine); tmpLine.Location.Y += baseLinesDistanceScale * maxHeight; @@ -466,7 +512,7 @@ void Font::ProcessText(const Array& fonts, const StringView& text, Array< } for (int32 i = 0; i < outputLines.Count(); i++) { - FontLineCache& line = outputLines[i]; + MultiFontLineCache& line = outputLines[i]; Float2 rootPos = line.Location + offset; // Fix upper left line corner to match desire text alignment @@ -480,6 +526,13 @@ void Font::ProcessText(const Array& fonts, const StringView& text, Array< } line.Location = rootPos; + + // Align all segments to center in case they have different heights + for (int32 j = 0; j < line.Segments.Count(); j++) + { + MultiFontSegmentCache& segment = line.Segments[j]; + segment.Location.Y += (line.MaxAscender - fonts[segment.FontIndex]->GetAscender()) / 2; + } } } diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index d8ac1b708..0425f2bc5 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -8,9 +8,11 @@ #include "Engine/Content/AssetReference.h" #include "Engine/Scripting/ScriptingObject.h" #include "TextLayoutOptions.h" +#include "MultiFont.h" class FontAsset; struct FontTextureAtlasSlot; +struct MultiFontLineCache; // The default DPI that engine is using #define DefaultDPI 96 @@ -20,7 +22,7 @@ struct FontTextureAtlasSlot; /// API_STRUCT(NoDefault) struct TextRange { -DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange); + DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange); /// /// The start index (inclusive). @@ -90,7 +92,7 @@ struct TIsPODType /// API_STRUCT(NoDefault) struct FontLineCache { -DECLARE_SCRIPTING_TYPE_MINIMAL(FontLineCache); + DECLARE_SCRIPTING_TYPE_MINIMAL(FontLineCache); /// /// The root position of the line (upper left corner). @@ -108,7 +110,7 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(FontLineCache); API_FIELD() int32 FirstCharIndex; /// - /// The last character index (from the input text). + /// The last character index (from the input text), inclusive. /// API_FIELD() int32 LastCharIndex; }; @@ -154,7 +156,7 @@ struct TIsPODType /// API_STRUCT(NoDefault) struct FontCharacterEntry { -DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); + DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); /// /// The character represented by this entry. @@ -223,7 +225,7 @@ struct TIsPODType /// API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API Font : public ManagedScriptingObject { -DECLARE_SCRIPTING_TYPE_NO_SPAWN(Font); + DECLARE_SCRIPTING_TYPE_NO_SPAWN(Font); friend FontAsset; private: @@ -346,7 +348,7 @@ public: /// The input text. /// The layout properties. /// The output lines list. - static void ProcessText(const Array& fonts, const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout); + static void ProcessText(const Array& fonts, const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout); /// /// Processes text to get cached lines for rendering. @@ -404,6 +406,7 @@ public: /// 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. /// @@ -412,6 +415,7 @@ public: /// The layout properties. /// The minimum size for that text and fot to render properly. API_FUNCTION() static Float2 MeasureText(const Array& fonts, const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout); + */ /// /// Measures minimum size of the rectangle that will be needed to draw given text. @@ -500,6 +504,7 @@ public: /// 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. /// @@ -509,6 +514,7 @@ public: /// The text layout properties. /// The character position (upper left corner which can be used for a caret position). API_FUNCTION() static Float2 GetCharPosition(const Array& fonts, const StringView& text, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout); + */ /// /// Calculates character position for given text and character index. @@ -553,6 +559,27 @@ public: /// True if the font contains the glyph of the char, otherwise false. API_FUNCTION() FORCE_INLINE bool ContainsChar(Char c); + /// + /// 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 static int32 GetCharFontIndex(const Array& fonts, Char c, int32 missing = -1) { + int32 fontIndex = 0; + while (fontIndex < fonts.Count() && !fonts[fontIndex]->ContainsChar(c)) + { + fontIndex++; + } + + if (fontIndex == fonts.Count()) { + return missing; + } + + return fontIndex; + } + /// /// Flushes the size of the face with the Free Type library backend. /// diff --git a/Source/Engine/Render2D/MultiFont.cpp b/Source/Engine/Render2D/MultiFont.cpp new file mode 100644 index 000000000..9844c12e0 --- /dev/null +++ b/Source/Engine/Render2D/MultiFont.cpp @@ -0,0 +1 @@ +#include "MultiFont.h" diff --git a/Source/Engine/Render2D/MultiFont.h b/Source/Engine/Render2D/MultiFont.h new file mode 100644 index 000000000..b1da832d5 --- /dev/null +++ b/Source/Engine/Render2D/MultiFont.h @@ -0,0 +1,77 @@ +#pragma once + +#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Collections/Dictionary.h" +#include "Font.h" + +/// +/// The font segment info generated during text processing. +/// +API_STRUCT(NoDefault) struct MultiFontSegmentCache +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(MultiFontSegmentCache); + + /// + /// The root position of the segment (upper left corner), relative to line. + /// + API_FIELD() Float2 Location; + + /// + /// The height of the current segment + /// + API_FIELD() float Height; + + /// + /// 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 segments 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 ascendent of the line. + /// + API_FIELD() float MaxAscender; + + /// + /// The index of the font to render with + /// + API_FIELD() Array Segments; +}; + +API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API MultiFont : public ManagedScriptingObject +{ + DECLARE_SCRIPTING_TYPE_NO_SPAWN(MultiFont); +}; diff --git a/Source/Engine/Render2D/Render2D.cpp b/Source/Engine/Render2D/Render2D.cpp index eedfbeffe..8a39d29be 100644 --- a/Source/Engine/Render2D/Render2D.cpp +++ b/Source/Engine/Render2D/Render2D.cpp @@ -3,6 +3,7 @@ #include "Render2D.h" #include "Font.h" #include "FontManager.h" +#include "MultiFont.h" #include "FontTextureAtlas.h" #include "RotatedRectangle.h" #include "SpriteAtlas.h" @@ -194,6 +195,7 @@ namespace // Drawing Array DrawCalls; Array Lines; + Array MultiFontLines; Array Lines2; bool IsScissorsRectEmpty; bool IsScissorsRectEnabled; @@ -1384,6 +1386,9 @@ void Render2D::DrawText(const Array& fonts, const StringView& text, const int32 kerning; float scale = 1.0f / FontManager::FontScale; + // Process text to get lines + Array maxAscenders; + // Render all characters FontCharacterEntry entry; Render2DDrawCall drawCall; @@ -1398,48 +1403,70 @@ void Render2D::DrawText(const Array& fonts, const StringView& text, const drawCall.AsChar.Mat = nullptr; } + int32 lineIndex = 0; + maxAscenders.Add(0); + for (int32 currentIndex = 0; currentIndex < text.Length(); currentIndex++) + { + if (text[currentIndex] != '\n') { + int32 fontIndex = Font::GetCharFontIndex(fonts, text[currentIndex], 0); + maxAscenders[lineIndex] = Math::Max(maxAscenders[lineIndex], static_cast(fonts[fontIndex]->GetAscender())); + } + else { + lineIndex++; + maxAscenders.Add(0); + } + } + + lineIndex = 0; // The following code cut the text into segments, according to the font used to render Float2 pointer = location; // The starting index of the current segment int32 startIndex = 0; // The index of the font used by the current segment - int32 segmentFontIndex = 0; + int32 currentFontIndex = Font::GetCharFontIndex(fonts, text[0], 0); // The maximum font height of the current line float maxHeight = 0; - for (int32 currentIndex = 0; currentIndex <= text.Length(); currentIndex++) + for (int32 currentIndex = 0; currentIndex < text.Length(); currentIndex++) { // Cache current character - const Char currentChar = currentIndex < text.Length() ? text[currentIndex] : 0; + const Char currentChar = text[currentIndex]; + int32 nextCharIndex = currentIndex + 1; + bool moveSegment = false; + bool moveLine = false; + int32 nextFontIndex = currentFontIndex; + + // Submit segment if text ends + if (nextCharIndex == text.Length()) { + moveSegment = true; + } // Check if it isn't a newline character if (currentChar != '\n') { - int32 fontIndex = 0; - if (currentIndex < text.Length()) { - while (fontIndex < fonts.Count() && !fonts[fontIndex]->ContainsChar(currentChar)) - { - fontIndex++; - } - - // If no font can match the char, then use the segment font - if (fontIndex == fonts.Count()) { - fontIndex = segmentFontIndex; - } - - // Do nothing if the char still belongs to the current segment - if (fontIndex == segmentFontIndex) { - continue; - } + // Get character entry + if (nextCharIndex < text.Length()) { + nextFontIndex = Font::GetCharFontIndex(fonts, text[nextCharIndex], currentFontIndex); } + if (nextFontIndex != currentFontIndex) { + moveSegment = true; + } + } + else + { + // Move + moveLine = moveSegment = true; + } + + if (moveSegment) { // Render the pending segment before beginning the new segment - renderText:auto fontHeight = fonts[segmentFontIndex]->GetHeight(); + auto fontHeight = fonts[currentFontIndex]->GetHeight(); maxHeight = Math::Max(maxHeight, static_cast(fontHeight)); - auto fontDescender = fonts[segmentFontIndex]->GetDescender(); - for (int32 renderIndex = startIndex; renderIndex < currentIndex; renderIndex++) + auto fontDescender = fonts[currentFontIndex]->GetDescender(); + for (int32 renderIndex = startIndex; renderIndex <= currentIndex; renderIndex++) { // Get character entry - fonts[segmentFontIndex]->GetCharacter(text[renderIndex], entry); + fonts[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) @@ -1466,7 +1493,7 @@ void Render2D::DrawText(const Array& fonts, const StringView& text, const // Get kerning if (!isWhitespace && previous.IsValid) { - kerning = fonts[segmentFontIndex]->GetKerning(previous.Character, entry.Character); + kerning = fonts[currentFontIndex]->GetKerning(previous.Character, entry.Character); } else { @@ -1482,7 +1509,7 @@ void Render2D::DrawText(const Array& fonts, const StringView& text, const const float x = pointer.X + entry.OffsetX * scale; const float y = pointer.Y + (fontHeight + fontDescender - entry.OffsetY) * scale; - Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); + Rectangle charRect(x, y + (maxAscenders[lineIndex] - fonts[currentFontIndex]->GetAscender()) / 2, entry.UVSize.X * scale, entry.UVSize.Y * scale); Float2 upperLeftUV = entry.UV * invAtlasSize; Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; @@ -1498,22 +1525,17 @@ void Render2D::DrawText(const Array& fonts, const StringView& text, const pointer.X += entry.AdvanceX * scale; } - // Start new segment - startIndex = currentIndex; - segmentFontIndex = fontIndex; - - if (currentIndex == text.Length() - 1) { - currentIndex++; - goto renderText; + if (moveLine) { + pointer.X = location.X; + pointer.Y += maxHeight * scale; + // Clear max height + maxHeight = 0; + lineIndex++; } - } - else - { - // Move - pointer.X = location.X; - pointer.Y += maxHeight * scale; - // Clear max height - maxHeight = 0; + + // Start new segment + startIndex = nextCharIndex; + currentFontIndex = nextFontIndex; } } } @@ -1540,8 +1562,8 @@ void Render2D::DrawText(const Array& fonts, const StringView& text, const float scale = layout.Scale / FontManager::FontScale; // Process text to get lines - Lines.Clear(); - Font::ProcessText(fonts, text, Lines, layout); + MultiFontLines.Clear(); + Font::ProcessText(fonts, text, MultiFontLines, layout); // Render all lines FontCharacterEntry entry; @@ -1557,119 +1579,81 @@ void Render2D::DrawText(const Array& fonts, const StringView& text, const drawCall.AsChar.Mat = nullptr; } - for (int32 lineIndex = 0; lineIndex < Lines.Count(); lineIndex++) + for (int32 lineIndex = 0; lineIndex < MultiFontLines.Count(); lineIndex++) { - const FontLineCache& line = Lines[lineIndex]; - - // The following code cut the text into segments, according to the font used to render - Float2 pointer = line.Location; - // The starting index of the current segment - int32 startIndex = line.FirstCharIndex; - // The index of the font used by the current segment - int32 segmentFontIndex = 0; - // The maximum font height of the current line - float maxHeight = 0; - - // Partition and render all characters from the line - for (int32 charIndex = line.FirstCharIndex; charIndex <= line.LastCharIndex + 1; charIndex++) + const MultiFontLineCache& line = MultiFontLines[lineIndex]; + for (int32 segmentIndex = 0; segmentIndex < line.Segments.Count(); segmentIndex++) { - const Char c = charIndex <= line.LastCharIndex ? text[charIndex] : 0; + const MultiFontSegmentCache& segment = MultiFontLines[lineIndex].Segments[segmentIndex]; + auto fontHeight = fonts[segment.FontIndex]->GetHeight(); + auto fontDescender = fonts[segment.FontIndex]->GetDescender(); + Float2 pointer = line.Location + segment.Location; - if (c != '\n') + for (int32 charIndex = segment.FirstCharIndex; charIndex <= segment.LastCharIndex; charIndex++) { - int32 fontIndex = 0; - if (charIndex <= line.LastCharIndex) { - while (fontIndex < fonts.Count() && !fonts[fontIndex]->ContainsChar(c)) - { - fontIndex++; - } - - // If no font can match the char, then use the segment font - if (fontIndex == fonts.Count()) { - fontIndex = segmentFontIndex; - } - - - // Do nothing if the char still belongs to the current segment - if (fontIndex == segmentFontIndex) { - continue; - } - } - - - // Render the pending segment before beginning the new segment - auto fontHeight = fonts[segmentFontIndex]->GetHeight(); - maxHeight = Math::Max(maxHeight, static_cast(fontHeight)); - auto fontDescender = fonts[segmentFontIndex]->GetDescender(); - - const Char* pred = L"Type"; - if (text.Substring(0, Math::Min(4, text.Length())) == pred) { - // __debugbreak(); - } - for (int32 renderIndex = startIndex; renderIndex < charIndex; renderIndex++) + Char c = text[charIndex]; + if (c == '\n') { - // Get character entry - fonts[segmentFontIndex]->GetCharacter(text[renderIndex], entry); + continue; + } - // 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) - { - // Get texture atlas that contains current character - fontAtlasIndex = entry.TextureIndex; - fontAtlas = FontManager::GetAtlas(fontAtlasIndex); - if (fontAtlas) - { - fontAtlas->EnsureTextureCreated(); - invAtlasSize = 1.0f / fontAtlas->GetSize(); - drawCall.AsChar.Tex = fontAtlas->GetTexture(); - } - else - { - invAtlasSize = 1.0f; - drawCall.AsChar.Tex = nullptr; - } - } + // Get character entry + fonts[segment.FontIndex]->GetCharacter(c, entry); - // Get kerning - const bool isWhitespace = StringUtils::IsWhitespace(text[renderIndex]); - if (!isWhitespace && previous.IsValid) + // 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) + { + // Get texture atlas that contains current character + fontAtlasIndex = entry.TextureIndex; + fontAtlas = FontManager::GetAtlas(fontAtlasIndex); + if (fontAtlas) { - kerning = fonts[segmentFontIndex]->GetKerning(previous.Character, entry.Character); + fontAtlas->EnsureTextureCreated(); + invAtlasSize = 1.0f / fontAtlas->GetSize(); + drawCall.AsChar.Tex = fontAtlas->GetTexture(); } else { - kerning = 0; + invAtlasSize = 1.0f; + drawCall.AsChar.Tex = nullptr; } - pointer.X += (float)kerning * scale; - previous = entry; - - // Omit whitespace characters - if (!isWhitespace) - { - // Calculate character size and atlas coordinates - const float x = pointer.X + entry.OffsetX * scale; - const float y = pointer.Y - entry.OffsetY * scale + Math::Ceil((fontHeight + fontDescender) * scale); - - Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); - charRect.Offset(layout.Bounds.Location); - - Float2 upperLeftUV = entry.UV * invAtlasSize; - Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; - - // Add draw call - drawCall.StartIB = IBIndex; - drawCall.CountIB = 6; - DrawCalls.Add(drawCall); - WriteRect(charRect, color, upperLeftUV, rightBottomUV); - } - - // Move - pointer.X += entry.AdvanceX * scale; } - // Start new segment - startIndex = charIndex; - segmentFontIndex = fontIndex; + // Get kerning + const bool isWhitespace = StringUtils::IsWhitespace(c); + if (!isWhitespace && previous.IsValid) + { + kerning = fonts[segment.FontIndex]->GetKerning(previous.Character, entry.Character); + } + else + { + kerning = 0; + } + pointer.X += (float)kerning * scale; + previous = entry; + + // Omit whitespace characters + if (!isWhitespace) + { + // Calculate character size and atlas coordinates + const float x = pointer.X + entry.OffsetX * scale; + const float y = pointer.Y - entry.OffsetY * scale + Math::Ceil((fontHeight + fontDescender) * scale); + + Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); + charRect.Offset(layout.Bounds.Location); + + Float2 upperLeftUV = entry.UV * invAtlasSize; + Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; + + // Add draw call + drawCall.StartIB = IBIndex; + drawCall.CountIB = 6; + DrawCalls.Add(drawCall); + WriteRect(charRect, color, upperLeftUV, rightBottomUV); + } + + // Move + pointer.X += entry.AdvanceX * scale; } } } diff --git a/Source/Engine/UI/GUI/Common/Label.cs b/Source/Engine/UI/GUI/Common/Label.cs index b92e762e1..5fa8ad21d 100644 --- a/Source/Engine/UI/GUI/Common/Label.cs +++ b/Source/Engine/UI/GUI/Common/Label.cs @@ -235,7 +235,7 @@ namespace FlaxEngine.GUI } } - Render2D.DrawText(new Font[] { _font.GetFont(), Style.Current.FontCJK }, Material, _text, rect, color, hAlignment, wAlignment, Wrapping, BaseLinesGapScale, scale); + Render2D.DrawText([_font.GetFont(), Style.Current.FontCJK], Material, _text, rect, color, hAlignment, wAlignment, Wrapping, BaseLinesGapScale, scale); if (ClipText) Render2D.PopClip();