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 eb2f21356..c894abe6b 100644 --- a/Source/Editor/EditorAssets.cs +++ b/Source/Editor/EditorAssets.cs @@ -54,6 +54,8 @@ namespace FlaxEditor /// public static string PrimaryFont = "Editor/Fonts/Roboto-Regular"; + public static string CjkFont = "Editor/Fonts/NotoSansSC-Regular"; + /// /// The Inconsolata Regular font. /// diff --git a/Source/Editor/GUI/Row.cs b/Source/Editor/GUI/Row.cs index ca45ec6f5..f75f11922 100644 --- a/Source/Editor/GUI/Row.cs +++ b/Source/Editor/GUI/Row.cs @@ -43,8 +43,9 @@ namespace FlaxEditor.GUI { Depth = -1; - if (Height < Style.Current.FontMedium.Height) - Height = Style.Current.FontMedium.Height + 4; + var mediumHeight = Style.Current.FontMedium.GetMaxHeight(); + if (Height < mediumHeight) + Height = mediumHeight + 4; } /// diff --git a/Source/Editor/GUI/Timeline/GUI/PositionHandle.cs b/Source/Editor/GUI/Timeline/GUI/PositionHandle.cs index bedb61a5e..3f835a35f 100644 --- a/Source/Editor/GUI/Timeline/GUI/PositionHandle.cs +++ b/Source/Editor/GUI/Timeline/GUI/PositionHandle.cs @@ -36,16 +36,16 @@ namespace FlaxEditor.GUI.Timeline.GUI string labelText; switch (_timeline.TimeShowMode) { - case Timeline.TimeShowModes.Frames: - labelText = _timeline.CurrentFrame.ToString("###0", CultureInfo.InvariantCulture); - break; - case Timeline.TimeShowModes.Seconds: - labelText = _timeline.CurrentTime.ToString("###0.##'s'", CultureInfo.InvariantCulture); - break; - case Timeline.TimeShowModes.Time: - labelText = TimeSpan.FromSeconds(_timeline.CurrentTime).ToString("g"); - break; - default: throw new ArgumentOutOfRangeException(); + case Timeline.TimeShowModes.Frames: + labelText = _timeline.CurrentFrame.ToString("###0", CultureInfo.InvariantCulture); + break; + case Timeline.TimeShowModes.Seconds: + labelText = _timeline.CurrentTime.ToString("###0.##'s'", CultureInfo.InvariantCulture); + break; + case Timeline.TimeShowModes.Time: + labelText = TimeSpan.FromSeconds(_timeline.CurrentTime).ToString("g"); + break; + default: throw new ArgumentOutOfRangeException(); } var color = (_timeline.IsMovingPositionHandle ? style.ProgressNormal : style.Foreground).AlphaMultiplied(0.6f); Matrix3x3.RotationZ(Mathf.PiOverTwo, out var m1); diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index 95a273f19..af432baa7 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -237,11 +237,19 @@ namespace FlaxEditor.Options public int NumberOfGameClientsToLaunch = 1; private static FontAsset DefaultFont => FlaxEngine.Content.LoadAsyncInternal(EditorAssets.PrimaryFont); + + private static FontAsset ConsoleFont => FlaxEngine.Content.LoadAsyncInternal(EditorAssets.PrimaryFont); + private FontReference _titleFont = new FontReference(DefaultFont, 18); private FontReference _largeFont = new FontReference(DefaultFont, 14); private FontReference _mediumFont = new FontReference(DefaultFont, 9); private FontReference _smallFont = new FontReference(DefaultFont, 9); - private FontReference _outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont), 10); + private FontReference _outputLogFont = new FontReference(ConsoleFont, 10); + + /// + /// The fallback fonts. + /// + public FontAsset[] Fallbacks = [FlaxEngine.Content.LoadAsyncInternal(EditorAssets.CjkFont)]; /// /// Gets or sets the title font for editor UI. diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs index 1137e4c37..bf35105a5 100644 --- a/Source/Editor/Options/OptionsModule.cs +++ b/Source/Editor/Options/OptionsModule.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using FlaxEditor.Content.Settings; using FlaxEditor.Modules; using FlaxEngine; using FlaxEngine.GUI; @@ -217,12 +218,18 @@ namespace FlaxEditor.Options if (styleName == ThemeOptions.LightDefault) { Style.Current = CreateLightStyle(); - } + } else { Style.Current = CreateDefaultStyle(); } } + + var graphicsSetttings = GameSettings.Load(); + if (graphicsSetttings.EnableFontFallback && graphicsSetttings.FallbackFonts == null) + { + Render2D.FallbackFonts = graphicsSetttings.FallbackFonts = FontFallbackList.Create(Options.Interface.Fallbacks); + } } /// diff --git a/Source/Editor/Windows/Assets/FontWindow.cs b/Source/Editor/Windows/Assets/FontWindow.cs index ff4135165..bc91ca5e2 100644 --- a/Source/Editor/Windows/Assets/FontWindow.cs +++ b/Source/Editor/Windows/Assets/FontWindow.cs @@ -144,7 +144,7 @@ namespace FlaxEditor.Windows.Assets protected override void OnAssetLinked() { Asset.WaitForLoaded(); - _textPreview.Font = new FontReference(Asset.CreateFont(30)); + _textPreview.Font = new FontReference(Asset, 30); _inputText.Text = string.Format("This is a sample text using font {0}.", Asset.FamilyName); var options = Asset.Options; _proxy.Set(ref options); diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 6526d7c8a..f168d8d45 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -562,12 +562,12 @@ namespace FlaxEditor.Windows { switch (match.Groups["level"].Value) { - case "error": - textBlock.Style = _output.ErrorStyle; - break; - case "warning": - textBlock.Style = _output.WarningStyle; - break; + case "error": + textBlock.Style = _output.ErrorStyle; + break; + case "warning": + textBlock.Style = _output.WarningStyle; + break; } textBlock.Tag = new TextBlockTag { diff --git a/Source/Editor/Windows/SplashScreen.cpp b/Source/Editor/Windows/SplashScreen.cpp index 92894b537..cba59191e 100644 --- a/Source/Editor/Windows/SplashScreen.cpp +++ b/Source/Editor/Windows/SplashScreen.cpp @@ -258,13 +258,13 @@ void SplashScreen::OnDraw() return; // Title - const auto titleLength = _titleFont->MeasureText(GetTitle()); + const auto titleLength = _titleFont->MeasureTextInternal(GetTitle()); TextLayoutOptions layout; layout.Bounds = Rectangle(10 * s, 10 * s, width - 10 * s, 50 * s); layout.HorizontalAlignment = TextAlignment::Near; layout.VerticalAlignment = TextAlignment::Near; layout.Scale = Math::Min((width - 20 * s) / titleLength.X, 1.0f); - Render2D::DrawText(_titleFont, GetTitle(), Color::White, layout); + Render2D::DrawTextInternal(_titleFont, GetTitle(), Color::White, layout); // Subtitle String subtitle(_quote); @@ -279,14 +279,14 @@ void SplashScreen::OnDraw() layout.Scale = 1.0f; layout.HorizontalAlignment = TextAlignment::Far; layout.VerticalAlignment = TextAlignment::Far; - Render2D::DrawText(_subtitleFont, subtitle, Color::FromRGB(0x8C8C8C), layout); + Render2D::DrawTextInternal(_subtitleFont, subtitle, Color::FromRGB(0x8C8C8C), layout); // Additional info const float infoMargin = 6 * s; layout.Bounds = Rectangle(infoMargin, lightBarHeight + infoMargin, width - (2 * infoMargin), height - lightBarHeight - (2 * infoMargin)); layout.HorizontalAlignment = TextAlignment::Near; layout.VerticalAlignment = TextAlignment::Center; - Render2D::DrawText(_subtitleFont, _infoText, Color::FromRGB(0xFFFFFF) * 0.9f, layout); + Render2D::DrawTextInternal(_subtitleFont, _infoText, Color::FromRGB(0xFFFFFF) * 0.9f, layout); } bool SplashScreen::HasLoadedFonts() const diff --git a/Source/Engine/Core/Config/GameSettings.cs b/Source/Engine/Core/Config/GameSettings.cs index 40d9f5dbc..76c968968 100644 --- a/Source/Engine/Core/Config/GameSettings.cs +++ b/Source/Engine/Core/Config/GameSettings.cs @@ -119,7 +119,7 @@ namespace FlaxEditor.Content.Settings /// /// The custom settings to use with a game. Can be specified by the user to define game-specific options and be used by the external plugins (used as key-value pair). /// - [EditorOrder(1100), EditorDisplay("Other Settings"), Tooltip("The custom settings to use with a game. Can be specified by the user to define game-specific options and be used by the external plugins (used as key-value pair).")] + [EditorOrder(1500), EditorDisplay("Other Settings"), Tooltip("The custom settings to use with a game. Can be specified by the user to define game-specific options and be used by the external plugins (used as key-value pair).")] public Dictionary CustomSettings; #if FLAX_EDITOR || PLATFORM_WINDOWS diff --git a/Source/Engine/Core/Config/GameSettings.h b/Source/Engine/Core/Config/GameSettings.h index 54ad29a7b..675c26bd1 100644 --- a/Source/Engine/Core/Config/GameSettings.h +++ b/Source/Engine/Core/Config/GameSettings.h @@ -7,6 +7,8 @@ #include "Engine/Core/Types/String.h" #include "Engine/Core/Collections/Dictionary.h" +class FontFallbackList; + /// /// The main game engine configuration service. Loads and applies game configuration. /// diff --git a/Source/Engine/Core/Config/GraphicsSettings.h b/Source/Engine/Core/Config/GraphicsSettings.h index 81d80cb35..e109a55e4 100644 --- a/Source/Engine/Core/Config/GraphicsSettings.h +++ b/Source/Engine/Core/Config/GraphicsSettings.h @@ -6,6 +6,8 @@ #include "Engine/Graphics/Enums.h" #include "Engine/Graphics/PostProcessSettings.h" +class FontFallbackList; + /// /// Graphics rendering settings. /// @@ -118,6 +120,18 @@ public: API_FIELD(Attributes="EditorOrder(10000), EditorDisplay(\"Post Process Settings\", EditorDisplayAttribute.InlineStyle)") PostProcessSettings PostProcessSettings; + /// + /// Whether to enable font fallbacking globally. + /// + API_FIELD(Attributes = "EditorOrder(12000), EditorDisplay(\"Text Render Settings\", EditorDisplayAttribute.InlineStyle)") + bool EnableFontFallback = true; + + /// + /// The fallback fonts used for text rendering, ignored if null. + /// + API_FIELD(Attributes = "EditorOrder(12005), EditorDisplay(\"Text Render Settings\", EditorDisplayAttribute.InlineStyle)") + FontFallbackList* FallbackFonts; + private: /// /// Renamed UeeHDRProbes into UseHDRProbes diff --git a/Source/Engine/Graphics/Graphics.cpp b/Source/Engine/Graphics/Graphics.cpp index f91c58cea..20f29288b 100644 --- a/Source/Engine/Graphics/Graphics.cpp +++ b/Source/Engine/Graphics/Graphics.cpp @@ -8,6 +8,7 @@ #include "Engine/Core/Config/GraphicsSettings.h" #include "Engine/Engine/CommandLine.h" #include "Engine/Engine/EngineService.h" +#include "Engine/Render2D/Render2D.h" bool Graphics::UseVSync = false; Quality Graphics::AAQuality = Quality::Medium; @@ -69,6 +70,9 @@ void GraphicsSettings::Apply() Graphics::GIQuality = GIQuality; Graphics::PostProcessSettings = ::PostProcessSettings(); Graphics::PostProcessSettings.BlendWith(PostProcessSettings, 1.0f); + + Render2D::EnableFontFallback = EnableFontFallback; + Render2D::FallbackFonts = FallbackFonts; } void Graphics::DisposeDevice() diff --git a/Source/Engine/Render2D/FallbackFonts.cpp b/Source/Engine/Render2D/FallbackFonts.cpp new file mode 100644 index 000000000..7f81a42bb --- /dev/null +++ b/Source/Engine/Render2D/FallbackFonts.cpp @@ -0,0 +1,11 @@ +#include "FallbackFonts.h" +#include "FontManager.h" +#include "Engine/Core/Math/Math.h" + +FontFallbackList::FontFallbackList(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..fb247e4e2 --- /dev/null +++ b/Source/Engine/Render2D/FallbackFonts.h @@ -0,0 +1,119 @@ +#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; + +/// +/// Defines a list of fonts that can be used as a fallback, ordered by priority. +/// +API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API FontFallbackList : public ManagedScriptingObject +{ + DECLARE_SCRIPTING_TYPE_NO_SPAWN(FontFallbackList); +private: + Array _fontAssets; + + // Cache fallback fonts of various sizes + Dictionary*> _cache; + +public: + /// + /// Initializes a new instance of the class. + /// + /// The fallback font assets. + FontFallbackList(const Array& fonts); + + /// + /// Initializes a new instance of the class, exposed for C#. + /// + /// The fallback font assets. + /// The new instance. + API_FUNCTION() FORCE_INLINE static FontFallbackList* Create(const Array& fonts) { + return New(fonts); + } + + /// + /// Get the parent assets of fallback fonts. + /// + /// The font assets. + API_PROPERTY() FORCE_INLINE Array& GetFonts() { + return _fontAssets; + } + + /// + /// Set the fallback fonts. + /// + /// The parent assets of the new fonts. + API_PROPERTY() FORCE_INLINE void SetFonts(const Array& val) { + _fontAssets = val; + } + + /// + /// Gets the fallback fonts with the given size. + /// + /// The size. + /// The generated fonts. + API_FUNCTION() 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. + /// The primary font. + /// The number to return if none of the fonts can render. + /// -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; + } + + /// + /// Checks if every font is properly loaded. + /// + /// True if every font asset is non-null, otherwise false. + 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 90423f5ce..9a2037b3f 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 "FallbackFonts.h" Font::Font(FontAsset* parentAsset, float size) : ManagedScriptingObject(SpawnParams(Guid::New(), Font::TypeInitializer)) @@ -102,6 +103,18 @@ void Font::Invalidate() _characters.Clear(); } +float Font::GetMaxHeight(FontFallbackList* fallbacks) const +{ + float height = GetHeight(); + auto& fallbackFonts = fallbacks->GetFontList(GetSize()); + for (int32 i = 0; i < fallbackFonts.Count(); i++) + { + height = Math::Max(height, static_cast(fallbackFonts[i]->GetHeight())); + } + + return height; +} + void Font::ProcessText(const StringView& text, Array& outputLines, const TextLayoutOptions& layout) { float cursorX = 0; @@ -118,6 +131,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 +146,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 +168,6 @@ void Font::ProcessText(const StringView& text, Array& outputLines { // Break line moveLine = true; - currentIndex++; tmpLine.LastCharIndex++; } else @@ -178,8 +199,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 +247,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 +259,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,7 +305,262 @@ void Font::ProcessText(const StringView& text, Array& outputLines } } -Float2 Font::MeasureText(const StringView& text, const TextLayoutOptions& layout) +void Font::ProcessText(FontFallbackList* 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::MeasureTextInternal(const StringView& text, const TextLayoutOptions& layout) { // Check if there is no need to do anything if (text.IsEmpty()) @@ -304,7 +581,28 @@ Float2 Font::MeasureText(const StringView& text, const TextLayoutOptions& layout return max; } -int32 Font::HitTestText(const StringView& text, const Float2& location, const TextLayoutOptions& layout) +Float2 Font::MeasureTextInternal(FontFallbackList* 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::HitTestTextInternal(const StringView& text, const Float2& location, const TextLayoutOptions& layout) { // Check if there is no need to do anything if (text.Length() <= 0) @@ -378,7 +676,107 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te return smallestIndex; } -Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayoutOptions& layout) +int32 Font::HitTestTextInternal(FontFallbackList* 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::GetCharPositionInternal(const StringView& text, int32 index, const TextLayoutOptions& layout) { // Check if there is no need to do anything if (text.IsEmpty()) @@ -390,7 +788,7 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo ASSERT(lines.HasItems()); float scale = layout.Scale / FontManager::FontScale; float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; - Float2 rootOffset = layout.Bounds.Location + lines.First().Location; + Float2 rootOffset = layout.Bounds.Location; // Find line with that position FontCharacterEntry previous; @@ -429,7 +827,71 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo } // Position after last character in the last line - return rootOffset + Float2(lines.Last().Size.X, static_cast((lines.Count() - 1) * baseLinesDistance)); + return rootOffset + Float2(lines.Last().Location.X + lines.Last().Size.X, static_cast((lines.Count() - 1) * baseLinesDistance)); +} + +Float2 Font::GetCharPositionInternal(FontFallbackList* fallbacks, 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 + 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 90f723cd8..1fb41d032 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -8,9 +8,12 @@ #include "Engine/Content/AssetReference.h" #include "Engine/Scripting/ScriptingObject.h" #include "TextLayoutOptions.h" +#include "Render2D.h" class FontAsset; +class FontFallbackList; struct FontTextureAtlasSlot; +struct BlockedTextLineCache; // The default DPI that engine is using #define DefaultDPI 96 @@ -20,7 +23,7 @@ struct FontTextureAtlasSlot; /// API_STRUCT(NoDefault) struct TextRange { -DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange); + DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange); /// /// The start index (inclusive). @@ -90,7 +93,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 +111,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; }; @@ -119,6 +122,74 @@ struct TIsPODType enum { Value = true }; }; +/// +/// The font block info generated during text processing. +/// A block means a range of text that belongs to the same line and can be rendered with the same font. +/// +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 size 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 blocks that belongs to this line + /// + API_FIELD() Array Blocks; +}; + // Font glyph metrics: // // xmin xmax @@ -154,7 +225,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 +294,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: @@ -332,7 +403,31 @@ public: public: /// - /// Processes text to get cached lines for rendering. + /// Gets the maximum height among the font and the fallback fonts. + /// + /// The fallback fonts. + /// The maximum height. + API_FUNCTION() float GetMaxHeight(FontFallbackList* fallbacks) const; + + /// + /// Gets the maximum height among the font and the fallback fonts, uses the default font defined in . + /// + /// The fallback fonts. + /// The maximum height. + API_FUNCTION() FORCE_INLINE float GetMaxHeight() const + { + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) + { + return GetMaxHeight(Render2D::FallbackFonts); + } + else + { + return GetHeight(); + } + } + + /// + /// Processes text to get cached lines for rendering, with font fallbacking disabled. /// /// The input text. /// The layout properties. @@ -340,12 +435,12 @@ public: void ProcessText(const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout); /// - /// Processes text to get cached lines for rendering. + /// Processes text to get cached lines for rendering, with font fallbacking disabled. /// /// The input text. /// The layout properties. /// The output lines list. - API_FUNCTION() Array ProcessText(const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout) + API_FUNCTION() FORCE_INLINE Array ProcessText(const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout) { Array lines; ProcessText(text, lines, layout); @@ -353,13 +448,13 @@ public: } /// - /// Processes text to get cached lines for rendering. + /// Processes text to get cached lines for rendering, with font fallbacking disabled. /// /// 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) + API_FUNCTION() FORCE_INLINE 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); @@ -367,7 +462,7 @@ public: } /// - /// Processes text to get cached lines for rendering. + /// Processes text to get cached lines for rendering, with font fallbacking disabled. /// /// The input text. /// The output lines list. @@ -377,7 +472,7 @@ public: } /// - /// Processes text to get cached lines for rendering. + /// Processes text to get cached lines for rendering, with font fallbacking disabled. /// /// The input text. /// The input text range (substring range of the input text parameter). @@ -388,81 +483,349 @@ public: } /// - /// Measures minimum size of the rectangle that will be needed to draw given text. + /// Processes text to get cached lines for rendering, using custom fallback options. + /// + /// The input text. + /// The layout properties. + /// The output lines list. + void ProcessText(FontFallbackList* fallbacks, const StringView& text, Array& outputLines, API_PARAM(Ref) const TextLayoutOptions& layout); + + /// + /// Processes text to get cached lines for rendering, using custom fallback options. + /// + /// The input text. + /// The layout properties. + /// The output lines list. + API_FUNCTION() FORCE_INLINE Array ProcessText(FontFallbackList* 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, using custom fallback options. + /// + /// The input text. + /// The input text range (substring range of the input text parameter). + /// The layout properties. + /// The output lines list. + API_FUNCTION() FORCE_INLINE Array ProcessText(FontFallbackList* 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, using custom fallback options. + /// + /// The input text. + /// The output lines list. + API_FUNCTION() FORCE_INLINE Array ProcessText(FontFallbackList* fallbacks, const StringView& text) + { + return ProcessText(fallbacks, text, TextLayoutOptions()); + } + + /// + /// Processes text to get cached lines for rendering, using custom fallback options. + /// + /// The input text. + /// The input text range (substring range of the input text parameter). + /// The output lines list. + API_FUNCTION() FORCE_INLINE Array ProcessText(FontFallbackList* 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, with font fallbacking disabled. /// /// 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); + API_FUNCTION() Float2 MeasureTextInternal(const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout); /// - /// Measures minimum size of the rectangle that will be needed to draw given text. + /// Measures minimum size of the rectangle that will be needed to draw given text, with font fallbacking disabled. /// /// 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) + API_FUNCTION() FORCE_INLINE Float2 MeasureTextInternal(const StringView& text, API_PARAM(Ref) const TextRange& textRange, API_PARAM(Ref) const TextLayoutOptions& layout) { - return MeasureText(textRange.Substring(text), layout); + return MeasureTextInternal(textRange.Substring(text), layout); } /// - /// Measures minimum size of the rectangle that will be needed to draw given text + /// Measures minimum size of the rectangle that will be needed to draw given text, with font fallbacking disabled. + /// . + /// The input text to test. + /// The minimum size for that text and fot to render properly. + API_FUNCTION() FORCE_INLINE Float2 MeasureTextInternal(const StringView& text) + { + return MeasureTextInternal(text, TextLayoutOptions()); + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text, with font fallbacking disabled. + /// . + /// 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 MeasureTextInternal(const StringView& text, API_PARAM(Ref) const TextRange& textRange) + { + return MeasureTextInternal(textRange.Substring(text), TextLayoutOptions()); + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text, using custom fallback options. + /// + /// The input text to test. + /// The layout properties. + /// The minimum size for that text and fot to render properly. + API_FUNCTION() Float2 MeasureTextInternal(FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout); + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text, using custom fallback options. + /// + /// 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() FORCE_INLINE Float2 MeasureTextInternal(FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, API_PARAM(Ref) const TextLayoutOptions& layout) + { + return MeasureTextInternal(fallbacks, textRange.Substring(text), layout); + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text, using custom fallback options. + /// . + /// The input text to test. + /// The minimum size for that text and fot to render properly. + API_FUNCTION() FORCE_INLINE Float2 MeasureTextInternal(FontFallbackList* fallbacks, const StringView& text) + { + return MeasureTextInternal(fallbacks, text, TextLayoutOptions()); + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text, using custom fallback options. + /// . + /// 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 MeasureTextInternal(FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange) + { + return MeasureTextInternal(fallbacks, textRange.Substring(text), TextLayoutOptions()); + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text, follows the fallback settings defined in . + /// + /// The input text to test. + /// The layout properties. + /// The minimum size for that text and fot to render properly. + API_FUNCTION() FORCE_INLINE Float2 MeasureText(const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout) { + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return MeasureTextInternal(Render2D::FallbackFonts, text, layout); + } + else { + return MeasureTextInternal(text, layout); + } + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text, follows the fallback settings defined in . + /// + /// 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() FORCE_INLINE Float2 MeasureText(const StringView& text, API_PARAM(Ref) const TextRange& textRange, API_PARAM(Ref) const TextLayoutOptions& layout) + { + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return MeasureTextInternal(Render2D::FallbackFonts, textRange.Substring(text), layout); + } + else { + return MeasureTextInternal(textRange.Substring(text), layout); + } + } + + /// + /// Measures minimum size of the rectangle that will be needed to draw given text, follows the fallback settings defined in . /// . /// 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()); + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return MeasureTextInternal(Render2D::FallbackFonts, text, TextLayoutOptions()); + } + else { + return MeasureTextInternal(text, TextLayoutOptions()); + } } /// - /// Measures minimum size of the rectangle that will be needed to draw given text + /// Measures minimum size of the rectangle that will be needed to draw given text, follows the fallback settings defined in . /// . /// 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()); + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return MeasureTextInternal(Render2D::FallbackFonts, textRange.Substring(text), TextLayoutOptions()); + } + else { + return MeasureTextInternal(textRange.Substring(text), TextLayoutOptions()); + } } /// - /// Calculates hit character index at given location. + /// Calculates hit character index at given location, with font fallbacking disabled. + /// + /// 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 HitTestTextInternal(const StringView& text, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout); + + /// + /// Calculates hit character index at given location, with font fallbacking disabled. /// /// 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) + API_FUNCTION() FORCE_INLINE int32 HitTestTextInternal(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); + return HitTestTextInternal(textRange.Substring(text), location, layout); } /// - /// Calculates hit character index at given location. + /// Calculates hit character index at given location, with font fallbacking disabled. + /// + /// 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 HitTestTextInternal(const StringView& text, const Float2& location) + { + return HitTestTextInternal(text, location, TextLayoutOptions()); + } + + /// + /// Calculates hit character index at given location, with font fallbacking disabled. + /// + /// 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 HitTestTextInternal(const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Float2& location) + { + return HitTestTextInternal(textRange.Substring(text), location, TextLayoutOptions()); + } + + /// + /// Calculates hit character index at given location, using custom fallback options. /// /// 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); + API_FUNCTION() int32 HitTestTextInternal(FontFallbackList* fallbacks, const StringView& text, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout); /// - /// Calculates hit character index at given location. + /// Calculates hit character index at given location, using custom fallback options. + /// + /// 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() FORCE_INLINE int32 HitTestTextInternal(FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout) + { + return HitTestTextInternal(fallbacks, textRange.Substring(text), location, layout); + } + + /// + /// Calculates hit character index at given location, using custom fallback options. + /// + /// 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 HitTestTextInternal(FontFallbackList* fallbacks, const StringView& text, const Float2& location) + { + return HitTestTextInternal(fallbacks, text, location, TextLayoutOptions()); + } + + /// + /// Calculates hit character index at given location, using custom fallback options. + /// + /// 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 HitTestTextInternal(FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Float2& location) + { + return HitTestTextInternal(fallbacks, textRange.Substring(text), location, TextLayoutOptions()); + } + + /// + /// Calculates hit character index at given location, follows the fallback settings defined in . + /// + /// 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() FORCE_INLINE int32 HitTestText(const StringView& text, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout) { + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return HitTestTextInternal(Render2D::FallbackFonts, text, location, layout); + } + else { + return HitTestTextInternal(text, location, layout); + } + } + + /// + /// Calculates hit character index at given location, follows the fallback settings defined in . + /// + /// 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() FORCE_INLINE int32 HitTestText(const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Float2& location, API_PARAM(Ref) const TextLayoutOptions& layout) + { + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return HitTestTextInternal(Render2D::FallbackFonts, textRange.Substring(text), location, layout); + } + else { + return HitTestTextInternal(textRange.Substring(text), location, layout); + } + + } + + /// + /// Calculates hit character index at given location, follows the fallback settings defined in . /// /// 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()); + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return HitTestTextInternal(Render2D::FallbackFonts, text, location, TextLayoutOptions()); + } + else { + return HitTestTextInternal(text, location, TextLayoutOptions()); + } } /// - /// Calculates hit character index at given location. + /// Calculates hit character index at given location, follows the fallback settings defined in . /// /// The input text to test. /// The input text range (substring range of the input text parameter). @@ -470,44 +833,156 @@ public: /// 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()); + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return HitTestTextInternal(Render2D::FallbackFonts, textRange.Substring(text), location, TextLayoutOptions()); + } + else { + return HitTestTextInternal(textRange.Substring(text), location, TextLayoutOptions()); + } } /// - /// Calculates character position for given text and character index. + /// Calculates character position for given text and character index, with font fallbacking disabled. /// /// 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); + API_FUNCTION() Float2 GetCharPositionInternal(const StringView& text, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout); /// - /// Calculates character position for given text and character index. + /// Calculates character position for given text and character index, with font fallbacking disabled. /// /// 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) + API_FUNCTION() FORCE_INLINE Float2 GetCharPositionInternal(const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout) { - return GetCharPosition(textRange.Substring(text), index, layout); + return GetCharPositionInternal(textRange.Substring(text), index, layout); } /// - /// Calculates character position for given text and character index + /// Calculates character position for given text and character index, with font fallbacking disabled. + /// + /// 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 GetCharPositionInternal(const StringView& text, int32 index) + { + return GetCharPositionInternal(text, index, TextLayoutOptions()); + } + + /// + /// Calculates character position for given text and character index, with font fallbacking disabled. + /// + /// 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 GetCharPositionInternal(const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index) + { + return GetCharPositionInternal(textRange.Substring(text), index, TextLayoutOptions()); + } + + /// + /// Calculates character position for given text and character index, using custom fallback options. + /// + /// 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 GetCharPositionInternal(FontFallbackList* fallbacks, const StringView& text, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout); + + /// + /// Calculates character position for given text and character index, using custom fallback options. + /// + /// 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() FORCE_INLINE Float2 GetCharPositionInternal(FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout) + { + return GetCharPositionInternal(fallbacks, textRange.Substring(text), index, layout); + } + + /// + /// Calculates character position for given text and character index, using custom fallback options. + /// + /// 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 GetCharPositionInternal(FontFallbackList* fallbacks, const StringView& text, int32 index) + { + return GetCharPositionInternal(fallbacks, text, index, TextLayoutOptions()); + } + + /// + /// Calculates character position for given text and character index, using custom fallback options. + /// + /// 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 GetCharPositionInternal(FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index) + { + return GetCharPositionInternal(fallbacks, textRange.Substring(text), index, TextLayoutOptions()); + } + + /// + /// Calculates character position for given text and character index, follows the fallback settings defined in . + /// + /// 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() FORCE_INLINE Float2 GetCharPosition(const StringView& text, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout) { + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return GetCharPositionInternal(Render2D::FallbackFonts, text, index, layout); + } + else { + return GetCharPositionInternal(text, index, layout); + } + } + + /// + /// Calculates character position for given text and character index, follows the fallback settings defined in . + /// + /// 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() FORCE_INLINE Float2 GetCharPosition(const StringView& text, API_PARAM(Ref) const TextRange& textRange, int32 index, API_PARAM(Ref) const TextLayoutOptions& layout) + { + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return GetCharPositionInternal(Render2D::FallbackFonts, textRange.Substring(text), index, layout); + } + else { + return GetCharPositionInternal(textRange.Substring(text), index, layout); + } + } + + /// + /// Calculates character position for given text and character index, follows the fallback settings defined in . /// /// 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()); + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return GetCharPositionInternal(Render2D::FallbackFonts, text, index, TextLayoutOptions()); + } + else { + return GetCharPositionInternal(text, index, TextLayoutOptions()); + } } /// - /// Calculates character position for given text and character index + /// Calculates character position for given text and character index, follows the fallback settings defined in . /// /// The input text to test. /// The input text range (substring range of the input text parameter). @@ -515,7 +990,12 @@ public: /// 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()); + if (Render2D::EnableFontFallback && Render2D::FallbackFonts) { + return GetCharPositionInternal(Render2D::FallbackFonts, textRange.Substring(text), index, TextLayoutOptions()); + } + else { + return GetCharPositionInternal(textRange.Substring(text), index, TextLayoutOptions()); + } } /// diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index a477e5f4d..ea629367f 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -199,6 +199,17 @@ bool FontAsset::Save(const StringView& path) #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. + +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..f3c909911 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() 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/FontManager.cpp b/Source/Engine/Render2D/FontManager.cpp index bb7edf974..5b08c1d87 100644 --- a/Source/Engine/Render2D/FontManager.cpp +++ b/Source/Engine/Render2D/FontManager.cpp @@ -155,6 +155,9 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry) // Get the index to the glyph in the font face const FT_UInt glyphIndex = FT_Get_Char_Index(face, c); + if (glyphIndex == 0) { + LOG(Warning, "Font `{}` doesn't contain character `\\u{:x}`, consider choosing another font. ", String(face->family_name), c); + } // Load the glyph const FT_Error error = FT_Load_Glyph(face, glyphIndex, glyphFlags); diff --git a/Source/Engine/Render2D/Render2D.cpp b/Source/Engine/Render2D/Render2D.cpp index b56c3e1a2..fa52baff0 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 "FallbackFonts.h" #include "FontTextureAtlas.h" #include "RotatedRectangle.h" #include "SpriteAtlas.h" @@ -27,11 +28,11 @@ #if USE_EDITOR #define RENDER2D_CHECK_RENDERING_STATE \ - if (!Render2D::IsRendering()) \ - { \ - LOG(Error, "Calling Render2D is only valid during rendering."); \ - return; \ - } + if (!Render2D::IsRendering()) \ + { \ + LOG(Error, "Calling Render2D is only valid during rendering."); \ + return; \ + } #else #define RENDER2D_CHECK_RENDERING_STATE #endif @@ -54,7 +55,7 @@ const bool DownsampleForBlur = false; PACK_STRUCT(struct Data { Matrix ViewProjection; - }); +}); PACK_STRUCT(struct BlurData { Float2 InvBufferSize; @@ -62,7 +63,7 @@ PACK_STRUCT(struct BlurData { float Dummy0; Float4 Bounds; Float4 WeightAndOffsets[RENDER2D_BLUR_MAX_SAMPLES / 2]; - }); +}); enum class DrawCallType : byte { @@ -181,6 +182,8 @@ struct ClipMask }; Render2D::RenderingFeatures Render2D::Features = RenderingFeatures::VertexSnapping; +bool Render2D::EnableFontFallback = true; +FontFallbackList* Render2D::FallbackFonts = nullptr; namespace { @@ -194,6 +197,7 @@ namespace // Drawing Array DrawCalls; Array Lines; + Array BlockedTextLines; Array Lines2; bool IsScissorsRectEmpty; bool IsScissorsRectEnabled; @@ -1142,7 +1146,7 @@ void DrawBatch(int32 startIndex, int32 count) Context->DrawIndexed(countIb, 0, d.StartIB); } -void Render2D::DrawText(Font* font, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial) +void Render2D::DrawTextInternal(Font* font, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial) { RENDER2D_CHECK_RENDERING_STATE; @@ -1174,7 +1178,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, drawCall.AsChar.Mat = nullptr; } Float2 pointer = location; - for (int32 currentIndex = 0; currentIndex <= text.Length(); currentIndex++) + for (int32 currentIndex = 0; currentIndex < text.Length(); currentIndex++) { // Cache current character const Char currentChar = text[currentIndex]; @@ -1250,12 +1254,12 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } } -void Render2D::DrawText(Font* font, const StringView& text, const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial) +void Render2D::DrawTextInternal(Font* font, const StringView& text, const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial) { - DrawText(font, textRange.Substring(text), color, location, customMaterial); + DrawTextInternal(font, textRange.Substring(text), color, location, customMaterial); } -void Render2D::DrawText(Font* font, const StringView& text, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) +void Render2D::DrawTextInternal(Font* font, const StringView& text, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) { RENDER2D_CHECK_RENDERING_STATE; @@ -1363,9 +1367,317 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } } -void Render2D::DrawText(Font* font, const StringView& text, const TextRange& textRange, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) +void Render2D::DrawTextInternal(Font* font, const StringView& text, const TextRange& textRange, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) { - DrawText(font, textRange.Substring(text), color, layout, customMaterial); + DrawTextInternal(font, textRange.Substring(text), color, layout, customMaterial); +} + +void Render2D::DrawTextInternal(Font* font, FontFallbackList* fallbacks, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial) +{ + RENDER2D_CHECK_RENDERING_STATE; + + // Check if there is no need to do anything + 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; + FontCharacterEntry previous; + int32 kerning; + float scale = 1.0f / FontManager::FontScale; + + // Process text to get lines + Array maxAscenders; + + // Render all characters + FontCharacterEntry entry; + Render2DDrawCall drawCall; + if (customMaterial) + { + drawCall.Type = DrawCallType::DrawCharMaterial; + drawCall.AsChar.Mat = customMaterial; + } + else + { + drawCall.Type = DrawCallType::DrawChar; + drawCall.AsChar.Mat = nullptr; + } + + 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++) + { + 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++; + maxAscenders.Add(0); + } + } + + lineIndex = 0; + // The following code cut the text into blocks, according to the font used to render + Float2 pointer = location; + // The starting index of the current block + int32 startIndex = 0; + // The index of the font used by the current block + 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++) + { + // Cache current character + const Char currentChar = text[currentIndex]; + int32 nextCharIndex = currentIndex + 1; + bool moveBlock = false; + bool moveLine = false; + int32 nextFontIndex = currentFontIndex; + + // Submit block if text ends + if (nextCharIndex == text.Length()) { + moveBlock = true; + } + + // Check if it isn't a newline character + if (currentChar != '\n') + { + // Get character entry + if (nextCharIndex < text.Length()) { + nextFontIndex = fallbacks->GetCharFallbackIndex(text[nextCharIndex], font); + } + + if (nextFontIndex != currentFontIndex) { + moveBlock = true; + } + } + else + { + // Move + moveLine = moveBlock = true; + } + + if (moveBlock) { + // Render the pending block before beginning the new block + auto fontHeight = getFont(currentFontIndex)->GetHeight(); + maxHeight = Math::Max(maxHeight, static_cast(fontHeight)); + auto fontDescender = getFont(currentFontIndex)->GetDescender(); + for (int32 renderIndex = startIndex; renderIndex <= currentIndex; renderIndex++) + { + // Get character 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) + { + // Get texture atlas that contains current character + fontAtlasIndex = entry.TextureIndex; + fontAtlas = FontManager::GetAtlas(fontAtlasIndex); + if (fontAtlas) + { + fontAtlas->EnsureTextureCreated(); + drawCall.AsChar.Tex = fontAtlas->GetTexture(); + invAtlasSize = 1.0f / fontAtlas->GetSize(); + } + else + { + drawCall.AsChar.Tex = nullptr; + invAtlasSize = 1.0f; + } + } + + // Check if character is a whitespace + const bool isWhitespace = StringUtils::IsWhitespace(text[renderIndex]); + + // Get kerning + if (!isWhitespace && previous.IsValid) + { + kerning = getFont(currentFontIndex)->GetKerning(previous.Character, entry.Character); + } + else + { + kerning = 0; + } + pointer.X += 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 + (fontHeight + fontDescender - entry.OffsetY) * 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; + + // Add draw call + drawCall.StartIB = IBIndex; + drawCall.CountIB = 6; + DrawCalls.Add(drawCall); + WriteRect(charRect, color, upperLeftUV, rightBottomUV); + } + + // Move + pointer.X += entry.AdvanceX * scale; + } + + if (moveLine) { + pointer.X = location.X; + pointer.Y += maxHeight * scale; + // Clear max height + maxHeight = 0; + lineIndex++; + } + + // Start new block + startIndex = nextCharIndex; + currentFontIndex = nextFontIndex; + } + } +} + +void Render2D::DrawTextInternal(Font* font, FontFallbackList* fallbacks, const StringView& text, const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial) +{ + DrawTextInternal(font, fallbacks, textRange.Substring(text), color, location, customMaterial); +} + +void Render2D::DrawTextInternal(Font* font, FontFallbackList* fallbacks, const StringView& text, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) +{ + RENDER2D_CHECK_RENDERING_STATE; + + // Check if there is no need to do anything + 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; + FontCharacterEntry previous; + int32 kerning; + float scale = layout.Scale / FontManager::FontScale; + + // Process text to get lines + BlockedTextLines.Clear(); + font->ProcessText(fallbacks, text, BlockedTextLines, layout); + + // Render all lines + FontCharacterEntry entry; + Render2DDrawCall drawCall; + if (customMaterial) + { + drawCall.Type = DrawCallType::DrawCharMaterial; + drawCall.AsChar.Mat = customMaterial; + } + else + { + drawCall.Type = DrawCallType::DrawChar; + drawCall.AsChar.Mat = nullptr; + } + + auto getFont = [&](int32 index)->Font* { + return index >= 0 ? fallbackFonts[index] : font; + }; + + for (int32 lineIndex = 0; lineIndex < BlockedTextLines.Count(); lineIndex++) + { + const BlockedTextLineCache& line = BlockedTextLines[lineIndex]; + for (int32 blockIndex = 0; blockIndex < line.Blocks.Count(); blockIndex++) + { + 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++) + { + Char c = text[charIndex]; + if (c == '\n') + { + continue; + } + + // Get character 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) + { + // 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 kerning + const bool isWhitespace = StringUtils::IsWhitespace(c); + if (!isWhitespace && previous.IsValid) + { + kerning = getFont(block.FallbackFontIndex)->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; + } + } + } +} + +void Render2D::DrawTextInternal(Font* font, FontFallbackList* fallbacks, const StringView& text, const TextRange& textRange, const Color& color, const TextLayoutOptions& layout, MaterialBase* customMaterial) +{ + DrawTextInternal(font, fallbacks, textRange.Substring(text), color, layout, customMaterial); } FORCE_INLINE bool NeedAlphaWithTint(const Color& color) @@ -1869,22 +2181,22 @@ void Render2D::DrawBezier(const Float2& p1, const Float2& p2, const Float2& p3, { RENDER2D_CHECK_RENDERING_STATE; - // Find amount of segments to use + // Find amount of blocks to use const Float2 d1 = p2 - p1; const Float2 d2 = p3 - p2; const Float2 d3 = p4 - p3; const float len = d1.Length() + d2.Length() + d3.Length(); - const int32 segmentCount = Math::Clamp(Math::CeilToInt(len * 0.05f), 1, 100); - const float segmentCountInv = 1.0f / segmentCount; + const int32 blockCount = Math::Clamp(Math::CeilToInt(len * 0.05f), 1, 100); + const float blockCountInv = 1.0f / blockCount; - // Draw segmented curve + // Draw blocked curve Float2 p; AnimationUtils::Bezier(p1, p2, p3, p4, 0, p); Lines2.Clear(); Lines2.Add(p); - for (int32 i = 1; i <= segmentCount; i++) + for (int32 i = 1; i <= blockCount; i++) { - const float t = i * segmentCountInv; + const float t = i * blockCountInv; AnimationUtils::Bezier(p1, p2, p3, p4, t, p); Lines2.Add(p); } @@ -1931,7 +2243,7 @@ void Render2D::DrawBlur(const Rectangle& rect, float blurStrength) void Render2D::DrawTexturedTriangles(GPUTexture* t, const Span& vertices, const Span& uvs) { RENDER2D_CHECK_RENDERING_STATE; - CHECK(vertices.Length() == uvs.Length()) + CHECK(vertices.Length() == uvs.Length()); Render2DDrawCall& drawCall = DrawCalls.AddOne(); drawCall.Type = DrawCallType::FillTexture; @@ -1977,7 +2289,7 @@ void Render2D::DrawTexturedTriangles(GPUTexture* t, const Span& indices, drawCall.StartIB = IBIndex; drawCall.CountIB = indices.Length(); drawCall.AsTexture.Ptr = t; - + for (int32 i = 0; i < indices.Length();) { const uint16 i0 = indices.Get()[i++]; diff --git a/Source/Engine/Render2D/Render2D.cs b/Source/Engine/Render2D/Render2D.cs index c4d9e81b4..b36f155df 100644 --- a/Source/Engine/Render2D/Render2D.cs +++ b/Source/Engine/Render2D/Render2D.cs @@ -1,11 +1,13 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using FlaxEngine.GUI; using System; namespace FlaxEngine { partial class Render2D { + /// /// Pushes transformation layer. /// @@ -100,7 +102,7 @@ namespace FlaxEngine } /// - /// Draws a text. + /// Draws a text, follows the font fallback settings defined in . /// /// The font to use. /// The text to render. @@ -126,7 +128,7 @@ namespace FlaxEngine } /// - /// Draws a text using a custom material shader. Given material must have GUI domain and a public parameter named Font (texture parameter used for a font atlas sampling). + /// Draws a text using a custom material shader. Given material must have GUI domain and a public parameter named Font (texture parameter used for a font atlas sampling). Follows the font fallback settings defined in . /// /// The font to use. /// Custom material for font characters rendering. It must contain texture parameter named Font used to sample font texture. diff --git a/Source/Engine/Render2D/Render2D.h b/Source/Engine/Render2D/Render2D.h index 2b890ced9..6657d8542 100644 --- a/Source/Engine/Render2D/Render2D.h +++ b/Source/Engine/Render2D/Render2D.h @@ -15,6 +15,7 @@ struct Matrix3x3; struct Viewport; struct TextRange; class Font; +class FontFallbackList; class GPUPipelineState; class GPUTexture; class GPUTextureView; @@ -33,7 +34,7 @@ API_CLASS(Static) class FLAXENGINE_API Render2D /// /// The rendering features and options flags. /// - API_ENUM(Attributes="Flags") enum class RenderingFeatures + API_ENUM(Attributes = "Flags") enum class RenderingFeatures { /// /// The none. @@ -53,6 +54,7 @@ API_CLASS(Static) class FLAXENGINE_API Render2D }; public: + /// /// Checks if interface is during rendering phrase (Draw calls may be performed without failing). /// @@ -68,6 +70,10 @@ public: /// API_FIELD() static RenderingFeatures Features; + API_FIELD() static bool EnableFontFallback; + + API_FIELD() static FontFallbackList* FallbackFonts; + /// /// Called when frame rendering begins by the graphics device. /// @@ -174,17 +180,17 @@ public: public: /// - /// Draws a text. + /// Draws a text, with font fallbacking disabled. /// /// The font to use. /// The text to render. /// 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(Font* font, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); + API_FUNCTION() static void DrawTextInternal(Font* font, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); /// - /// Draws a text. + /// Draws a text, with font fallbacking disabled. /// /// The font to use. /// The text to render. @@ -192,20 +198,20 @@ 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(Font* font, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); + API_FUNCTION() static void DrawTextInternal(Font* font, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); /// - /// Draws a text with formatting. + /// Draws a text with formatting, with font fallbacking disabled. /// /// The font to use. /// The text to render. /// 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(Font* font, const StringView& text, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); + API_FUNCTION() static void DrawTextInternal(Font* font, const StringView& text, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); /// - /// Draws a text with formatting. + /// Draws a text with formatting, with font fallbacking disabled. /// /// The font to use. /// The text to render. @@ -213,7 +219,120 @@ 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(Font* font, 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 DrawTextInternal(Font* font, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); + + /// + /// Draws a text, using custom fallback options. + /// + /// The fonts to use, ordered by priority. + /// The text to render. + /// The input text range (substring range of the input text parameter). + /// 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 DrawTextInternal(Font* font, FontFallbackList* fallbacks, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); + + /// + /// Draws a text with formatting, using custom fallback options. + /// + /// The fonts to use, ordered by priority. + /// The text to render. + /// 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 DrawTextInternal(Font* font, FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr); + + /// + /// Draws a text with formatting, using custom fallback options. + /// + /// The fonts to use, ordered by priority. + /// The text to render. + /// The input text range (substring range of the input text parameter). + /// 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 DrawTextInternal(Font* font, FontFallbackList* fallbacks, const StringView& text, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); + + /// + /// Draws a text with formatting, using custom fallback options. + /// + /// The fonts to use, ordered by priority. + /// The text to render. + /// The input text range (substring range of the input text parameter). + /// 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 DrawTextInternal(Font* font, FontFallbackList* fallbacks, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr); + + /// + /// Draws a text, follows the fallback settings defined in . + /// + /// The font to use. + /// The text to render. + /// 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() FORCE_INLINE static void DrawText(Font* font, const StringView& text, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr) { + if (EnableFontFallback && FallbackFonts) { + DrawTextInternal(font, FallbackFonts, text, color, location, customMaterial); + } + else { + DrawTextInternal(font, text, color, location, customMaterial); + } + } + + /// + /// Draws a text, follows the fallback settings defined in . + /// + /// The font to use. + /// The text to render. + /// The input text range (substring range of the input text parameter). + /// 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() FORCE_INLINE static void DrawText(Font* font, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, const Float2& location, MaterialBase* customMaterial = nullptr) { + if (EnableFontFallback && FallbackFonts) { + DrawTextInternal(font, FallbackFonts, text, textRange, color, location, customMaterial); + } + else { + DrawTextInternal(font, text, textRange, color, location, customMaterial); + } + } + + /// + /// Draws a text with formatting, follows the fallback settings defined in . + /// + /// The font to use. + /// The text to render. + /// 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() FORCE_INLINE static void DrawText(Font* font, const StringView& text, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr) { + if (EnableFontFallback && FallbackFonts) { + DrawTextInternal(font, FallbackFonts, text, color, layout, customMaterial); + } + else { + DrawTextInternal(font, text, color, layout, customMaterial); + } + } + + /// + /// Draws a text with formatting, follows the fallback settings defined in . + /// + /// The font to use. + /// The text to render. + /// The input text range (substring range of the input text parameter). + /// 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() FORCE_INLINE static void DrawText(Font* font, const StringView& text, API_PARAM(Ref) const TextRange& textRange, const Color& color, API_PARAM(Ref) const TextLayoutOptions& layout, MaterialBase* customMaterial = nullptr) { + if (EnableFontFallback && FallbackFonts) { + DrawTextInternal(font, FallbackFonts, text, textRange, color, layout, customMaterial); + } + else { + DrawTextInternal(font, text, textRange, color, layout, customMaterial); + } + } /// /// Fills a rectangle area. diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs index 188333ff1..a4cfd76b1 100644 --- a/Source/Engine/Scripting/Scripting.cs +++ b/Source/Engine/Scripting/Scripting.cs @@ -294,14 +294,12 @@ namespace FlaxEngine style.DragWindow = style.BackgroundSelected * 0.7f; // Use optionally bundled default font (matches Editor) - var defaultFont = Content.LoadAsyncInternal("Editor/Fonts/Roboto-Regular"); - if (defaultFont) - { - style.FontTitle = defaultFont.CreateFont(18); - style.FontLarge = defaultFont.CreateFont(14); - style.FontMedium = defaultFont.CreateFont(9); - style.FontSmall = defaultFont.CreateFont(9); - } + FontAsset defaultFont = Content.LoadAsyncInternal("Editor/Fonts/Roboto-Regular"); + + style.FontTitle = new FontReference(defaultFont, 18).GetFont(); + style.FontLarge = new FontReference(defaultFont, 14).GetFont(); + style.FontMedium = new FontReference(defaultFont, 9).GetFont(); + style.FontSmall = new FontReference(defaultFont, 9).GetFont(); Style.Current = style; } diff --git a/Source/Engine/UI/GUI/Common/Label.cs b/Source/Engine/UI/GUI/Common/Label.cs index 3c7c04fb2..8de54fda3 100644 --- a/Source/Engine/UI/GUI/Common/Label.cs +++ b/Source/Engine/UI/GUI/Common/Label.cs @@ -182,6 +182,12 @@ namespace FlaxEngine.GUI set => _autoFitTextRange = value; } + /// + /// Gets or sets whether to fallback when the primary font cannot render a char. + /// + [EditorOrder(120), DefaultValue(true), Tooltip("Whether to fallback when the font cannot render a char.")] + public bool EnableFontFallback { get; set; } = true; + /// /// Initializes a new instance of the class. /// @@ -233,7 +239,23 @@ namespace FlaxEngine.GUI } } - Render2D.DrawText(_font.GetFont(), Material, _text, rect, color, hAlignment, wAlignment, Wrapping, BaseLinesGapScale, scale); + if (EnableFontFallback) + { + Render2D.DrawText(_font.GetFont(), Material, _text, rect, color, hAlignment, wAlignment, Wrapping, BaseLinesGapScale, scale); + } + else + { + var layout = new TextLayoutOptions + { + Bounds = rect, + HorizontalAlignment = hAlignment, + VerticalAlignment = wAlignment, + TextWrapping = Wrapping, + Scale = scale, + BaseLinesGapScale = BaseLinesGapScale, + }; + Render2D.DrawTextInternal(_font.GetFont(), _text, color, ref layout, Material); + } if (ClipText) Render2D.PopClip(); @@ -254,7 +276,8 @@ namespace FlaxEngine.GUI layout.Bounds.Size.X = Width - Margin.Width; else if (_autoWidth && !_autoHeight) layout.Bounds.Size.Y = Height - Margin.Height; - _textSize = font.MeasureText(_text, ref layout); + _textSize = EnableFontFallback ? + font.MeasureText(_text, ref layout) : font.MeasureTextInternal(_text, ref layout); _textSize.Y *= BaseLinesGapScale; // Check if size is controlled via text diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs index 2270136ae..ab01df12b 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs @@ -1,5 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System; using System.Collections.Generic; using System.Runtime.InteropServices; using FlaxEngine.Utilities; diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs index 044c65044..df8e0be7c 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs @@ -213,18 +213,18 @@ namespace FlaxEngine.GUI style.Alignment &= ~TextBlockStyle.Alignments.VerticalMask; switch (valign) { - case "top": - style.Alignment = TextBlockStyle.Alignments.Top; - break; - case "bottom": - style.Alignment = TextBlockStyle.Alignments.Bottom; - break; - case "middle": - style.Alignment = TextBlockStyle.Alignments.Middle; - break; - case "baseline": - style.Alignment = TextBlockStyle.Alignments.Baseline; - break; + case "top": + style.Alignment = TextBlockStyle.Alignments.Top; + break; + case "bottom": + style.Alignment = TextBlockStyle.Alignments.Bottom; + break; + case "middle": + style.Alignment = TextBlockStyle.Alignments.Middle; + break; + case "baseline": + style.Alignment = TextBlockStyle.Alignments.Baseline; + break; } } context.StyleStack.Push(style); @@ -245,15 +245,15 @@ namespace FlaxEngine.GUI style.Alignment &= ~TextBlockStyle.Alignments.VerticalMask; switch (valign) { - case "left": - style.Alignment = TextBlockStyle.Alignments.Left; - break; - case "right": - style.Alignment = TextBlockStyle.Alignments.Right; - break; - case "center": - style.Alignment = TextBlockStyle.Alignments.Center; - break; + case "left": + style.Alignment = TextBlockStyle.Alignments.Left; + break; + case "right": + style.Alignment = TextBlockStyle.Alignments.Right; + break; + case "center": + style.Alignment = TextBlockStyle.Alignments.Center; + break; } } context.StyleStack.Push(style); @@ -280,7 +280,7 @@ namespace FlaxEngine.GUI foreach (var id in ids) { var path = Content.GetEditorAssetPath(id); - if (!string.IsNullOrEmpty(path) && + if (!string.IsNullOrEmpty(path) && string.Equals(name, System.IO.Path.GetFileNameWithoutExtension(path), System.StringComparison.OrdinalIgnoreCase)) { return Content.LoadAsync(id, type); diff --git a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs index 438a7e3d8..46f0fb1ad 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs @@ -316,6 +316,7 @@ namespace FlaxEngine.GUI color = textBlock.Style.ShadowColor; if (!enabled) color *= 0.6f; + // We don't need font fallbacks for rich text since the font is user-selected Render2D.DrawText(font, _text, ref textBlock.Range, color, textBlock.Bounds.Location + textBlock.Style.ShadowOffset, textBlock.Style.CustomMaterial); } diff --git a/Source/Engine/UI/GUI/Common/TextBox.cs b/Source/Engine/UI/GUI/Common/TextBox.cs index ee4f744a6..53266a1b2 100644 --- a/Source/Engine/UI/GUI/Common/TextBox.cs +++ b/Source/Engine/UI/GUI/Common/TextBox.cs @@ -1,5 +1,8 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System.ComponentModel; + namespace FlaxEngine.GUI { /// @@ -64,6 +67,12 @@ namespace FlaxEngine.GUI [EditorDisplay("Text Style"), EditorOrder(2022), Tooltip("The color of the selection (Transparent if not used).")] public Color SelectionColor { get; set; } + /// + /// Gets or sets whether to fallback when the primary font cannot render a char. + /// + [EditorOrder(120), DefaultValue(true), Tooltip("Whether to fallback when the font cannot render a char.")] + public bool EnableFontFallback { get; set; } = true; + /// /// Initializes a new instance of the class. /// @@ -103,7 +112,8 @@ namespace FlaxEngine.GUI return Float2.Zero; } - return font.MeasureText(_text, ref _layout); + return EnableFontFallback ? font.MeasureText(_text, ref _layout) : + font.MeasureTextInternal(_text, ref _layout); } /// @@ -116,8 +126,9 @@ namespace FlaxEngine.GUI return Float2.Zero; } - height = font.Height / DpiScale; - return font.GetCharPosition(_text, index, ref _layout); + height = (EnableFontFallback ? font.GetMaxHeight() : font.Height) / DpiScale; + return EnableFontFallback ? font.GetCharPosition(_text, index, ref _layout) : + font.GetCharPositionInternal(_text, index, ref _layout); } /// @@ -129,7 +140,8 @@ namespace FlaxEngine.GUI return 0; } - return font.HitTestText(_text, location, ref _layout); + return EnableFontFallback ? font.HitTestText(_text, location, ref _layout) : + font.HitTestTextInternal(_text, location, ref _layout); } /// @@ -168,9 +180,13 @@ namespace FlaxEngine.GUI // Check if sth is selected to draw selection if (HasSelection) { - var leftEdge = font.GetCharPosition(_text, SelectionLeft, ref _layout); - var rightEdge = font.GetCharPosition(_text, SelectionRight, ref _layout); - float fontHeight = font.Height / DpiScale; + var leftEdge = EnableFontFallback ? + font.GetCharPosition(_text, SelectionLeft, ref _layout) : + font.GetCharPositionInternal(_text, SelectionLeft, ref _layout); + var rightEdge = EnableFontFallback ? + font.GetCharPosition(_text, SelectionRight, ref _layout) : + font.GetCharPositionInternal(_text, SelectionRight, ref _layout); + float fontHeight = font.GetMaxHeight() / DpiScale; // Draw selection background float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); @@ -210,11 +226,19 @@ namespace FlaxEngine.GUI var color = TextColor; if (!enabled) color *= 0.6f; - Render2D.DrawText(font, _text, color, ref _layout, TextMaterial); + if (EnableFontFallback) + Render2D.DrawText(font, _text, color, ref _layout, TextMaterial); + else + // Draw without fallback + Render2D.DrawTextInternal(font, _text, color, ref _layout, TextMaterial); } else if (!string.IsNullOrEmpty(_watermarkText) && !IsFocused) { - Render2D.DrawText(font, _watermarkText, WatermarkTextColor, ref _layout, TextMaterial); + if (EnableFontFallback) + Render2D.DrawText(font, _watermarkText, WatermarkTextColor, ref _layout, TextMaterial); + else + // Draw without fallback + Render2D.DrawTextInternal(font, _watermarkText, WatermarkTextColor, ref _layout, TextMaterial); } // Caret